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

Commit

Permalink
Terms and conditions checkbox
Browse files Browse the repository at this point in the history
  • Loading branch information
pavle995 authored and coderbyheart committed Jul 11, 2022
1 parent 3e7244f commit d44407d
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 3 deletions.
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',
)
}
2 changes: 1 addition & 1 deletion frontend/src/pages/groups/TermsAndCondCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const TermsAndCondCheckbox: FunctionComponent<Props> = ({
className="cursor-pointer text-blue-500"
onClick={() => setShowTermsAndCond(true)}
>
&nbsp;I accept terms and conditions
I accept terms and conditions
</a>
</div>
)
Expand Down
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()
}
6 changes: 5 additions & 1 deletion src/server/feat/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import setNewPasswordUsingTokenAndEmail from '../../routes/password/new'
import sendVerificationTokenByEmail from '../../routes/password/token'
import registerUser from '../../routes/register'
import confirmRegistrationByEmail from '../../routes/register/confirm'
import { adminUpdateUser } from '../../routes/user/update'
import {
adminUpdateUser,
updateTermsAndConditions,
} from '../../routes/user/update'
import { adminListUsers } from '../../routes/users'
import sendShipmentExportCsv from '../../sendShipmentExportCsv'
import { addRequestId } from '../addRequestId'
Expand Down Expand Up @@ -90,6 +93,7 @@ export const backend = ({

app.get('/users', cookieAuth, adminListUsers)
app.patch('/user/:id', cookieAuth, adminUpdateUser)
app.patch('/user/termsandcond/:id', cookieAuth, updateTermsAndConditions)

app.use(compression())

Expand Down
114 changes: 114 additions & 0 deletions src/tests/updateUser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { json } from 'body-parser'
import cookieParser from 'cookie-parser'
import express, { Express } from 'express'
import { createServer, Server } from 'http'
import passport from 'passport'
import request, { SuperTest, Test } from 'supertest'
import { v4 } from 'uuid'
import {
authCookie as getAuthCookie,
authCookieName,
cookieAuthStrategy,
} from '../authenticateRequest'
import UserAccount from '../models/user_account'
import { HTTPStatusCode } from '../rest/response/HttpStatusCode'
import login from '../routes/login'
import { hashPassword } from '../routes/register'
import { updateTermsAndConditions } from '../routes/user/update'
import { tokenCookieRx } from './helpers/auth'

jest.setTimeout(15 * 1000)

const cookieAuth = passport.authenticate('cookie', { session: false })
passport.use(cookieAuthStrategy)

const password = '2DhE.sf!f9Z3u8x'

describe('User update API', () => {
let app: Express
let httpServer: Server
let r: SuperTest<Test>

const getExpressCookie = getAuthCookie(1800)

beforeAll(async () => {
app = express()
app.use(cookieParser(process.env.COOKIE_SECRET ?? 'cookie-secret'))
app.use(json())
app.use(passport.initialize())
app.post('/auth/login', login(getExpressCookie))
app.patch('/user/termsandcond/:id', cookieAuth, updateTermsAndConditions)
httpServer = createServer(app)
await new Promise<void>((resolve) =>
httpServer.listen(8888, '127.0.0.1', undefined, resolve),
)
r = request('http://127.0.0.1:8888')
})
afterAll(async () => {
httpServer.close()
})

const getCookieForUserAccount = async ({
email,
password,
}: {
email: string
password: string
}) => {
const res = await r
.post('/auth/login')
.send({
email,
password,
})
.expect(HTTPStatusCode.NoContent)
.expect('set-cookie', tokenCookieRx)

return tokenCookieRx.exec(res.header['set-cookie'])?.[1] as string
}

const userEmail = `some-user${v4()}@example.com`
let userId: number

/** Create and log in admin */
beforeAll(async () => {
await UserAccount.create({
email: userEmail,
isAdmin: false, // not an admin
name: 'Some User',
passwordHash: hashPassword(password, 1),
isConfirmed: true,
})

const user = await UserAccount.findOneByEmail(userEmail)
if (user?.id != undefined) {
userId = user?.id
}
})

describe('users should be allowed to update its termsAndConditionsAcceptedAt', () => {
let userAuthCookie: string
beforeAll(async () => {
userAuthCookie = await getCookieForUserAccount({
email: userEmail,
password,
})
})
test('probaj-ovo', () =>
r
.patch(`/user/termsandcond/${userId as number}`)
.set('Cookie', [`${authCookieName}=${userAuthCookie}`])
.send({
termsAndConditionsAcceptedAt: new Date(),
})
.expect(HTTPStatusCode.NoContent))
test('users should not be allowed to update other users termsAndConditionsAcceptedAt', () =>
r
.patch('/user/termsandcond/999')
.set('Cookie', [`${authCookieName}=${userAuthCookie}`])
.send({
termsAndConditionsAcceptedAt: new Date(),
})
.expect(HTTPStatusCode.Forbidden))
})
})

0 comments on commit d44407d

Please sign in to comment.