Compare commits
8 Commits
@nhost/goo
...
@nhost/nho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f33e07b191 | ||
|
|
017f1a6c7b | ||
|
|
93957c8af3 | ||
|
|
2505b2e26b | ||
|
|
5f4b4d2acc | ||
|
|
71a8ce4446 | ||
|
|
5ef5189898 | ||
|
|
791b7295fb |
@@ -1,5 +1,25 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 1.6.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@9.0.0
|
||||
- @nhost/nextjs@2.1.2
|
||||
|
||||
## 1.6.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@8.0.1
|
||||
- @nhost/nextjs@2.1.1
|
||||
|
||||
## 1.6.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5ef5189: fix: update `@apollo/client` to `3.9.4` to fix a cache bug
|
||||
|
||||
## 1.6.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -4,7 +4,6 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
});
|
||||
const { version } = require('./package.json');
|
||||
|
||||
|
||||
const cspHeader = `
|
||||
default-src 'self' *.nhost.run ws://*.nhost.run nhost.run ws://nhost.run;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' cdn.segment.com js.stripe.com;
|
||||
@@ -19,7 +18,7 @@ const cspHeader = `
|
||||
frame-src 'self' js.stripe.com;
|
||||
block-all-mixed-content;
|
||||
upgrade-insecure-requests;
|
||||
`
|
||||
`;
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
reactStrictMode: true,
|
||||
@@ -41,7 +40,7 @@ module.exports = withBundleAnalyzer({
|
||||
headers: [
|
||||
{
|
||||
key: 'X-Frame-Options',
|
||||
value: 'SAMEORIGIN'
|
||||
value: 'SAMEORIGIN',
|
||||
},
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
@@ -49,7 +48,7 @@ module.exports = withBundleAnalyzer({
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -19,7 +19,7 @@
|
||||
"e2e": "pnpm install-browsers && pnpm playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.1",
|
||||
"@apollo/client": "^3.9.4",
|
||||
"@codemirror/lang-sql": "^6.5.5",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.3",
|
||||
@@ -106,7 +106,7 @@
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/builder-webpack5": "^6.5.16",
|
||||
"@storybook/manager-webpack5": "^6.5.16",
|
||||
"@storybook/react": "^6.5.16",
|
||||
"@storybook/react": "^7.6.15",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
|
||||
@@ -25,12 +25,12 @@ function Plan({ planName, price, setPlan, planId, selectedPlanId }: any) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="my-4 grid w-full grid-flow-col items-center justify-between gap-2 px-1"
|
||||
className="grid items-center justify-between w-full grid-flow-col gap-2 px-1 my-4"
|
||||
onClick={setPlan}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="grid grid-flow-row gap-y-0.5">
|
||||
<div className="grid grid-flow-col items-center justify-start gap-2">
|
||||
<div className="grid items-center justify-start grid-flow-col gap-2">
|
||||
<Checkbox
|
||||
onChange={setPlan}
|
||||
checked={selectedPlanId === planId}
|
||||
@@ -40,7 +40,7 @@ function Plan({ planName, price, setPlan, planId, selectedPlanId }: any) {
|
||||
<Text
|
||||
variant="h3"
|
||||
component="p"
|
||||
className="self-center text-left font-medium"
|
||||
className="self-center font-medium text-left"
|
||||
>
|
||||
Upgrade to {planName}
|
||||
</Text>
|
||||
@@ -156,7 +156,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
|
||||
if (pollingCurrentProject) {
|
||||
return (
|
||||
<Box className="mx-auto w-full max-w-xl rounded-lg p-6 text-left">
|
||||
<Box className="w-full max-w-xl p-6 mx-auto text-left rounded-lg">
|
||||
<div className="flex flex-col">
|
||||
<div className="mx-auto">
|
||||
<Image
|
||||
@@ -179,7 +179,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className="mx-auto mt-4 w-full max-w-sm"
|
||||
className="w-full max-w-sm mx-auto mt-4"
|
||||
onClick={() => {
|
||||
if (close) {
|
||||
close();
|
||||
@@ -196,7 +196,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="w-full max-w-xl rounded-lg p-6 text-left">
|
||||
<Box className="w-full max-w-xl p-6 text-left rounded-lg">
|
||||
<BaseDialog
|
||||
open={showPaymentModal}
|
||||
onClose={() => setShowPaymentModal(false)}
|
||||
@@ -241,7 +241,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid grid-flow-row gap-2">
|
||||
<div className="grid grid-flow-row gap-2 mt-2">
|
||||
<Button
|
||||
onClick={handleChangePlanClick}
|
||||
disabled={!selectedPlan}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/docs
|
||||
|
||||
## 2.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 791b729: fix: remove auth method
|
||||
|
||||
## 2.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/docs",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "mintlify dev"
|
||||
|
||||
@@ -11,18 +11,9 @@ info:
|
||||
servers:
|
||||
- url: https://local.auth.nhost.run/v1
|
||||
description: API Server
|
||||
security:
|
||||
- AdminSecret: []
|
||||
- BearerAuth: []
|
||||
|
||||
components:
|
||||
|
||||
securitySchemes:
|
||||
AdminSecret:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-Hasura-Admin-Secret
|
||||
description: Hasura Admin Secret
|
||||
BearerAuth:
|
||||
scheme: bearer
|
||||
type: http
|
||||
@@ -1524,7 +1515,6 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DisabledEndpointError'
|
||||
description: The feature is not activated
|
||||
security: []
|
||||
summary: Sign In TOTP
|
||||
tags:
|
||||
- Authentication
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/cli
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.5
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.4
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/cli",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.6",
|
||||
"main": "src/index.mjs",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# @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
|
||||
|
||||
- @nhost/react@3.1.1
|
||||
- @nhost/react-apollo@8.0.1
|
||||
|
||||
## 0.1.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-apollo",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.14",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen",
|
||||
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.1",
|
||||
"@apollo/client": "^3.9.4",
|
||||
"@nhost/react": "workspace:^",
|
||||
"@nhost/react-apollo": "workspace:^",
|
||||
"clsx": "^1.2.1",
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @nhost-examples/codegen-react-query
|
||||
|
||||
## 0.1.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [017f1a6]
|
||||
- @nhost/react@3.2.0
|
||||
|
||||
## 0.1.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.1.1
|
||||
|
||||
## 0.1.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-query",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.15",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen",
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# @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
|
||||
|
||||
- @nhost/react@3.1.1
|
||||
- @nhost/react-urql@5.0.1
|
||||
|
||||
## 0.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-urql",
|
||||
"private": true,
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.11",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/multi-tenant-one-to-many
|
||||
|
||||
## 2.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.5
|
||||
|
||||
## 2.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.4
|
||||
|
||||
## 2.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/multi-tenant-one-to-many",
|
||||
"private": true,
|
||||
"version": "2.0.2",
|
||||
"version": "2.0.4",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# @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
|
||||
|
||||
- @nhost/react@3.1.1
|
||||
- @nhost/react-apollo@8.0.1
|
||||
- @nhost/nextjs@2.1.1
|
||||
|
||||
## 0.1.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/nextjs",
|
||||
"version": "0.1.14",
|
||||
"version": "0.1.16",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -15,7 +15,7 @@
|
||||
"verify:fix": "run-p prettier:fix lint:fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.1",
|
||||
"@apollo/client": "^3.9.4",
|
||||
"@mantine/core": "^4.2.12",
|
||||
"@mantine/hooks": "^4.2.12",
|
||||
"@mantine/next": "^4.2.12",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/node-storage
|
||||
|
||||
## 0.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.5
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.4
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/node-storage",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.8",
|
||||
"private": true,
|
||||
"description": "This is an example of how to use the Storage with Node.js",
|
||||
"main": "src/index.mjs",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost-examples/nextjs-server-components
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.5
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.4
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/nextjs-server-components",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -9,7 +9,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.1",
|
||||
"@apollo/client": "^3.9.4",
|
||||
"@nhost/nhost-js": "workspace:^",
|
||||
"autoprefixer": "10.4.15",
|
||||
"cookies-next": "^3.0.0",
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# @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
|
||||
|
||||
- @nhost/react@3.1.1
|
||||
- @nhost/react-apollo@8.0.1
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -46,4 +46,4 @@ delete_permissions:
|
||||
permission:
|
||||
filter:
|
||||
user_id:
|
||||
_eq: X-Hasura-User-Id
|
||||
_eq: x-hasura-auth-elevated
|
||||
|
||||
@@ -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
|
||||
@@ -11,3 +11,4 @@
|
||||
- "!include public_todos.yaml"
|
||||
- "!include storage_buckets.yaml"
|
||||
- "!include storage_files.yaml"
|
||||
- "!include storage_virus.yaml"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@nhost-examples/react-apollo",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.1",
|
||||
"@apollo/client": "^3.9.4",
|
||||
"@mantine/core": "^4.2.12",
|
||||
"@mantine/dropzone": "^4.2.12",
|
||||
"@mantine/hooks": "^4.2.12",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
1
examples/react-apollo/src/utils/index.ts
Normal file
1
examples/react-apollo/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './security-keys'
|
||||
18
examples/react-apollo/src/utils/security-keys.ts
Normal file
18
examples/react-apollo/src/utils/security-keys.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -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()]
|
||||
})
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @nhost-examples/react-gqty
|
||||
|
||||
## 1.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [017f1a6]
|
||||
- @nhost/react@3.2.0
|
||||
|
||||
## 1.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.1.1
|
||||
|
||||
## 1.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/react-gqty",
|
||||
"private": true,
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# @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
|
||||
|
||||
- @nhost/nhost-js@3.0.4
|
||||
- @nhost/apollo@6.0.4
|
||||
- @nhost/vue@2.1.1
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -11,3 +11,4 @@
|
||||
- "!include public_notes.yaml"
|
||||
- "!include storage_buckets.yaml"
|
||||
- "!include storage_files.yaml"
|
||||
- "!include storage_virus.yaml"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/vue-apollo",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
@@ -14,7 +14,7 @@
|
||||
"verify:fix": "run-p prettier:fix lint:fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.1",
|
||||
"@apollo/client": "^3.9.4",
|
||||
"@mdi/font": "5.9.55",
|
||||
"@nhost/apollo": "workspace:^",
|
||||
"@nhost/nhost-js": "workspace:^",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# @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
|
||||
|
||||
- @nhost/apollo@6.0.4
|
||||
- @nhost/vue@2.1.1
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/vue-quickstart",
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.13",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
@@ -11,7 +11,7 @@
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.1",
|
||||
"@apollo/client": "^3.9.4",
|
||||
"@nhost/apollo": "workspace:^",
|
||||
"@nhost/vue": "workspace:^",
|
||||
"@vue/apollo-composable": "4.0.0-alpha.18",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost/apollo
|
||||
|
||||
## 6.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.5
|
||||
|
||||
## 6.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.0.4
|
||||
|
||||
## 6.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/apollo",
|
||||
"version": "6.0.3",
|
||||
"version": "6.0.5",
|
||||
"description": "Nhost Apollo Client library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -68,7 +68,7 @@
|
||||
"graphql-ws": "^5.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apollo/client": "^3.9.1",
|
||||
"@apollo/client": "^3.9.4",
|
||||
"@nhost/nhost-js": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,20 @@
|
||||
# @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
|
||||
|
||||
- @nhost/apollo@6.0.4
|
||||
- @nhost/react@3.1.1
|
||||
|
||||
## 8.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react-apollo",
|
||||
"version": "8.0.0",
|
||||
"version": "9.0.0",
|
||||
"description": "Nhost React Apollo client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -71,7 +71,7 @@
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apollo/client": "^3.9.1",
|
||||
"@apollo/client": "^3.9.4",
|
||||
"@nhost/react": "workspace:*",
|
||||
"@types/react": "^18.2.50",
|
||||
"graphql": "16.8.1",
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @nhost/react-urql
|
||||
|
||||
## 6.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [017f1a6]
|
||||
- @nhost/react@3.2.0
|
||||
|
||||
## 5.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.1.1
|
||||
|
||||
## 5.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react-urql",
|
||||
"version": "5.0.0",
|
||||
"version": "6.0.0",
|
||||
"description": "Nhost React URQL client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/hasura-auth-js
|
||||
|
||||
## 2.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 017f1a6: feat: add elevated permission examples
|
||||
|
||||
## 2.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/hasura-auth-js",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"description": "Hasura-auth client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/hasura-storage-js
|
||||
|
||||
## 2.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 2505b2e: fix: fix headers sent with getPresignedUrl
|
||||
|
||||
## 2.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/hasura-storage-js",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"description": "Hasura-storage client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -99,12 +99,6 @@ export class HasuraStorageApi {
|
||||
async downloadFile(params: StorageDownloadFileParams): Promise<StorageDownloadFileResponse> {
|
||||
try {
|
||||
const { fileId, headers: customHeaders = {}, ...imageTransformationParams } = params
|
||||
const authHeaders = this.generateAuthHeaders()
|
||||
|
||||
const headers = new Headers(customHeaders)
|
||||
authHeaders.forEach((value, key) => {
|
||||
headers?.append(key, value)
|
||||
})
|
||||
|
||||
const urlWithParams = appendImageTransformationParameters(
|
||||
`${this.url}/files/${fileId}`,
|
||||
@@ -113,7 +107,7 @@ export class HasuraStorageApi {
|
||||
|
||||
const response = await fetch(urlWithParams, {
|
||||
method: 'GET',
|
||||
headers
|
||||
headers: {...this.generateAuthHeaders(), ...customHeaders}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -131,6 +125,7 @@ export class HasuraStorageApi {
|
||||
async getPresignedUrl(params: ApiGetPresignedUrlParams): Promise<ApiGetPresignedUrlResponse> {
|
||||
try {
|
||||
const { fileId } = params
|
||||
|
||||
const response = await fetch(`${this.url}/files/${fileId}/presignedurl`, {
|
||||
method: 'GET',
|
||||
headers: this.generateAuthHeaders()
|
||||
@@ -185,16 +180,19 @@ export class HasuraStorageApi {
|
||||
return this
|
||||
}
|
||||
|
||||
private generateAuthHeaders(): Headers {
|
||||
const headers = new Headers()
|
||||
private generateAuthHeaders(): HeadersInit | undefined {
|
||||
if (!this.adminSecret && !this.accessToken) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (this.adminSecret) {
|
||||
headers.append('x-hasura-admin-secret', this.adminSecret)
|
||||
}
|
||||
if (this.accessToken) {
|
||||
headers.append('Authorization', `Bearer ${this.accessToken}`)
|
||||
return {
|
||||
'x-hasura-admin-secret': this.adminSecret
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
return {
|
||||
Authorization: `Bearer ${this.accessToken}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @nhost/nextjs
|
||||
|
||||
## 2.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [017f1a6]
|
||||
- @nhost/react@3.2.0
|
||||
|
||||
## 2.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.1.1
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/nextjs",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.2",
|
||||
"description": "Nhost NextJS library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @nhost/nhost-js
|
||||
|
||||
## 3.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [017f1a6]
|
||||
- @nhost/hasura-auth-js@2.3.0
|
||||
|
||||
## 3.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2505b2e]
|
||||
- @nhost/hasura-storage-js@2.4.0
|
||||
|
||||
## 3.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/nhost-js",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.5",
|
||||
"description": "Nhost JavaScript SDK",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @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
|
||||
|
||||
- @nhost/nhost-js@3.0.4
|
||||
|
||||
## 3.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0",
|
||||
"description": "Nhost React library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @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
|
||||
|
||||
- @nhost/nhost-js@3.0.4
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/vue",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.0",
|
||||
"description": "Nhost Vue library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -34,3 +34,4 @@ export * from './useUserRoles'
|
||||
export * from './useElevateSecurityKeyEmail'
|
||||
export * from './useSignUpEmailSecurityKey'
|
||||
export * from './useSignInEmailSecurityKey'
|
||||
export * from './useAddSecurityKey'
|
||||
|
||||
67
packages/vue/src/useAddSecurityKey.ts
Normal file
67
packages/vue/src/useAddSecurityKey.ts
Normal 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 }
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
2399
pnpm-lock.yaml
generated
2399
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user