Skip to content
This repository has been archived by the owner on Jul 17, 2022. It is now read-only.

Term and conditions #844

Open
wants to merge 2 commits into
base: saga
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
19 changes: 19 additions & 0 deletions db/migrations/001-t&c.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DataTypes, QueryInterface } from 'sequelize'

export const up = async (queryInterface: QueryInterface) => {
await queryInterface.addColumn(
`UserAccounts`,
'termsAndConditionsAcceptedAt',
{
allowNull: true,
type: DataTypes.TIME,
},
)
}

export const down = async (queryInterface: QueryInterface) => {
await queryInterface.removeColumn(
`UserAccounts`,
'termsAndConditionsAcceptedAt',
)
}
27 changes: 27 additions & 0 deletions frontend/src/components/TermsAndConditions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { FunctionComponent } from 'react'
import CloseIcon from './alert/CloseIcon'

interface Props {
close: () => void
}

const TermsAndConditions: FunctionComponent<Props> = ({ close }) => {
return (
<div className="bg-gray-300 inset-1/4 fixed text-justify rounded-md">
<div className="absolute top-0 right-0">
<CloseIcon
color="text-black"
onClick={() => {
close()
}}
/>
</div>
<div className="p-4">
<h3 className="text-center text-2xl">Terms & Conditions</h3>
Placeholder text for terms and conitions
</div>
</div>
)
}

export default TermsAndConditions
23 changes: 23 additions & 0 deletions frontend/src/hooks/useUser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { headers, SERVER_URL, throwOnProblem } from './useAuth'

const updateTermsAndConditions = ({
id,
termsAndConditionsAcceptedAt,
}: {
id: number
termsAndConditionsAcceptedAt: Date
}) =>
fetch(`${SERVER_URL}/user/termsandcond/${id}`, {
credentials: 'include',
method: 'PATCH',
headers: {
...headers,
},
body: JSON.stringify(termsAndConditionsAcceptedAt),
}).then(throwOnProblem(`Updating terms and conditions failed!`))

