Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/common/src/services/Backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ export interface User extends UserInfo {
readonly isEnsoTeamMember: boolean
/** Information about any pending invitation to a different organization / team. */
readonly invitation?: Invitation
/** String hash of accepted terms of service policy. */
readonly tosAccepted: string
/** String hash of accepted privacy policy. */
readonly ppAccepted: string
}

/** A user group related to the current user. */
Expand Down Expand Up @@ -1125,13 +1129,17 @@ export interface CreateUserRequestBody {
readonly userName: string
readonly userEmail: EmailAddress
readonly organizationId: OrganizationId | null
readonly tosAccepted: string
readonly ppAccepted: string
}

/** HTTP request body for the "update user" endpoint. */
export interface UpdateUserRequestBody {
readonly username?: string
readonly organizationId?: OrganizationId
readonly switchOrganization?: boolean
readonly tosAccepted?: string
readonly ppAccepted?: string
}

/** HTTP request body for the "change user group" endpoint. */
Expand Down
2 changes: 2 additions & 0 deletions app/gui/integration-test/mock/cloudApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,8 @@ export async function mockCloudApi(page: Page) {
isOrganizationAdmin: true,
isEnsoTeamMember: true,
plan: backend.Plan.free,
tosAccepted: '1c8a655202e59f0efebf5a83a703662527aa97247052964f959a8488382604b8',
ppAccepted: '31b113f5f2b6ab7131ca02d70ad3f0c158075020c3670a5eefe8670ba829bcc9',
}
return currentUser
})
Expand Down
11 changes: 4 additions & 7 deletions app/gui/src/components/RegistrationPage.vue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire vue wrapper is not needed.

You may just move import RegistrationReact... and const Registration = ... directly to router.ts, see other how react pages are loaded there. withDataLoader is no longer needed.

Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
<script lang="ts">
import RegistrationReact from '#/pages/authentication/Registration'
import { useUserAgreements } from '$/composables/userAgreements'
import type { DataLoader } from '$/router'
import { reactComponent } from '@/util/react'
import { useQueryClient } from '@tanstack/vue-query'
import { Ok } from 'enso-common/src/utilities/data/result'

const Registration = reactComponent(RegistrationReact)

type Props = { userAgreedFn: () => void }
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
type Props = {}

export const dataLoader: DataLoader<Props> = {
async beforeRouteEnter() {
const queryClient = useQueryClient()
const { userAgreed } = await useUserAgreements(queryClient)
return Ok({ userAgreedFn: userAgreed })
return Ok({})
},
}
</script>
Expand All @@ -24,5 +21,5 @@ defineProps<Props>()
</script>

<template>
<Registration :userAgreed="userAgreedFn" />
<Registration />
</template>
32 changes: 10 additions & 22 deletions app/gui/src/composables/userAgreements.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
import LocalStorage from '#/utilities/LocalStorage'
import { useAuth } from '$/providers/auth'
import { useBackends } from '$/providers/backends'
import { proxyRefs } from '$/utils/reactivity'
import * as vueQuery from '@tanstack/vue-query'
import { computed, effectScope } from 'vue'
import * as z from 'zod'

declare module '#/utilities/LocalStorage' {
/** Metadata containing the version hash of the terms of service that the user has accepted. */
interface LocalStorageData {
readonly termsOfService: z.infer<typeof TOS_SCHEMA>
readonly privacyPolicy: z.infer<typeof PRIVACY_POLICY_SCHEMA>
}
}

const TEN_MINUTES_MS = 600_000
const TOS_SCHEMA = z.object({ versionHash: z.string() })
const PRIVACY_POLICY_SCHEMA = z.object({ versionHash: z.string() })
const TOS_ENDPOINT_SCHEMA = z.object({ hash: z.string() })
const PRIVACY_POLICY_ENDPOINT_SCHEMA = z.object({ hash: z.string() })

LocalStorage.registerKey('termsOfService', { schema: TOS_SCHEMA })
LocalStorage.registerKey('privacyPolicy', { schema: PRIVACY_POLICY_SCHEMA })

