Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
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
6 changes: 6 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 @@ -1131,6 +1135,8 @@ 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
17 changes: 16 additions & 1 deletion app/gui/src/authentication/cognito.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ export interface ISessionProvider {
username: string,
password: string,
organizationId: string | null,
tosHash: string,
ppHash: string,
) => Promise<results.Err<SignUpError> | results.Ok<unknown>>
readonly confirmSignUp: (
email: string,
Expand Down Expand Up @@ -322,13 +324,21 @@ export class Cognito implements ISessionProvider {
*
* Does not rely on federated identity providers (e.g., Google or GitHub).
*/
async signUp(username: string, password: string, organizationId: string | null) {
async signUp(
username: string,
password: string,
organizationId: string | null,
tosHash: string,
ppHash: string,
) {
const result = await results.Result.wrapAsync(async () => {
const params = intoSignUpParams(
this.supportsDeepLinks,
username.toLowerCase(),
password,
organizationId,
tosHash,
ppHash,
)
await amplify.signUp(params)
})
Expand Down Expand Up @@ -706,6 +716,8 @@ function intoSignUpParams(
username: string,
password: string,
organizationId: string | null,
tosHashAccepted: string,
ppHashAccepted: string,
): amplify.SignUpInput {
return {
username,
Expand All @@ -721,6 +733,9 @@ function intoSignUpParams(
*/
...(supportsDeepLinks ? { 'custom:fromDesktop': JSON.stringify(true) } : {}),
...(organizationId != null ? { 'custom:organizationId': organizationId } : {}),
/** Custom attributes that stores hashes of accepted terms of service and privacy policy. */
...{ 'custom:initTosHash': tosHashAccepted },
...{ initPpHash: ppHashAccepted },
},
},
}
Expand Down
28 changes: 0 additions & 28 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.

This file was deleted.

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
21 changes: 10 additions & 11 deletions app/gui/src/dashboard/pages/authentication/Registration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,20 @@ import { useEventCallback } from '#/hooks/eventCallbackHooks'
import AuthenticationPage from '#/pages/authentication/AuthenticationPage'
import { passwordWithPatternSchema } from '#/pages/authentication/schemas'
import { DASHBOARD_PATH, LOGIN_PATH } from '$/appUtils'
import {
latestPrivacyPolicyQueryOptions,
latestTermsOfServiceQueryOptions,
} from '$/composables/userAgreements'
import { useAuth } from '$/providers/auth'
import { useBackends, useLocalStorage, useRouter, useSession, useText } from '$/providers/react'
import { useQueryParam } from '$/providers/react/queryParams'
import * as vueQuery from '@tanstack/vue-query'
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 All @@ -45,6 +43,7 @@ export default function Registration(props: RegistrationProps) {
const [organizationId] = useQueryParam('organization_id')
const [redirectTo] = useQueryParam('redirect_to')
const [isManualCodeEntry, setIsManualCodeEntry] = useState(false)
const queryClient = vueQuery.useQueryClient()

const signupForm = Form.useForm({
defaultValues: { email: initialEmail ?? '', agreedToTos: [], agreedToPrivacyPolicy: [] },
Expand Down Expand Up @@ -72,9 +71,9 @@ export default function Registration(props: RegistrationProps) {
}
}),
onSubmit: async ({ email, password }) => {
userAgreed()

await signUp(email, password, organizationId ?? null)
const tosHash = (await queryClient.fetchQuery(latestTermsOfServiceQueryOptions)).hash
const ppHash = (await queryClient.fetchQuery(latestPrivacyPolicyQueryOptions)).hash
await signUp(email, password, organizationId ?? null, tosHash, ppHash)

stepperState.nextStep()
},
Expand Down
1 change: 0 additions & 1 deletion app/gui/src/providers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ function createAuthStore(

return true
}

const usersMeQueryOptions = createUsersMeQuery(session, remoteBackend, setUsername)

const usersMeQuery = vueQuery.useQuery(usersMeQueryOptions)
Expand Down
10 changes: 8 additions & 2 deletions app/gui/src/providers/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,16 @@ export function createSessionStore(
meta: { invalidates: [sessionQueryOptions.queryKey], awaitInvalidates: true },
})

const signUp = async (username: string, password: string, organizationId: string | null) => {
const signUp = async (
username: string,
password: string,
organizationId: string | null,
tosHash: string,
ppHash: string,
) => {
const auth = assertAuthService()
analytics.cloudSignUp.before()
const result = await auth.signUp(username, password, organizationId)
const result = await auth.signUp(username, password, organizationId, tosHash, ppHash)

if (result.err) {
throw new Error(result.val.message)
Expand Down
3 changes: 2 additions & 1 deletion app/gui/src/router/router.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Login from '#/pages/authentication/Login'
import Registration from '#/pages/authentication/Registration'
import {
CONFIRM_REGISTRATION_PATH,
DASHBOARD_PATH,
Expand Down Expand Up @@ -43,7 +44,7 @@ const routes = [
{
path: REGISTRATION_PATH,
meta: { access: 'guest' },
component: withDataLoader(() => import('$/components/RegistrationPage.vue')),
component: reactComponent(Registration),
},
{
path: UNAVAILABLE_PATH,
Expand Down
1 change: 0 additions & 1 deletion app/gui/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"./src/components/CommandPalette.vue",
"./src/components/LoadingScreen.vue",
"./src/components/ProtectedLayout.vue",
"./src/components/RegistrationPage.vue",
"./src/components/WithCurrentProject.vue",
"./src/composables/appTitle.ts",
"./src/composables/paywall/FeaturesConfiguration.ts",
Expand Down
Loading