export const useUser = () => {
return {
updateTermsAndConditions,
}
}
13 changes: 12 additions & 1 deletion frontend/src/pages/groups/GroupCreatePage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FunctionComponent } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../../hooks/useAuth'
import { useUser } from '../../hooks/useUser'
import LayoutWithNav from '../../layouts/LayoutWithNav'
import {
AllGroupsDocument,
Expand All @@ -12,8 +13,9 @@ import { setEmptyFieldsToUndefined } from '../../utils/setEmptyFieldsToUndefined
import GroupForm from './GroupForm'

const GroupCreatePage: FunctionComponent = () => {
const { refreshMe } = useAuth()
const { refreshMe, me: profile } = useAuth()
const navigate = useNavigate()
const user = useUser()

const [addGroup, { loading: mutationIsLoading, error: mutationError }] =
useCreateGroupMutation()
Expand All @@ -30,6 +32,14 @@ const GroupCreatePage: FunctionComponent = () => {
refetchQueries: [{ query: AllGroupsDocument }],
})

// updated user with termsAndConditionsAcceptedAt time
if (profile != undefined) {
user.updateTermsAndConditions({
id: profile.id,
termsAndConditionsAcceptedAt: input.termsAndConditionsAcceptedAt,
})
}

// Because we cache the association between a group captain and their
// group, we need to refresh that association when they create their first
// group.
Expand Down Expand Up @@ -65,6 +75,7 @@ const GroupCreatePage: FunctionComponent = () => {
onSubmit={onSubmit}
isLoading={mutationIsLoading}
submitButtonLabel="Create group"
renderTermsAndConditions={true}
/>
</main>
</div>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/groups/GroupEditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const GroupEditPage: FunctionComponent = () => {
submitButtonLabel="Save changes"
onSubmit={onSubmit}
defaultValues={originalGroupData}
renderTermsAndConditions={false}
/>
</main>
</div>
Expand Down
46 changes: 45 additions & 1 deletion frontend/src/pages/groups/GroupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ import {
PropsWithChildren,
ReactNode,
useEffect,
useState,
} from 'react'
import { useForm } from 'react-hook-form'
import Button from '../../components/Button'
import SelectField from '../../components/forms/SelectField'
import TextArea from '../../components/forms/TextArea'
import TextField from '../../components/forms/TextField'
import TermsAndConditions from '../../components/TermsAndConditions'
import { GROUP_TYPE_OPTIONS } from '../../data/constants'
import { useAuth } from '../../hooks/useAuth'
import { useCountries } from '../../hooks/useCountries'
import { useRegions } from '../../hooks/useRegions'
import { GroupCreateInput, GroupQuery, GroupType } from '../../types/api-types'
import { formatRegion } from '../../utils/format'
import { stripIdAndTypename } from '../../utils/types'
import TermsAndCondCheckbox from './TermsAndCondCheckbox'

interface Props {
/**
Expand All @@ -35,6 +38,11 @@ interface Props {
* The callback triggered when the user submits the form
*/
onSubmit: (input: GroupCreateInput) => void
/**
* If true, checkbox for terms and conditions will be displayed
* We don't want to display checkbox terms and conditions on update page
*/
renderTermsAndConditions: boolean
}

/**
Expand All @@ -44,6 +52,10 @@ const GroupForm: FunctionComponent<PropsWithChildren<Props>> = (props) => {
const { me: profile } = useAuth()
const countries = useCountries()
const regions = useRegions()
const [showTermsAndCond, setShowTermsAndCond] = useState<boolean>(false)
const [timeTcChecked, setTimeTcChecked] = useState<Date | null>(null)

console.log(profile)
Copy link
Member

Choose a reason for hiding this comment

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

Remove.


const {
register,
Expand Down Expand Up @@ -73,6 +85,10 @@ const GroupForm: FunctionComponent<PropsWithChildren<Props>> = (props) => {
input.groupType = GroupType.Regular
}

if (timeTcChecked !== null) {
//input.termsAndConditionsAcceptedAt = timeTcChecked
}

props.onSubmit({
...input,
// FIXME: for some reasons, servingRegions will be `false` in no item is selected
Expand All @@ -81,8 +97,32 @@ const GroupForm: FunctionComponent<PropsWithChildren<Props>> = (props) => {
})
})

function handleTcChange() {
if (timeTcChecked === null) {
setTimeTcChecked(new Date())
} else {
setTimeTcChecked(null)
}
}

let termsAndConditions
if (props.renderTermsAndConditions) {
termsAndConditions = (
<TermsAndCondCheckbox
handleTcChange={handleTcChange}
timeTcChecked={timeTcChecked}
setShowTermsAndCond={setShowTermsAndCond}
/>
)
} else {
termsAndConditions = null
}

return (
<form onSubmit={submitForm}>
{showTermsAndCond ? (
<TermsAndConditions close={() => setShowTermsAndCond(false)} />
) : null}
<fieldset className="space-y-4">
<TextField
label="Group name"
Expand Down Expand Up @@ -174,6 +214,7 @@ const GroupForm: FunctionComponent<PropsWithChildren<Props>> = (props) => {
register={register}
errors={errors}
/>
{termsAndConditions}
</fieldset>

<fieldset className="mt-12">
Expand All @@ -199,7 +240,10 @@ const GroupForm: FunctionComponent<PropsWithChildren<Props>> = (props) => {
variant="primary"
type="submit"
className="mt-6"
disabled={props.isLoading}
disabled={
props.isLoading ||
(timeTcChecked === null && props.renderTermsAndConditions)
}
>
{props.submitButtonLabel}
</Button>
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/pages/groups/TermsAndCondCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FunctionComponent } from 'react'
import CheckboxField from '../../components/forms/CheckboxField'

interface Props {
timeTcChecked: Date | null
handleTcChange: () => void
setShowTermsAndCond: (tc: boolean) => void
}

const TermsAndCondCheckbox: FunctionComponent<Props> = ({
timeTcChecked,
handleTcChange,
setShowTermsAndCond,
}) => {
return (
<div className="flex flex-row">
<CheckboxField
label=""
checked={timeTcChecked === null ? false : true}
onChange={handleTcChange}
className="cursor-pointer"
/>
<a
className="cursor-pointer text-blue-500"
onClick={() => setShowTermsAndCond(true)}
>
I accept terms and conditions
</a>
</div>
)
}

export default TermsAndCondCheckbox
1 change: 1 addition & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ input GroupCreateInput {
primaryContact: ContactInfoInput!
website: String
servingRegions: [ID!]
termsAndConditionsAcceptedAt: DateTime
}

input GroupUpdateInput {
Expand Down
4 changes: 4 additions & 0 deletions src/models/user_account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface UserAccountAttributes {
isAdmin?: boolean
isConfirmed?: boolean
name: string
termsAndConditionsAcceptedAt?: Date
}

export interface UserAccountCreationAttributes
Expand Down Expand Up @@ -57,6 +58,9 @@ export default class UserAccount extends Model<
@Column
public isConfirmed!: boolean

@Column
public termsAndConditionsAcceptedAt!: Date

@CreatedAt
@Column
public readonly createdAt!: Date
Expand Down
75 changes: 74 additions & 1 deletion src/routes/user/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Request, Response } from 'express'
import { AuthContext } from '../../authenticateRequest'
import { errorsToProblemDetail } from '../../input-validation/errorsToProblemDetail'
import { validateIdInput } from '../../input-validation/idInputSchema'
import { DateTime } from '../../input-validation/types'
import { validateWithJSONSchema } from '../../input-validation/validateWithJSONSchema'
import UserAccount from '../../models/user_account'
import { HTTPStatusCode } from '../../rest/response/HttpStatusCode'
Expand All @@ -15,6 +16,7 @@ const adminUpdateUserInput = Type.Object(
isAdmin: Type.Optional(Type.Boolean()),
email: Type.Optional(emailInput),
name: Type.Optional(Type.String({ minLength: 1, maxLength: 255 })),
termsAndConditionsAcceptedAt: Type.Optional(DateTime),
},
{ additionalProperties: false },
)
Expand Down Expand Up @@ -51,7 +53,78 @@ export const adminUpdateUser = async (request: Request, response: Response) => {
status: HTTPStatusCode.NotFound,
})

await user.update(validInput.value)
let input
if (validInput.value.termsAndConditionsAcceptedAt != undefined) {
input = {
...validInput.value,
termsAndConditionsAcceptedAt: new Date(
validInput.value.termsAndConditionsAcceptedAt,
),
}
} else {
input = {
...validInput.value,
termsAndConditionsAcceptedAt: undefined,
}
}

await user.update(input)

return response.status(HTTPStatusCode.NoContent).end()
}

const updateUserTermsAndCondInput = Type.Object(
{
termsAndConditionsAcceptedAt: DateTime,
},
{ additionalProperties: false },
)

const validateUpdateUserTermsAndCondInput = validateWithJSONSchema(
updateUserTermsAndCondInput,
)

export const updateTermsAndConditions = async (
request: Request,
response: Response,
) => {
const id = parseInt(request.params.id, 10)
// Validate user id and update input
const validId = validateIdInput({ id })
if ('errors' in validId) {
return respondWithProblem(response, errorsToProblemDetail(validId.errors))
}

const validInput = validateUpdateUserTermsAndCondInput(request.body)
if ('errors' in validInput) {
return respondWithProblem(
response,
errorsToProblemDetail(validInput.errors),
)
}

// Check if user updates itself terms and conditions
const authContext = request.user as AuthContext
if (authContext.userId != id) {
return response.sendStatus(HTTPStatusCode.Forbidden).end()
}

// Find user
const user = await UserAccount.findByPk(validId.value.id)
if (user === null) {
return respondWithProblem(response, {
title: `User with id ${validId.value.id} not found!`,
status: HTTPStatusCode.NotFound,
})
}

let input = {
termsAndConditionsAcceptedAt: new Date(
validInput.value.termsAndConditionsAcceptedAt,
),
}

await user.update(input)

return response.status(HTTPStatusCode.NoContent).end()
}
Loading