const latestTermsOfServiceQueryOptions = vueQuery.queryOptions({
export const latestTermsOfServiceQueryOptions = vueQuery.queryOptions({
queryKey: ['termsOfService', 'currentVersion'],
queryFn: async () => {
const response = await fetch(new URL('/eula.json', $config.HOST))
Expand All @@ -37,7 +25,7 @@ const latestTermsOfServiceQueryOptions = vueQuery.queryOptions({
refetchInterval: TEN_MINUTES_MS,
})

const latestPrivacyPolicyQueryOptions = vueQuery.queryOptions({
export const latestPrivacyPolicyQueryOptions = vueQuery.queryOptions({
queryKey: ['privacyPolicy', 'currentVersion'],
queryFn: async () => {
const response = await fetch(new URL('/privacy.json', $config.HOST))
Expand All @@ -58,9 +46,11 @@ const latestPrivacyPolicyQueryOptions = vueQuery.queryOptions({
* and Privacy Policy.
*/
export async function useUserAgreements(queryClient: vueQuery.QueryClient) {
const localStorage = LocalStorage.getInstance()
const cachedTosHash = computed(() => localStorage.get('termsOfService'))
const cachedPrivacyPolicyHash = computed(() => localStorage.get('privacyPolicy'))
const { remoteBackend } = useBackends()
const auth = useAuth()

const cachedTosHash = computed(() => ({ versionHash: auth.session?.user?.tosAccepted }))
const cachedPrivacyPolicyHash = computed(() => ({ versionHash: auth.session?.user?.ppAccepted }))

// a scope to run after await -
const scope = effectScope()
Expand All @@ -85,10 +75,8 @@ export async function useUserAgreements(queryClient: vueQuery.QueryClient) {
const agreedToPrivacyPolicy = computed(
() => privacyPolicyHash.value === cachedPrivacyPolicyHash.value?.versionHash,
)

const userAgreed = () => {
localStorage.set('termsOfService', { versionHash: tosHash.value })
localStorage.set('privacyPolicy', { versionHash: privacyPolicyHash.value })
remoteBackend.updateUser({ tosAccepted: tosHash.value, ppAccepted: privacyPolicyHash.value })
}

return proxyRefs({
Expand Down
11 changes: 1 addition & 10 deletions app/gui/src/dashboard/pages/authentication/Registration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,8 @@ import { useEffect, useState } from 'react'

const CONFIRM_SIGN_IN_INTERVAL = 5_000

/** Properties of {@link Registration} component. */
export interface RegistrationProps {
/** Called when the user agrees to the current Terms of Service and Privacy Policy. */
readonly userAgreed: () => void
}

/** A form for users to register an account. */
export default function Registration(props: RegistrationProps) {
const { userAgreed } = props
export default function Registration() {
const { signUp, confirmSignUp, resendSignUp, signInWithPassword } = useSession()

const { router } = useRouter()
Expand Down Expand Up @@ -72,8 +65,6 @@ export default function Registration(props: RegistrationProps) {
}
}),
onSubmit: async ({ email, password }) => {
userAgreed()

await signUp(email, password, organizationId ?? null)

stepperState.nextStep()
Expand Down
9 changes: 8 additions & 1 deletion app/gui/src/providers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type * as cognitoModule from '$/authentication/cognito'
import {
latestPrivacyPolicyQueryOptions,
latestTermsOfServiceQueryOptions,
} from '$/composables/userAgreements'
import { useFeatureFlag } from '$/providers/featureFlags'
import * as analytics from '$/utils/analytics'
import { proxyRefs, type ToValue } from '$/utils/reactivity'
Expand Down Expand Up @@ -122,6 +126,8 @@ function createAuthStore(
await updateUserMutation.mutateAsync({ username })
} else {
const orgId = await organizationId()
const tosHash = (await queryClient.fetchQuery(latestTermsOfServiceQueryOptions)).hash
const ppHash = (await queryClient.fetchQuery(latestPrivacyPolicyQueryOptions)).hash
const email = session.value?.email ?? ''

invariant(orgId == null || backendModule.isOrganizationId(orgId), 'Invalid organization ID')
Expand All @@ -130,6 +136,8 @@ function createAuthStore(
userName: username,
userEmail: backendModule.EmailAddress(email),
organizationId: orgId != null ? orgId : null,
tosAccepted: tosHash,
ppAccepted: ppHash,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here we have assumption that user accepted tos and pp during registration? Please add a comment, because it looks little buggy atm.

Also, technically tos and pp could change in a time between registration and first run of the application, but I don't feel we need to cover it right now :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

| So here we have assumption that user accepted tos and pp during registration? Please add a comment, because it looks little buggy atm.
yes we do. user can not submit the registration form without checking tos and pp and the hashes are being send within createUser request body

| Also, technically tos and pp could change in a time between registration and first run of the application, but I don't feel we need to cover it right now :)

Backend returns users accepted tos and pp and if they are different than the ones downloaded from pages then users is asked to reconfirm acceptation on applicatio open. Same the new hashes are being send with updateUser request

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh I see the case you mean.. I will add custom cognito attribues to pass accepted hashes

})
}
// Wait until the backend returns a value from `users/me`,
Expand All @@ -141,7 +149,6 @@ function createAuthStore(

return true
}

const usersMeQueryOptions = createUsersMeQuery(session, remoteBackend, setUsername)

const usersMeQuery = vueQuery.useQuery(usersMeQueryOptions)
Expand Down
2 changes: 1 addition & 1 deletion app/gui/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -521,12 +521,12 @@
"./src/project-view/components/GraphEditor/GraphEdge/layout.ts",
"./src/project-view/components/GraphEditor/GraphEdges.vue",
"./src/project-view/components/GraphEditor/GraphNode.vue",
"./src/project-view/components/GraphEditor/GraphNodeAlignmentSubmenu.vue",
"./src/project-view/components/GraphEditor/GraphNode/nodeMessage.ts",
"./src/project-view/components/GraphEditor/GraphNode/nodeVisualization.ts",
"./src/project-view/components/GraphEditor/GraphNodeComment.vue",
"./src/project-view/components/GraphEditor/GraphNodeMessage.vue",
"./src/project-view/components/GraphEditor/GraphNodeOutputPorts.vue",
"./src/project-view/components/GraphEditor/GraphNodeSubmenu.vue",
"./src/project-view/components/GraphEditor/GraphNodes.vue",
"./src/project-view/components/GraphEditor/GraphVisualization.vue",
"./src/project-view/components/GraphEditor/GraphVisualization/VisualizationToolbar.vue",
Expand Down
Loading