Compare commits
8 Commits
@nhost/rea
...
@nhost/nex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f33e07b191 | ||
|
|
017f1a6c7b | ||
|
|
93957c8af3 | ||
|
|
2505b2e26b | ||
|
|
5f4b4d2acc | ||
|
|
71a8ce4446 | ||
|
|
5ef5189898 | ||
|
|
791b7295fb |
@@ -1,5 +1,25 @@
|
|||||||
# @nhost/dashboard
|
# @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
|
## 1.6.6
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
|||||||
});
|
});
|
||||||
const { version } = require('./package.json');
|
const { version } = require('./package.json');
|
||||||
|
|
||||||
|
|
||||||
const cspHeader = `
|
const cspHeader = `
|
||||||
default-src 'self' *.nhost.run ws://*.nhost.run nhost.run ws://nhost.run;
|
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;
|
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;
|
frame-src 'self' js.stripe.com;
|
||||||
block-all-mixed-content;
|
block-all-mixed-content;
|
||||||
upgrade-insecure-requests;
|
upgrade-insecure-requests;
|
||||||
`
|
`;
|
||||||
|
|
||||||
module.exports = withBundleAnalyzer({
|
module.exports = withBundleAnalyzer({
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
@@ -41,7 +40,7 @@ module.exports = withBundleAnalyzer({
|
|||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: 'X-Frame-Options',
|
key: 'X-Frame-Options',
|
||||||
value: 'SAMEORIGIN'
|
value: 'SAMEORIGIN',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Content-Security-Policy',
|
key: 'Content-Security-Policy',
|
||||||
@@ -49,7 +48,7 @@ module.exports = withBundleAnalyzer({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
},
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/dashboard",
|
"name": "@nhost/dashboard",
|
||||||
"version": "1.6.6",
|
"version": "1.6.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"e2e": "pnpm install-browsers && pnpm playwright test"
|
"e2e": "pnpm install-browsers && pnpm playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.9.1",
|
"@apollo/client": "^3.9.4",
|
||||||
"@codemirror/lang-sql": "^6.5.5",
|
"@codemirror/lang-sql": "^6.5.5",
|
||||||
"@emotion/cache": "^11.11.0",
|
"@emotion/cache": "^11.11.0",
|
||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.11.3",
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
"@storybook/addon-postcss": "^2.0.0",
|
"@storybook/addon-postcss": "^2.0.0",
|
||||||
"@storybook/builder-webpack5": "^6.5.16",
|
"@storybook/builder-webpack5": "^6.5.16",
|
||||||
"@storybook/manager-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",
|
"@storybook/testing-library": "^0.2.2",
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"@testing-library/dom": "^9.3.4",
|
"@testing-library/dom": "^9.3.4",
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ function Plan({ planName, price, setPlan, planId, selectedPlanId }: any) {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="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}
|
onClick={setPlan}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<div className="grid grid-flow-row gap-y-0.5">
|
<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
|
<Checkbox
|
||||||
onChange={setPlan}
|
onChange={setPlan}
|
||||||
checked={selectedPlanId === planId}
|
checked={selectedPlanId === planId}
|
||||||
@@ -40,7 +40,7 @@ function Plan({ planName, price, setPlan, planId, selectedPlanId }: any) {
|
|||||||
<Text
|
<Text
|
||||||
variant="h3"
|
variant="h3"
|
||||||
component="p"
|
component="p"
|
||||||
className="self-center text-left font-medium"
|
className="self-center font-medium text-left"
|
||||||
>
|
>
|
||||||
Upgrade to {planName}
|
Upgrade to {planName}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -156,7 +156,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
|||||||
|
|
||||||
if (pollingCurrentProject) {
|
if (pollingCurrentProject) {
|
||||||
return (
|
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="flex flex-col">
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<Image
|
<Image
|
||||||
@@ -179,7 +179,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
|||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className="mx-auto mt-4 w-full max-w-sm"
|
className="w-full max-w-sm mx-auto mt-4"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (close) {
|
if (close) {
|
||||||
close();
|
close();
|
||||||
@@ -196,7 +196,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<BaseDialog
|
||||||
open={showPaymentModal}
|
open={showPaymentModal}
|
||||||
onClose={() => setShowPaymentModal(false)}
|
onClose={() => setShowPaymentModal(false)}
|
||||||
@@ -241,7 +241,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 grid grid-flow-row gap-2">
|
<div className="grid grid-flow-row gap-2 mt-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleChangePlanClick}
|
onClick={handleChangePlanClick}
|
||||||
disabled={!selectedPlan}
|
disabled={!selectedPlan}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# @nhost/docs
|
# @nhost/docs
|
||||||
|
|
||||||
|
## 2.4.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 791b729: fix: remove auth method
|
||||||
|
|
||||||
## 2.3.0
|
## 2.3.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/docs",
|
"name": "@nhost/docs",
|
||||||
"version": "2.3.0",
|
"version": "2.4.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "mintlify dev"
|
"start": "mintlify dev"
|
||||||
|
|||||||
@@ -11,18 +11,9 @@ info:
|
|||||||
servers:
|
servers:
|
||||||
- url: https://local.auth.nhost.run/v1
|
- url: https://local.auth.nhost.run/v1
|
||||||
description: API Server
|
description: API Server
|
||||||
security:
|
|
||||||
- AdminSecret: []
|
|
||||||
- BearerAuth: []
|
|
||||||
|
|
||||||
components:
|
components:
|
||||||
|
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
AdminSecret:
|
|
||||||
type: apiKey
|
|
||||||
in: header
|
|
||||||
name: X-Hasura-Admin-Secret
|
|
||||||
description: Hasura Admin Secret
|
|
||||||
BearerAuth:
|
BearerAuth:
|
||||||
scheme: bearer
|
scheme: bearer
|
||||||
type: http
|
type: http
|
||||||
@@ -1524,7 +1515,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/DisabledEndpointError'
|
$ref: '#/components/schemas/DisabledEndpointError'
|
||||||
description: The feature is not activated
|
description: The feature is not activated
|
||||||
security: []
|
|
||||||
summary: Sign In TOTP
|
summary: Sign In TOTP
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# @nhost-examples/cli
|
# @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
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/cli",
|
"name": "@nhost-examples/cli",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"main": "src/index.mjs",
|
"main": "src/index.mjs",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
# @nhost-examples/codegen-react-apollo
|
# @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
|
## 0.1.12
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/codegen-react-apollo",
|
"name": "@nhost-examples/codegen-react-apollo",
|
||||||
"version": "0.1.12",
|
"version": "0.1.14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"codegen": "graphql-codegen",
|
"codegen": "graphql-codegen",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.9.1",
|
"@apollo/client": "^3.9.4",
|
||||||
"@nhost/react": "workspace:^",
|
"@nhost/react": "workspace:^",
|
||||||
"@nhost/react-apollo": "workspace:^",
|
"@nhost/react-apollo": "workspace:^",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
# @nhost-examples/codegen-react-query
|
# @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
|
## 0.1.13
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/codegen-react-query",
|
"name": "@nhost-examples/codegen-react-query",
|
||||||
"version": "0.1.13",
|
"version": "0.1.15",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"codegen": "graphql-codegen",
|
"codegen": "graphql-codegen",
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
# @nhost-examples/react-urql
|
# @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
|
## 0.0.9
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/codegen-react-urql",
|
"name": "@nhost-examples/codegen-react-urql",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.9",
|
"version": "0.0.11",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# @nhost-examples/multi-tenant-one-to-many
|
# @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
|
## 2.0.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/multi-tenant-one-to-many",
|
"name": "@nhost-examples/multi-tenant-one-to-many",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.2",
|
"version": "2.0.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
# @nhost-examples/nextjs
|
# @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
|
## 0.1.14
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/nextjs",
|
"name": "@nhost-examples/nextjs",
|
||||||
"version": "0.1.14",
|
"version": "0.1.16",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"verify:fix": "run-p prettier:fix lint:fix"
|
"verify:fix": "run-p prettier:fix lint:fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.9.1",
|
"@apollo/client": "^3.9.4",
|
||||||
"@mantine/core": "^4.2.12",
|
"@mantine/core": "^4.2.12",
|
||||||
"@mantine/hooks": "^4.2.12",
|
"@mantine/hooks": "^4.2.12",
|
||||||
"@mantine/next": "^4.2.12",
|
"@mantine/next": "^4.2.12",
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# @nhost-examples/node-storage
|
# @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
|
## 0.0.6
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/node-storage",
|
"name": "@nhost-examples/node-storage",
|
||||||
"version": "0.0.6",
|
"version": "0.0.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "This is an example of how to use the Storage with Node.js",
|
"description": "This is an example of how to use the Storage with Node.js",
|
||||||
"main": "src/index.mjs",
|
"main": "src/index.mjs",
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# @nhost-examples/nextjs-server-components
|
# @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
|
## 0.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/nextjs-server-components",
|
"name": "@nhost-examples/nextjs-server-components",
|
||||||
"version": "0.2.0",
|
"version": "0.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.9.1",
|
"@apollo/client": "^3.9.4",
|
||||||
"@nhost/nhost-js": "workspace:^",
|
"@nhost/nhost-js": "workspace:^",
|
||||||
"autoprefixer": "10.4.15",
|
"autoprefixer": "10.4.15",
|
||||||
"cookies-next": "^3.0.0",
|
"cookies-next": "^3.0.0",
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
# @nhost-examples/react-apollo
|
# @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
|
## 0.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -46,4 +46,4 @@ delete_permissions:
|
|||||||
permission:
|
permission:
|
||||||
filter:
|
filter:
|
||||||
user_id:
|
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 public_todos.yaml"
|
||||||
- "!include storage_buckets.yaml"
|
- "!include storage_buckets.yaml"
|
||||||
- "!include storage_files.yaml"
|
- "!include storage_files.yaml"
|
||||||
|
- "!include storage_virus.yaml"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[global]
|
[global]
|
||||||
|
|
||||||
[hasura]
|
[hasura]
|
||||||
version = 'v2.25.1-ce'
|
version = 'v2.33.4-ce'
|
||||||
adminSecret = '{{ secrets.HASURA_GRAPHQL_ADMIN_SECRET }}'
|
adminSecret = '{{ secrets.HASURA_GRAPHQL_ADMIN_SECRET }}'
|
||||||
webhookSecret = '{{ secrets.NHOST_WEBHOOK_SECRET }}'
|
webhookSecret = '{{ secrets.NHOST_WEBHOOK_SECRET }}'
|
||||||
|
|
||||||
@@ -28,7 +28,10 @@ httpPoolSize = 100
|
|||||||
version = 18
|
version = 18
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
version = '0.25.0'
|
version = '0.26.0'
|
||||||
|
|
||||||
|
[auth.elevatedPrivileges]
|
||||||
|
mode = 'required'
|
||||||
|
|
||||||
[auth.redirections]
|
[auth.redirections]
|
||||||
clientUrl = 'https://react-apollo.example.nhost.io/'
|
clientUrl = 'https://react-apollo.example.nhost.io/'
|
||||||
@@ -149,12 +152,12 @@ enabled = true
|
|||||||
issuer = 'nhost'
|
issuer = 'nhost'
|
||||||
|
|
||||||
[postgres]
|
[postgres]
|
||||||
version = '14.6-20230406-2'
|
version = '14.6-20240129-1'
|
||||||
|
|
||||||
[provider]
|
[provider]
|
||||||
|
|
||||||
[storage]
|
[storage]
|
||||||
version = '0.3.4'
|
version = '0.6.0'
|
||||||
|
|
||||||
[observability]
|
[observability]
|
||||||
[observability.grafana]
|
[observability.grafana]
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/react-apollo",
|
"name": "@nhost-examples/react-apollo",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.9.1",
|
"@apollo/client": "^3.9.4",
|
||||||
"@mantine/core": "^4.2.12",
|
"@mantine/core": "^4.2.12",
|
||||||
"@mantine/dropzone": "^4.2.12",
|
"@mantine/dropzone": "^4.2.12",
|
||||||
"@mantine/hooks": "^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 { SiApollographql } from 'react-icons/si'
|
||||||
import { useLocation, useNavigate } from 'react-router'
|
import { useLocation, useNavigate } from 'react-router'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
import { showNotification } from '@mantine/notifications'
|
||||||
|
|
||||||
import { Group, MantineColor, Navbar, Text, ThemeIcon, UnstyledButton } from '@mantine/core'
|
import {
|
||||||
import { useAuthenticated, useSignOut } from '@nhost/react'
|
Button,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
MantineColor,
|
||||||
|
Navbar,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
UnstyledButton
|
||||||
|
} from '@mantine/core'
|
||||||
|
import { useAuthenticated, useElevateSecurityKeyEmail, useSignOut, useUserData } from '@nhost/react'
|
||||||
interface MenuItemProps {
|
interface MenuItemProps {
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
color?: MantineColor
|
color?: MantineColor
|
||||||
@@ -59,10 +69,45 @@ const data: MenuItemProps[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default function NavBar() {
|
export default function NavBar() {
|
||||||
const authenticated = useAuthenticated()
|
const userData = useUserData()
|
||||||
const { signOut } = useSignOut()
|
|
||||||
const navigate = useNavigate()
|
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} />)
|
const links = data.map((link) => <MenuItem {...link} key={link.label} />)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar width={{ sm: 300, lg: 400, base: 100 }} aria-label="main navigation">
|
<Navbar width={{ sm: 300, lg: 400, base: 100 }} aria-label="main navigation">
|
||||||
<Navbar.Section grow mt="md">
|
<Navbar.Section grow mt="md">
|
||||||
@@ -78,6 +123,13 @@ export default function NavBar() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Navbar.Section>
|
</Navbar.Section>
|
||||||
|
|
||||||
|
<Card p="lg" m="sm">
|
||||||
|
<Group position="apart">
|
||||||
|
<span>Elevated permissions: {String(elevated)}</span>
|
||||||
|
<Button onClick={handleElevate}>Elevate</Button>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
</Navbar>
|
</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 { gql, useMutation } from '@apollo/client'
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +22,8 @@ import { showNotification } from '@mantine/notifications'
|
|||||||
import { useAuthQuery } from '@nhost/react-apollo'
|
import { useAuthQuery } from '@nhost/react-apollo'
|
||||||
import { useElevateSecurityKeyEmail, useUserData } from '@nhost/react'
|
import { useElevateSecurityKeyEmail, useUserData } from '@nhost/react'
|
||||||
import { FaTrash } from 'react-icons/fa'
|
import { FaTrash } from 'react-icons/fa'
|
||||||
|
import { SECURITY_KEYS_LIST } from 'src/utils'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
const NOTES_LIST = gql`
|
const NOTES_LIST = gql`
|
||||||
query notesList {
|
query notesList {
|
||||||
@@ -54,15 +61,44 @@ export const NotesPage: React.FC = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [content, setContent] = useInputState('')
|
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 [addNoteMutation] = useMutation<InsertNoteMutation>(INSERT_NOTE)
|
||||||
const [deleteNoteMutation] = useMutation<DeleteNoteMutation>(DELETE_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
|
if (!content) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkElevatedPermission()
|
||||||
|
} catch (error) {
|
||||||
|
showNotification({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Could not elevate permissions'
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
addNoteMutation({
|
addNoteMutation({
|
||||||
variables: { content },
|
variables: { content },
|
||||||
onCompleted: () => setContent(''),
|
onCompleted: () => setContent(''),
|
||||||
@@ -94,9 +130,20 @@ export const NotesPage: React.FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteNote = (noteId: string) => {
|
const deleteNote = async (noteId: string) => {
|
||||||
if (!noteId) return
|
if (!noteId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkElevatedPermission()
|
||||||
|
} catch (error) {
|
||||||
|
showNotification({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Could not elevate permissions'
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
deleteNoteMutation({
|
deleteNoteMutation({
|
||||||
variables: { noteId },
|
variables: { noteId },
|
||||||
onCompleted: () => setContent(''),
|
onCompleted: () => setContent(''),
|
||||||
@@ -126,20 +173,6 @@ export const NotesPage: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
{loading && <Loader />}
|
{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">
|
<Card shadow="sm" p="lg" m="sm">
|
||||||
<Title>Secret Notes</Title>
|
<Title>Secret Notes</Title>
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|||||||
@@ -2,11 +2,25 @@ import { useState } from 'react'
|
|||||||
|
|
||||||
import { Button, Card, Grid, TextInput, Title } from '@mantine/core'
|
import { Button, Card, Grid, TextInput, Title } from '@mantine/core'
|
||||||
import { showNotification } from '@mantine/notifications'
|
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 = () => {
|
export const ChangeEmail: React.FC = () => {
|
||||||
const [newEmail, setNewEmail] = useState('')
|
const userId = useUserId()
|
||||||
const email = useUserEmail()
|
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({
|
const { changeEmail } = useChangeEmail({
|
||||||
redirectTo: '/profile'
|
redirectTo: '/profile'
|
||||||
})
|
})
|
||||||
@@ -19,7 +33,26 @@ export const ChangeEmail: React.FC = () => {
|
|||||||
})
|
})
|
||||||
return
|
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)
|
const result = await changeEmail(newEmail)
|
||||||
|
|
||||||
if (result.needsEmailVerification) {
|
if (result.needsEmailVerification) {
|
||||||
showNotification({
|
showNotification({
|
||||||
message: `An email has been sent to ${newEmail}. Please check your inbox and follow the link to confirm the email change.`
|
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 (
|
return (
|
||||||
<Card shadow="sm" p="lg" m="sm">
|
<Card shadow="sm" p="lg" m="sm">
|
||||||
<Title>Change email</Title>
|
<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 { Button, Card, Grid, PasswordInput, Title } from '@mantine/core'
|
||||||
import { showNotification } from '@mantine/notifications'
|
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 = () => {
|
export const ChangePassword: React.FC = () => {
|
||||||
|
const userEmail = useUserEmail()
|
||||||
|
const userId = useUserId()
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const { changePassword } = useChangePassword()
|
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 () => {
|
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)
|
const result = await changePassword(password)
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
showNotification({
|
showNotification({
|
||||||
|
|||||||
@@ -2,11 +2,56 @@ import { useState } from 'react'
|
|||||||
|
|
||||||
import { Button, Card, TextInput, Title } from '@mantine/core'
|
import { Button, Card, TextInput, Title } from '@mantine/core'
|
||||||
import { showNotification } from '@mantine/notifications'
|
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 = () => {
|
export const Mfa: React.FC = () => {
|
||||||
|
const userId = useUserId()
|
||||||
|
const userEmail = useUserEmail()
|
||||||
const [code, setCode] = useState('')
|
const [code, setCode] = useState('')
|
||||||
const { generateQrCode, activateMfa, isActivated, isGenerated, qrCodeDataUrl } = useConfigMfa()
|
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 generate = async () => {
|
||||||
const result = await generateQrCode()
|
const result = await generateQrCode()
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
@@ -33,7 +78,7 @@ export const Mfa: React.FC = () => {
|
|||||||
onChange={(e) => setCode(e.target.value)}
|
onChange={(e) => setCode(e.target.value)}
|
||||||
placeholder="Enter activation code"
|
placeholder="Enter activation code"
|
||||||
/>
|
/>
|
||||||
<Button fullWidth onClick={() => activateMfa(code)}>
|
<Button fullWidth onClick={activate}>
|
||||||
Activate
|
Activate
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,36 +2,22 @@ import { useState } from 'react'
|
|||||||
import { FaMinus } from 'react-icons/fa'
|
import { FaMinus } from 'react-icons/fa'
|
||||||
import { RemoveSecurityKeyMutation, SecurityKeysQuery } from 'src/generated'
|
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 { ActionIcon, Button, Card, SimpleGrid, Table, TextInput, Title } from '@mantine/core'
|
||||||
import { useInputState } from '@mantine/hooks'
|
import { useInputState } from '@mantine/hooks'
|
||||||
import { showNotification } from '@mantine/notifications'
|
import { showNotification } from '@mantine/notifications'
|
||||||
import { useAddSecurityKey, useUserId } from '@nhost/react'
|
import { useAddSecurityKey, useUserId } from '@nhost/react'
|
||||||
import { useAuthQuery } from '@nhost/react-apollo'
|
import { useAuthQuery } from '@nhost/react-apollo'
|
||||||
|
import { REMOVE_SECURITY_KEY, SECURITY_KEYS_LIST } from 'src/utils'
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const SecurityKeys: React.FC = () => {
|
export const SecurityKeys: React.FC = () => {
|
||||||
const { add } = useAddSecurityKey()
|
|
||||||
const userId = useUserId()
|
const userId = useUserId()
|
||||||
|
const client = useApolloClient()
|
||||||
|
const { add } = useAddSecurityKey()
|
||||||
// Nickname of the security key
|
// Nickname of the security key
|
||||||
const [nickname, setNickname] = useInputState('')
|
const [nickname, setNickname] = useInputState('')
|
||||||
const [list, setList] = useState<{ id: string; nickname?: string | null }[]>([])
|
const [list, setList] = useState<{ id: string; nickname?: string | null }[]>([])
|
||||||
|
|
||||||
useAuthQuery<SecurityKeysQuery>(SECURITY_KEYS_LIST, {
|
useAuthQuery<SecurityKeysQuery>(SECURITY_KEYS_LIST, {
|
||||||
variables: { userId },
|
variables: { userId },
|
||||||
onCompleted: ({ authUserSecurityKeys }) => {
|
onCompleted: ({ authUserSecurityKeys }) => {
|
||||||
@@ -43,9 +29,10 @@ export const SecurityKeys: React.FC = () => {
|
|||||||
|
|
||||||
const addKey = async (e: React.FormEvent<HTMLFormElement>) => {
|
const addKey = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const { key, isError, error } = await add(nickname)
|
const { key, isError, error } = await add(nickname)
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
console.log(error)
|
|
||||||
showNotification({
|
showNotification({
|
||||||
color: 'red',
|
color: 'red',
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
@@ -53,11 +40,17 @@ export const SecurityKeys: React.FC = () => {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setNickname('')
|
setNickname('')
|
||||||
|
|
||||||
|
// refetch securityKeys so that we know if need to elevate in other components
|
||||||
|
await client.refetchQueries({
|
||||||
|
include: [SECURITY_KEYS_LIST]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (key) {
|
if (key) {
|
||||||
setList([...list, key])
|
setList([...list, key])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [removeKey] = useMutation<RemoveSecurityKeyMutation>(REMOVE_SECURITY_KEY, {
|
const [removeKey] = useMutation<RemoveSecurityKeyMutation>(REMOVE_SECURITY_KEY, {
|
||||||
onCompleted: ({ deleteAuthUserSecurityKey }) => {
|
onCompleted: ({ deleteAuthUserSecurityKey }) => {
|
||||||
if (deleteAuthUserSecurityKey?.id) {
|
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 (
|
return (
|
||||||
<Card shadow="sm" p="lg" m="sm">
|
<Card shadow="sm" p="lg" m="sm">
|
||||||
<Title>Security keys</Title>
|
<Title>Security keys</Title>
|
||||||
@@ -79,7 +91,7 @@ export const SecurityKeys: React.FC = () => {
|
|||||||
<tr key={id}>
|
<tr key={id}>
|
||||||
<td>{nickname || id}</td>
|
<td>{nickname || id}</td>
|
||||||
<td>
|
<td>
|
||||||
<ActionIcon onClick={() => removeKey({ variables: { id } })} color="red">
|
<ActionIcon onClick={() => handleRemoveKey(id)} color="red">
|
||||||
<FaMinus />
|
<FaMinus />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</td>
|
</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 { defineConfig } from 'vite'
|
||||||
|
import path from 'path'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
@@ -8,5 +8,10 @@ export default defineConfig({
|
|||||||
include: ['react/jsx-runtime'],
|
include: ['react/jsx-runtime'],
|
||||||
exclude: ['@nhost/react']
|
exclude: ['@nhost/react']
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
src: path.resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
},
|
||||||
plugins: [react()]
|
plugins: [react()]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
# @nhost-examples/react-gqty
|
# @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
|
## 1.0.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/react-gqty",
|
"name": "@nhost-examples/react-gqty",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.2",
|
"version": "1.0.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,26 @@
|
|||||||
# @nhost-examples/vue-apollo
|
# @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
|
## 0.1.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -31,3 +31,19 @@ object_relationships:
|
|||||||
- name: user
|
- name: user
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on: user_id
|
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 public_notes.yaml"
|
||||||
- "!include storage_buckets.yaml"
|
- "!include storage_buckets.yaml"
|
||||||
- "!include storage_files.yaml"
|
- "!include storage_files.yaml"
|
||||||
|
- "!include storage_virus.yaml"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[global]
|
[global]
|
||||||
|
|
||||||
[hasura]
|
[hasura]
|
||||||
version = 'v2.25.1-ce'
|
version = 'v2.33.4-ce'
|
||||||
adminSecret = '{{ secrets.HASURA_GRAPHQL_ADMIN_SECRET }}'
|
adminSecret = '{{ secrets.HASURA_GRAPHQL_ADMIN_SECRET }}'
|
||||||
webhookSecret = '{{ secrets.NHOST_WEBHOOK_SECRET }}'
|
webhookSecret = '{{ secrets.NHOST_WEBHOOK_SECRET }}'
|
||||||
|
|
||||||
@@ -28,7 +28,10 @@ httpPoolSize = 100
|
|||||||
version = 18
|
version = 18
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
version = '0.25.0'
|
version = '0.26.0'
|
||||||
|
|
||||||
|
[auth.elevatedPrivileges]
|
||||||
|
mode = 'required'
|
||||||
|
|
||||||
[auth.redirections]
|
[auth.redirections]
|
||||||
clientUrl = 'http://localhost:5173'
|
clientUrl = 'http://localhost:5173'
|
||||||
@@ -66,11 +69,11 @@ expiresIn = 43200
|
|||||||
enabled = false
|
enabled = false
|
||||||
|
|
||||||
[auth.method.emailPasswordless]
|
[auth.method.emailPasswordless]
|
||||||
enabled = false
|
enabled = true
|
||||||
|
|
||||||
[auth.method.emailPassword]
|
[auth.method.emailPassword]
|
||||||
hibpEnabled = false
|
hibpEnabled = false
|
||||||
emailVerificationRequired = false
|
emailVerificationRequired = true
|
||||||
passwordMinLength = 9
|
passwordMinLength = 9
|
||||||
|
|
||||||
[auth.method.smsPasswordless]
|
[auth.method.smsPasswordless]
|
||||||
@@ -137,12 +140,12 @@ timeout = 60000
|
|||||||
enabled = false
|
enabled = false
|
||||||
|
|
||||||
[postgres]
|
[postgres]
|
||||||
version = '14.6-20230406-2'
|
version = '14.6-20240129-1'
|
||||||
|
|
||||||
[provider]
|
[provider]
|
||||||
|
|
||||||
[storage]
|
[storage]
|
||||||
version = '0.3.4'
|
version = '0.6.0'
|
||||||
|
|
||||||
[observability]
|
[observability]
|
||||||
[observability.grafana]
|
[observability.grafana]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/vue-apollo",
|
"name": "@nhost-examples/vue-apollo",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"verify:fix": "run-p prettier:fix lint:fix"
|
"verify:fix": "run-p prettier:fix lint:fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.9.1",
|
"@apollo/client": "^3.9.4",
|
||||||
"@mdi/font": "5.9.55",
|
"@mdi/font": "5.9.55",
|
||||||
"@nhost/apollo": "workspace:^",
|
"@nhost/apollo": "workspace:^",
|
||||||
"@nhost/nhost-js": "workspace:^",
|
"@nhost/nhost-js": "workspace:^",
|
||||||
|
|||||||
@@ -17,19 +17,68 @@
|
|||||||
prepend-icon="mdi-exit-to-app"
|
prepend-icon="mdi-exit-to-app"
|
||||||
@click="signOutHandler"
|
@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-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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useRouter } from 'vue-router'
|
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 router = useRouter()
|
||||||
const { signOut } = useSignOut()
|
const { signOut } = useSignOut()
|
||||||
|
const showElevateError = ref(false)
|
||||||
|
const showElevateSuccess = ref(false)
|
||||||
|
const loggedOutWarning = ref(false)
|
||||||
const authenticated = useAuthenticated()
|
const authenticated = useAuthenticated()
|
||||||
|
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
|
||||||
|
|
||||||
const signOutHandler = async () => {
|
const signOutHandler = async () => {
|
||||||
await signOut()
|
await signOut()
|
||||||
router.push('/')
|
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>
|
</script>
|
||||||
|
|||||||
@@ -2,7 +2,228 @@
|
|||||||
<div className="d-flex align-center flex-column">
|
<div className="d-flex align-center flex-column">
|
||||||
<v-card width="400">
|
<v-card width="400">
|
||||||
<v-card-title>Profile page</v-card-title>
|
<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>
|
</v-card>
|
||||||
</div>
|
</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>
|
</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">
|
<div className="d-flex align-center flex-column">
|
||||||
<v-card width="400" tile>
|
<v-card width="400" tile>
|
||||||
<v-card-title>Secret Notes</v-card-title>
|
<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-col>
|
||||||
<v-row class="px-4 mb-2 align-center">
|
<v-row class="px-4 mb-2 align-center">
|
||||||
<v-text-field v-model="content" label="Note" class="mt-4 mr-2" />
|
<v-text-field v-model="content" label="Note" class="mt-4 mr-2" />
|
||||||
<v-btn size="large" @click="insertNote({ content }, { refetchQueries: ['notesList'] })"
|
<v-btn size="large" @click="addNote">Add</v-btn>
|
||||||
>Add</v-btn
|
|
||||||
>
|
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-list density="compact" v-if="result">
|
<v-list density="compact" v-if="result">
|
||||||
<v-list-subheader>Notes</v-list-subheader>
|
<v-list-subheader>Notes</v-list-subheader>
|
||||||
<v-list-item v-for="(note, i) in result.notes" :key="i" :value="note.id">
|
<v-list-item v-for="(note, i) in result.notes" :key="i" :value="note.id">
|
||||||
<div className="d-flex align-center justify-space-between">
|
<div className="d-flex align-center justify-space-between">
|
||||||
<v-list-item-title v-text="note.content"></v-list-item-title>
|
<v-list-item-title v-text="note.content"></v-list-item-title>
|
||||||
<v-btn
|
<v-btn variant="flat" prepend-icon="mdi-delete" @click="deleteNote(note.id)" />
|
||||||
variant="flat"
|
|
||||||
prepend-icon="mdi-delete"
|
|
||||||
@click="deleteNote({ noteId: note.id }, { refetchQueries: ['notesList'] })"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
@@ -33,26 +21,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<error-snack-bar :error="insertNoteError" />
|
<error-snack-bar :error="insertNoteError" />
|
||||||
<error-snack-bar :error="deleteNoteError" />
|
<error-snack-bar :error="deleteNoteError" />
|
||||||
|
<error-snack-bar v-model="showElevatePermissionError"
|
||||||
|
>Could not elevate permission</error-snack-bar
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, unref } from 'vue'
|
||||||
|
|
||||||
import { gql } from '@apollo/client/core'
|
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'
|
import { useQuery, useMutation } from '@vue/apollo-composable'
|
||||||
|
|
||||||
const content = ref('')
|
const content = ref('')
|
||||||
|
const userId = useUserId()
|
||||||
const user = useUserData()
|
const userEmail = useUserEmail()
|
||||||
|
const isAuthenticated = useAuthenticated()
|
||||||
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
|
const { elevated, elevateEmailSecurityKey } = useElevateSecurityKeyEmail()
|
||||||
|
const showElevatePermissionError = ref(false)
|
||||||
const elevatePermission = async (email: string | undefined) => {
|
|
||||||
if (email) {
|
|
||||||
await elevateEmailSecurityKey(email)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const GET_NOTES = gql`
|
const GET_NOTES = gql`
|
||||||
query notesList {
|
query notesList {
|
||||||
@@ -81,8 +67,15 @@ const DELETE_NOTE = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const isAuthenticated = useAuthenticated()
|
const SECURITY_KEYS_LIST = gql`
|
||||||
// TODO check if the query always runs with the headers
|
query securityKeys($userId: uuid!) {
|
||||||
|
authUserSecurityKeys(where: { userId: { _eq: $userId } }) {
|
||||||
|
id
|
||||||
|
nickname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const { result } = useQuery(
|
const { result } = useQuery(
|
||||||
GET_NOTES,
|
GET_NOTES,
|
||||||
null,
|
null,
|
||||||
@@ -91,6 +84,44 @@ const { result } = useQuery(
|
|||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
const { mutate: insertNote, error: insertNoteError } = useMutation(INSERT_NOTE)
|
const { result: securityKeys } = useQuery(SECURITY_KEYS_LIST, { userId })
|
||||||
const { mutate: deleteNote, error: deleteNoteError } = useMutation(DELETE_NOTE)
|
|
||||||
|
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>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
# @nhost-examples/vue-quickstart
|
# @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
|
## 0.0.11
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost-examples/vue-quickstart",
|
"name": "@nhost-examples/vue-quickstart",
|
||||||
"version": "0.0.11",
|
"version": "0.0.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.9.1",
|
"@apollo/client": "^3.9.4",
|
||||||
"@nhost/apollo": "workspace:^",
|
"@nhost/apollo": "workspace:^",
|
||||||
"@nhost/vue": "workspace:^",
|
"@nhost/vue": "workspace:^",
|
||||||
"@vue/apollo-composable": "4.0.0-alpha.18",
|
"@vue/apollo-composable": "4.0.0-alpha.18",
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# @nhost/apollo
|
# @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
|
## 6.0.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/apollo",
|
"name": "@nhost/apollo",
|
||||||
"version": "6.0.3",
|
"version": "6.0.5",
|
||||||
"description": "Nhost Apollo Client library",
|
"description": "Nhost Apollo Client library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"graphql-ws": "^5.14.3"
|
"graphql-ws": "^5.14.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@apollo/client": "^3.9.1",
|
"@apollo/client": "^3.9.4",
|
||||||
"@nhost/nhost-js": "workspace:*"
|
"@nhost/nhost-js": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,20 @@
|
|||||||
# @nhost/react-apollo
|
# @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
|
## 8.0.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/react-apollo",
|
"name": "@nhost/react-apollo",
|
||||||
"version": "8.0.0",
|
"version": "9.0.0",
|
||||||
"description": "Nhost React Apollo client",
|
"description": "Nhost React Apollo client",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"react-dom": "^17.0.0 || ^18.0.0"
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@apollo/client": "^3.9.1",
|
"@apollo/client": "^3.9.4",
|
||||||
"@nhost/react": "workspace:*",
|
"@nhost/react": "workspace:*",
|
||||||
"@types/react": "^18.2.50",
|
"@types/react": "^18.2.50",
|
||||||
"graphql": "16.8.1",
|
"graphql": "16.8.1",
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
# @nhost/react-urql
|
# @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
|
## 5.0.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/react-urql",
|
"name": "@nhost/react-urql",
|
||||||
"version": "5.0.0",
|
"version": "6.0.0",
|
||||||
"description": "Nhost React URQL client",
|
"description": "Nhost React URQL client",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# @nhost/hasura-auth-js
|
# @nhost/hasura-auth-js
|
||||||
|
|
||||||
|
## 2.3.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 017f1a6: feat: add elevated permission examples
|
||||||
|
|
||||||
## 2.2.0
|
## 2.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/hasura-auth-js",
|
"name": "@nhost/hasura-auth-js",
|
||||||
"version": "2.2.0",
|
"version": "2.3.0",
|
||||||
"description": "Hasura-auth client",
|
"description": "Hasura-auth client",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -445,16 +445,14 @@ export class HasuraAuthClient {
|
|||||||
*
|
*
|
||||||
* @docs https://docs.nhost.io/reference/javascript/auth/elevate-security-key
|
* @docs https://docs.nhost.io/reference/javascript/auth/elevate-security-key
|
||||||
*/
|
*/
|
||||||
async elevateWebAuthn(
|
async elevateEmailSecurityKey(email: string) {
|
||||||
email: string
|
|
||||||
): Promise<SignInResponse & { providerUrl?: string; provider?: string }> {
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
throw Error('A user email is required')
|
throw Error('A user email is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await elevateEmailSecurityKeyPromise(this._client, email)
|
const res = await elevateEmailSecurityKeyPromise(this._client, email)
|
||||||
|
|
||||||
return { ...getAuthenticationResult(res), mfa: null }
|
return { ...res, mfa: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ import {
|
|||||||
PublicKeyCredentialRequestOptionsJSON
|
PublicKeyCredentialRequestOptionsJSON
|
||||||
} from '@simplewebauthn/typescript-types'
|
} from '@simplewebauthn/typescript-types'
|
||||||
import {
|
import {
|
||||||
|
AuthActionErrorState,
|
||||||
|
AuthActionSuccessState,
|
||||||
AuthClient,
|
AuthClient,
|
||||||
|
AuthErrorPayload,
|
||||||
CodifiedError,
|
CodifiedError,
|
||||||
postFetch,
|
postFetch,
|
||||||
SessionActionHandlerResult,
|
|
||||||
SignInResponse
|
SignInResponse
|
||||||
} from '..'
|
} from '..'
|
||||||
import { startAuthentication } from '@simplewebauthn/browser'
|
import { startAuthentication } from '@simplewebauthn/browser'
|
||||||
|
|
||||||
export interface ElevateWithSecurityKeyHandlerResult extends SessionActionHandlerResult {
|
export interface ElevateWithSecurityKeyHandlerResult
|
||||||
|
extends AuthActionSuccessState,
|
||||||
|
AuthActionErrorState {
|
||||||
elevated: boolean
|
elevated: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,48 +40,42 @@ export const elevateEmailSecurityKeyPromise = (authClient: AuthClient, email: st
|
|||||||
throw new CodifiedError(e as Error)
|
throw new CodifiedError(e as Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
try {
|
||||||
data: { session },
|
const {
|
||||||
error: signInError
|
data: { session },
|
||||||
} = await postFetch<SignInResponse>(
|
error: signInError
|
||||||
`${authClient.backendUrl}/elevate/webauthn/verify`,
|
} = await postFetch<SignInResponse>(
|
||||||
{
|
`${authClient.backendUrl}/elevate/webauthn/verify`,
|
||||||
email,
|
{
|
||||||
credential
|
email,
|
||||||
},
|
credential
|
||||||
accessToken
|
},
|
||||||
)
|
accessToken
|
||||||
|
)
|
||||||
|
|
||||||
if (session && !signInError) {
|
if (session && !signInError) {
|
||||||
authClient.interpreter?.send({
|
authClient.interpreter?.send({
|
||||||
type: 'SESSION_UPDATE',
|
type: 'SESSION_UPDATE',
|
||||||
data: {
|
data: {
|
||||||
session
|
session
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
authClient.interpreter?.onTransition((state) => {
|
|
||||||
if (state.matches({ authentication: 'signedIn' })) {
|
|
||||||
resolve({
|
resolve({
|
||||||
accessToken: state.context.accessToken.value,
|
|
||||||
refreshToken: state.context.refreshToken.value,
|
|
||||||
error: null,
|
error: null,
|
||||||
isError: false,
|
isError: false,
|
||||||
isSuccess: true,
|
isSuccess: true,
|
||||||
user: state.context.user,
|
|
||||||
elevated: true
|
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
|
# @nhost/hasura-storage-js
|
||||||
|
|
||||||
|
## 2.4.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 2505b2e: fix: fix headers sent with getPresignedUrl
|
||||||
|
|
||||||
## 2.3.0
|
## 2.3.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/hasura-storage-js",
|
"name": "@nhost/hasura-storage-js",
|
||||||
"version": "2.3.0",
|
"version": "2.4.0",
|
||||||
"description": "Hasura-storage client",
|
"description": "Hasura-storage client",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -99,12 +99,6 @@ export class HasuraStorageApi {
|
|||||||
async downloadFile(params: StorageDownloadFileParams): Promise<StorageDownloadFileResponse> {
|
async downloadFile(params: StorageDownloadFileParams): Promise<StorageDownloadFileResponse> {
|
||||||
try {
|
try {
|
||||||
const { fileId, headers: customHeaders = {}, ...imageTransformationParams } = params
|
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(
|
const urlWithParams = appendImageTransformationParameters(
|
||||||
`${this.url}/files/${fileId}`,
|
`${this.url}/files/${fileId}`,
|
||||||
@@ -113,7 +107,7 @@ export class HasuraStorageApi {
|
|||||||
|
|
||||||
const response = await fetch(urlWithParams, {
|
const response = await fetch(urlWithParams, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers
|
headers: {...this.generateAuthHeaders(), ...customHeaders}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -131,6 +125,7 @@ export class HasuraStorageApi {
|
|||||||
async getPresignedUrl(params: ApiGetPresignedUrlParams): Promise<ApiGetPresignedUrlResponse> {
|
async getPresignedUrl(params: ApiGetPresignedUrlParams): Promise<ApiGetPresignedUrlResponse> {
|
||||||
try {
|
try {
|
||||||
const { fileId } = params
|
const { fileId } = params
|
||||||
|
|
||||||
const response = await fetch(`${this.url}/files/${fileId}/presignedurl`, {
|
const response = await fetch(`${this.url}/files/${fileId}/presignedurl`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: this.generateAuthHeaders()
|
headers: this.generateAuthHeaders()
|
||||||
@@ -185,16 +180,19 @@ export class HasuraStorageApi {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateAuthHeaders(): Headers {
|
private generateAuthHeaders(): HeadersInit | undefined {
|
||||||
const headers = new Headers()
|
if (!this.adminSecret && !this.accessToken) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
if (this.adminSecret) {
|
if (this.adminSecret) {
|
||||||
headers.append('x-hasura-admin-secret', this.adminSecret)
|
return {
|
||||||
}
|
'x-hasura-admin-secret': this.adminSecret
|
||||||
if (this.accessToken) {
|
}
|
||||||
headers.append('Authorization', `Bearer ${this.accessToken}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers
|
return {
|
||||||
|
Authorization: `Bearer ${this.accessToken}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
# @nhost/nextjs
|
# @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
|
## 2.1.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/nextjs",
|
"name": "@nhost/nextjs",
|
||||||
"version": "2.1.0",
|
"version": "2.1.2",
|
||||||
"description": "Nhost NextJS library",
|
"description": "Nhost NextJS library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @nhost/nhost-js
|
# @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
|
## 3.0.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/nhost-js",
|
"name": "@nhost/nhost-js",
|
||||||
"version": "3.0.3",
|
"version": "3.0.5",
|
||||||
"description": "Nhost JavaScript SDK",
|
"description": "Nhost JavaScript SDK",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# @nhost/react
|
# @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
|
## 3.1.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/react",
|
"name": "@nhost/react",
|
||||||
"version": "3.1.0",
|
"version": "3.2.0",
|
||||||
"description": "Nhost React library",
|
"description": "Nhost React library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -35,14 +35,16 @@ export const useElevateSecurityKeyEmail = (): ElevateWithSecurityKeyHook => {
|
|||||||
const nhost = useNhostClient()
|
const nhost = useNhostClient()
|
||||||
const claims = useHasuraClaims()
|
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) =>
|
const elevateEmailSecurityKey: ElevateWithSecurityKeyHandler = (email: string) =>
|
||||||
elevateEmailSecurityKeyPromise(nhost.auth.client, email)
|
elevateEmailSecurityKeyPromise(nhost.auth.client, email)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setElevated(claims?.['x-hasura-auth-elevated'] === user?.id)
|
setElevated(!!hasElevatedClaim)
|
||||||
}, [claims, user])
|
}, [hasElevatedClaim])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elevated,
|
elevated,
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# @nhost/vue
|
# @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
|
## 2.1.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/vue",
|
"name": "@nhost/vue",
|
||||||
"version": "2.1.0",
|
"version": "2.2.0",
|
||||||
"description": "Nhost Vue library",
|
"description": "Nhost Vue library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -34,3 +34,4 @@ export * from './useUserRoles'
|
|||||||
export * from './useElevateSecurityKeyEmail'
|
export * from './useElevateSecurityKeyEmail'
|
||||||
export * from './useSignUpEmailSecurityKey'
|
export * from './useSignUpEmailSecurityKey'
|
||||||
export * from './useSignInEmailSecurityKey'
|
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,
|
elevateEmailSecurityKeyPromise,
|
||||||
ElevateWithSecurityKeyHandlerResult
|
ElevateWithSecurityKeyHandlerResult
|
||||||
} from '@nhost/nhost-js'
|
} from '@nhost/nhost-js'
|
||||||
import { computed, ref, unref } from 'vue'
|
import { computed, unref } from 'vue'
|
||||||
import { RefOrValue } from './helpers'
|
import { RefOrValue } from './helpers'
|
||||||
import { useHasuraClaims } from './useHasuraClaims'
|
import { useHasuraClaims } from './useHasuraClaims'
|
||||||
import { useNhostClient } from './useNhostClient'
|
import { useNhostClient } from './useNhostClient'
|
||||||
@@ -39,7 +39,11 @@ export const useElevateSecurityKeyEmail = (): ElevateWithSecurityKeyResult => {
|
|||||||
const claims = useHasuraClaims()
|
const claims = useHasuraClaims()
|
||||||
const { nhost } = useNhostClient()
|
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>) =>
|
const elevateEmailSecurityKey: ElevateWithSecurityKeyHandler = (email: RefOrValue<string>) =>
|
||||||
elevateEmailSecurityKeyPromise(nhost.auth.client, unref(email))
|
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