diff --git a/apps/console/schema.graphql b/apps/console/schema.graphql index 2276215e..736f15de 100644 --- a/apps/console/schema.graphql +++ b/apps/console/schema.graphql @@ -28,6 +28,21 @@ input Boolean_comparison_exp { _nin: [Boolean!] } +""" +Boolean expression to compare columns of type "Int". All fields are combined with logical 'AND'. +""" +input Int_comparison_exp { + _eq: Int + _gt: Int + _gte: Int + _in: [Int!] + _is_null: Boolean + _lt: Int + _lte: Int + _neq: Int + _nin: [Int!] +} + input ListUniversitiesInput { country: String name: String @@ -252,6 +267,140 @@ input action_stream_cursor_value_input { value: String } +""" +User webauthn security keys. Don't modify its structure as Hasura Auth relies on it to function properly. +""" +type authUserSecurityKeys { + id: uuid! + nickname: String + + """An object relationship""" + user: users! +} + +""" +aggregated selection of "auth.user_security_keys" +""" +type authUserSecurityKeys_aggregate { + aggregate: authUserSecurityKeys_aggregate_fields + nodes: [authUserSecurityKeys!]! +} + +input authUserSecurityKeys_aggregate_bool_exp { + count: authUserSecurityKeys_aggregate_bool_exp_count +} + +input authUserSecurityKeys_aggregate_bool_exp_count { + arguments: [authUserSecurityKeys_select_column!] + distinct: Boolean + filter: authUserSecurityKeys_bool_exp + predicate: Int_comparison_exp! +} + +""" +aggregate fields of "auth.user_security_keys" +""" +type authUserSecurityKeys_aggregate_fields { + count(columns: [authUserSecurityKeys_select_column!], distinct: Boolean): Int! + max: authUserSecurityKeys_max_fields + min: authUserSecurityKeys_min_fields +} + +""" +order by aggregate values of table "auth.user_security_keys" +""" +input authUserSecurityKeys_aggregate_order_by { + count: order_by + max: authUserSecurityKeys_max_order_by + min: authUserSecurityKeys_min_order_by +} + +""" +Boolean expression to filter rows from the table "auth.user_security_keys". All fields are combined with a logical 'AND'. +""" +input authUserSecurityKeys_bool_exp { + _and: [authUserSecurityKeys_bool_exp!] + _not: authUserSecurityKeys_bool_exp + _or: [authUserSecurityKeys_bool_exp!] + id: uuid_comparison_exp + nickname: String_comparison_exp + user: users_bool_exp +} + +"""aggregate max on columns""" +type authUserSecurityKeys_max_fields { + id: uuid + nickname: String +} + +""" +order by max() on columns of table "auth.user_security_keys" +""" +input authUserSecurityKeys_max_order_by { + id: order_by + nickname: order_by +} + +"""aggregate min on columns""" +type authUserSecurityKeys_min_fields { + id: uuid + nickname: String +} + +""" +order by min() on columns of table "auth.user_security_keys" +""" +input authUserSecurityKeys_min_order_by { + id: order_by + nickname: order_by +} + +""" +response of any mutation on the table "auth.user_security_keys" +""" +type authUserSecurityKeys_mutation_response { + """number of rows affected by the mutation""" + affected_rows: Int! + + """data from the rows affected by the mutation""" + returning: [authUserSecurityKeys!]! +} + +"""Ordering options when selecting data from "auth.user_security_keys".""" +input authUserSecurityKeys_order_by { + id: order_by + nickname: order_by + user: users_order_by +} + +""" +select columns of table "auth.user_security_keys" +""" +enum authUserSecurityKeys_select_column { + """column name""" + id + + """column name""" + nickname +} + +""" +Streaming cursor of the table "authUserSecurityKeys" +""" +input authUserSecurityKeys_stream_cursor_input { + """Stream column input with initial value""" + initial_value: authUserSecurityKeys_stream_cursor_value_input! + + """cursor ordering""" + ordering: cursor_ordering +} + +"""Initial value of the column from where the streaming should start""" +input authUserSecurityKeys_stream_cursor_value_input { + id: uuid + nickname: String +} + scalar citext """ @@ -1140,6 +1289,19 @@ input jsonb_comparison_exp { """mutation root""" type mutation_root { + """ + delete single row from the table: "auth.user_security_keys" + """ + deleteAuthUserSecurityKey(id: uuid!): authUserSecurityKeys + + """ + delete data from the table: "auth.user_security_keys" + """ + deleteAuthUserSecurityKeys( + """filter the rows which have to be deleted""" + where: authUserSecurityKeys_bool_exp! + ): authUserSecurityKeys_mutation_response + """ delete data from the table: "device_pool" """ @@ -2334,6 +2496,51 @@ type query_root { """fetch data from the table: "action" using primary key columns""" action_by_pk(value: String!): action + """ + fetch data from the table: "auth.user_security_keys" using primary key columns + """ + authUserSecurityKey(id: uuid!): authUserSecurityKeys + + """ + fetch data from the table: "auth.user_security_keys" + """ + authUserSecurityKeys( + """distinct select on columns""" + distinct_on: [authUserSecurityKeys_select_column!] + + """limit the number of rows returned""" + limit: Int + + """skip the first n rows. Use only with order_by""" + offset: Int + + """sort the rows by one or more columns""" + order_by: [authUserSecurityKeys_order_by!] + + """filter the rows returned""" + where: authUserSecurityKeys_bool_exp + ): [authUserSecurityKeys!]! + + """ + fetch aggregated fields from the table: "auth.user_security_keys" + """ + authUserSecurityKeysAggregate( + """distinct select on columns""" + distinct_on: [authUserSecurityKeys_select_column!] + + """limit the number of rows returned""" + limit: Int + + """skip the first n rows. Use only with order_by""" + offset: Int + + """sort the rows by one or more columns""" + order_by: [authUserSecurityKeys_order_by!] + + """filter the rows returned""" + where: authUserSecurityKeys_bool_exp + ): authUserSecurityKeys_aggregate! + """ fetch data from the table: "device_pool" """ @@ -3206,6 +3413,65 @@ type subscription_root { where: action_bool_exp ): [action!]! + """ + fetch data from the table: "auth.user_security_keys" using primary key columns + """ + authUserSecurityKey(id: uuid!): authUserSecurityKeys + + """ + fetch data from the table: "auth.user_security_keys" + """ + authUserSecurityKeys( + """distinct select on columns""" + distinct_on: [authUserSecurityKeys_select_column!] + + """limit the number of rows returned""" + limit: Int + + """skip the first n rows. Use only with order_by""" + offset: Int + + """sort the rows by one or more columns""" + order_by: [authUserSecurityKeys_order_by!] + + """filter the rows returned""" + where: authUserSecurityKeys_bool_exp + ): [authUserSecurityKeys!]! + + """ + fetch aggregated fields from the table: "auth.user_security_keys" + """ + authUserSecurityKeysAggregate( + """distinct select on columns""" + distinct_on: [authUserSecurityKeys_select_column!] + + """limit the number of rows returned""" + limit: Int + + """skip the first n rows. Use only with order_by""" + offset: Int + + """sort the rows by one or more columns""" + order_by: [authUserSecurityKeys_order_by!] + + """filter the rows returned""" + where: authUserSecurityKeys_bool_exp + ): authUserSecurityKeys_aggregate! + + """ + fetch data from the table in a streaming manner: "auth.user_security_keys" + """ + authUserSecurityKeys_stream( + """maximum number of rows returned in a single batch""" + batch_size: Int! + + """cursor to stream the results returned by the query""" + cursor: [authUserSecurityKeys_stream_cursor_input]! + + """filter the rows returned""" + where: authUserSecurityKeys_bool_exp + ): [authUserSecurityKeys!]! + """ fetch data from the table: "device_pool" """ @@ -4158,6 +4424,42 @@ type users { otpMethodLastUsed: String phoneNumber: String phoneNumberVerified: Boolean! + + """An array relationship""" + securityKeys( + """distinct select on columns""" + distinct_on: [authUserSecurityKeys_select_column!] + + """limit the number of rows returned""" + limit: Int + + """skip the first n rows. Use only with order_by""" + offset: Int + + """sort the rows by one or more columns""" + order_by: [authUserSecurityKeys_order_by!] + + """filter the rows returned""" + where: authUserSecurityKeys_bool_exp + ): [authUserSecurityKeys!]! + + """An aggregate relationship""" + securityKeys_aggregate( + """distinct select on columns""" + distinct_on: [authUserSecurityKeys_select_column!] + + """limit the number of rows returned""" + limit: Int + + """skip the first n rows. Use only with order_by""" + offset: Int + + """sort the rows by one or more columns""" + order_by: [authUserSecurityKeys_order_by!] + + """filter the rows returned""" + where: authUserSecurityKeys_bool_exp + ): authUserSecurityKeys_aggregate! ticket: String ticketExpiresAt: timestamptz! updatedAt: timestamptz! @@ -4190,6 +4492,8 @@ input users_bool_exp { otpMethodLastUsed: String_comparison_exp phoneNumber: String_comparison_exp phoneNumberVerified: Boolean_comparison_exp + securityKeys: authUserSecurityKeys_bool_exp + securityKeys_aggregate: authUserSecurityKeys_aggregate_bool_exp ticket: String_comparison_exp ticketExpiresAt: timestamptz_comparison_exp updatedAt: timestamptz_comparison_exp @@ -4228,6 +4532,7 @@ input users_order_by { otpMethodLastUsed: order_by phoneNumber: order_by phoneNumberVerified: order_by + securityKeys_aggregate: authUserSecurityKeys_aggregate_order_by ticket: order_by ticketExpiresAt: order_by updatedAt: order_by diff --git a/apps/console/src/lib/components/magic-spell-textarea/README.md b/apps/console/src/lib/components/magic-spell-textarea/README.md index a2d19c7f..7f3d1f8e 100644 --- a/apps/console/src/lib/components/magic-spell-textarea/README.md +++ b/apps/console/src/lib/components/magic-spell-textarea/README.md @@ -1,5 +1,7 @@ # Magic Spell +This **wordsmith** svelte component enables user write *with confidence* in any web forms. + Based on nextjs [magic-spell](https://github.com/ai-ng/magic-spell/tree/main) ## Ref diff --git a/apps/console/src/lib/graphql/MUTATION.DeleteSecurityKey.gql b/apps/console/src/lib/graphql/MUTATION.DeleteSecurityKey.gql new file mode 100644 index 00000000..bb78a949 --- /dev/null +++ b/apps/console/src/lib/graphql/MUTATION.DeleteSecurityKey.gql @@ -0,0 +1,5 @@ +mutation RemoveSecurityKey($id: uuid!) { + deleteAuthUserSecurityKey(id: $id) { + id + } +} diff --git a/apps/console/src/lib/graphql/QUERY.ListSecurityKeys.gql b/apps/console/src/lib/graphql/QUERY.ListSecurityKeys.gql new file mode 100644 index 00000000..16bcae0b --- /dev/null +++ b/apps/console/src/lib/graphql/QUERY.ListSecurityKeys.gql @@ -0,0 +1,6 @@ +query ListSecurityKeys($userId: uuid!) { + authUserSecurityKeys(where: { userId: { _eq: $userId } }) { + id + nickname + } +} diff --git a/apps/console/src/lib/schema/user.ts b/apps/console/src/lib/schema/user.ts index c448294f..ae4aad70 100644 --- a/apps/console/src/lib/schema/user.ts +++ b/apps/console/src/lib/schema/user.ts @@ -71,10 +71,21 @@ export const signUpSchema = userSchema }) .superRefine((data, ctx) => checkConfirmPassword(ctx, data.confirmPassword, data.password)); -export const userUpdatePasswordSchema = userSchema +export const changePasswordSchema = userSchema .pick({ password: true, confirmPassword: true }) .superRefine((data, ctx) => checkConfirmPassword(ctx, data.confirmPassword, data.password)); +export const changeEmailSchema = userSchema.pick({ email: true }); + +export const webAuthnSchema = z.object({ + nickname: z + .string({ required_error: 'Nickname is required' }) + .min(2, { message: 'Nickname must contain at least 2 character(s)' }) + .max(256) + .trim(), +}); + + /** * Refine functions */ diff --git a/apps/console/src/routes/(app)/profile/+page.svelte b/apps/console/src/routes/(app)/profile/+page.svelte index edbd749c..1c98a4dd 100644 --- a/apps/console/src/routes/(app)/profile/+page.svelte +++ b/apps/console/src/routes/(app)/profile/+page.svelte @@ -1,27 +1,25 @@ @@ -57,14 +55,13 @@ export let data ))} --> -
diff --git a/apps/console/src/routes/(app)/profile/+page.ts b/apps/console/src/routes/(app)/profile/+page.ts new file mode 100644 index 00000000..91a7c42f --- /dev/null +++ b/apps/console/src/routes/(app)/profile/+page.ts @@ -0,0 +1,27 @@ +import { CachePolicy, ListSecurityKeysStore, order_by } from '$houdini'; +import { i18n } from '$lib/i18n.js'; +import { changeEmailSchema, changePasswordSchema, webAuthnSchema } from '$lib/schema/user'; +import { isAuthenticated } from '$lib/stores/user'; +import { Logger } from '@spectacular/utils'; +import { error } from '@sveltejs/kit'; +import { redirect } from '@sveltejs/kit'; +import { setError, setMessage, superValidate } from 'sveltekit-superforms'; +import { zod } from 'sveltekit-superforms/adapters'; +const log = new Logger('user.profile.browser'); + +const searchPoliciesStore = new ListSecurityKeysStore(); + +export const load = async (event) => { + const { + url: { pathname }, + } = event; + + if (!isAuthenticated) { + redirect(303, i18n.resolveRoute(`/signin?redirectTo=${pathname}`)); + } + const cpForm = await superValidate(zod(changePasswordSchema)); + const ceForm = await superValidate(zod(changeEmailSchema)); + const waForm = await superValidate(zod(webAuthnSchema)); + const items = {}; // data?.policies; + return { cpForm, ceForm, waForm, items }; +}; diff --git a/compose.yml b/compose.yml index 51646ab9..ca990ef5 100644 --- a/compose.yml +++ b/compose.yml @@ -248,7 +248,6 @@ services: AUTH_USER_DEFAULT_ROLE: user AUTH_WEBAUTHN_ATTESTATION_TIMEOUT: '60000' AUTH_WEBAUTHN_ENABLED: 'true' - AUTH_WEBAUTHN_RP_ID: ${APP_NAME} AUTH_WEBAUTHN_RP_NAME: ${APP_DESCRIPTION} AUTH_WEBAUTHN_RP_ORIGINS: ${AUTH_CLIENT_URL:-https://console${BASE_HOSTNAME}:5173,https://console.traefik.me} HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET:-hasura-admin-secret} diff --git a/nhost/metadata/databases/default/tables/auth_user_security_keys.yaml b/nhost/metadata/databases/default/tables/auth_user_security_keys.yaml index 9963b623..76254964 100644 --- a/nhost/metadata/databases/default/tables/auth_user_security_keys.yaml +++ b/nhost/metadata/databases/default/tables/auth_user_security_keys.yaml @@ -31,3 +31,53 @@ object_relationships: - name: user using: foreign_key_constraint_on: user_id +select_permissions: + - role: manager + permission: + columns: + - id + - nickname + filter: + user_id: + _eq: x-hasura-user-id + allow_aggregations: true + comment: "" + - role: supervisor + permission: + columns: + - id + - nickname + filter: + user_id: + _eq: x-hasura-user-id + allow_aggregations: true + comment: "" + - role: user + permission: + columns: + - id + - nickname + filter: + user_id: + _eq: x-hasura-user-id + allow_aggregations: true + comment: "" +delete_permissions: + - role: manager + permission: + filter: + user_id: + _eq: x-hasura-user-id + comment: "" + - role: supervisor + permission: + filter: + user_id: + _eq: x-hasura-user-id + comment: "" + - role: user + permission: + filter: + user_id: + _eq: x-hasura-user-id + comment: ""