Skip to content

Commit

Permalink
Merge pull request #33 from octo-technology/track-progress
Browse files Browse the repository at this point in the history
Track progress
  • Loading branch information
frankhillard authored Jun 23, 2020
2 parents de76488 + 0901201 commit 00fcf6c
Show file tree
Hide file tree
Showing 51 changed files with 776 additions and 1,004 deletions.
1 change: 1 addition & 0 deletions src/api/src/helpers/toPublicUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const toPublicUser = (user: User): PublicUser => {
_id: user._id,
username: user.username,
emailVerified: user.emailVerified,
progress: user.progress,
createdAt: user.createdAt,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { Context, Next } from 'koa'

import { firstError } from '../../../helpers/firstError'
import { ResponseError } from '../../../shared/mongo/ResponseError'
import { GetPublicUserInputs, GetPublicUserOutputs } from '../../../shared/user/GetPublicUser'
import { GetPublicUserInputs, GetPublicUserOutputs } from '../../../shared/page/GetPublicUser'
import { PublicUser } from '../../../shared/user/PublicUser'
import { UserModel } from '../../../shared/user/User'

export const PUBLIC_USER_MONGO_SELECTOR = '_id username emailVerified createdAt'
export const PUBLIC_USER_MONGO_SELECTOR = '_id username emailVerified progress createdAt'

export const getPublicUser = async (ctx: Context, next: Next): Promise<void> => {
const getPublicUserArgs = plainToClass(GetPublicUserInputs, ctx.request.body, { excludeExtraneousValues: true })
Expand Down
46 changes: 46 additions & 0 deletions src/api/src/resolvers/user/addProgress/addProgress.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Context, Next } from 'koa'

import { Jwt } from '../../../shared/user/Jwt'
import { User } from '../../../shared/user/User'
import { createTestUser } from '../../../test/createTestUser'
import { deleteTestUser } from '../../../test/deleteTestUser'
import { mockConnect } from '../../../test/mockConnect'
import { addProgress } from './addProgress'

let user: User
let next: Next
let jwt: Jwt

describe('User', () => {
beforeAll(async () => {
await mockConnect()
const created = await createTestUser('[email protected]', 'bob', 'Bob1234#')
user = created.user
jwt = created.jwt
next = created.next
})

it('can add progress', async (done) => {
const ctx: Context = {
request: {
headers: {
authorization: 'Bearer ' + jwt,
},
body: {
chapterDone: '/pascal/chapter-polymorphism',
},
},
} as Context

await addProgress(ctx, next)

expect(ctx.body.user).toBeDefined()
expect(ctx.body.user.progress).toContain('/pascal/chapter-polymorphism')

done()
})

afterAll(async () => {
await deleteTestUser(user._id)
})
})
41 changes: 41 additions & 0 deletions src/api/src/resolvers/user/addProgress/addProgress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { plainToClass } from 'class-transformer'
import { validateOrReject } from 'class-validator'
import { Context, Next } from 'koa'

import { firstError } from '../../../helpers/firstError'
import { toPublicUser } from '../../../helpers/toPublicUser'
import { AddProgressInputs, AddProgressOutputs } from '../../../shared/user/AddProgress'
import { PublicUser } from '../../../shared/user/PublicUser'
import { User, UserModel } from '../../../shared/user/User'
import { rateLimit } from '../../quota/rateLimit/rateLimit'
import { authenticate } from '../helpers/authenticate'

export const PUBLIC_USER_MONGO_SELECTOR = '_id username emailVerified createdAt'

export const addProgress = async (ctx: Context, next: Next): Promise<void> => {
const addProgressArgs = plainToClass(AddProgressInputs, ctx.request.body, { excludeExtraneousValues: true })
await validateOrReject(addProgressArgs, { forbidUnknownValues: true }).catch(firstError)
const { chapterDone } = addProgressArgs

const user: User = await authenticate(ctx)

await rateLimit(user._id)

await UserModel.updateOne(
{ _id: user._id },
{ $addToSet: { progress: chapterDone } },
).exec()

const updatedUser: User = await UserModel.findOne(
{ _id: user._id },
).lean() as User

const publicUser: PublicUser = toPublicUser(updatedUser)

const response: AddProgressOutputs = { user: publicUser }

ctx.status = 200
ctx.body = response

await next()
}
17 changes: 10 additions & 7 deletions src/api/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import * as Router from '@koa/router'
import { Context } from 'koa'

import { signUp } from './resolvers/user/signUp/signUp'
import { getPublicUser } from './resolvers/page/getPublicUser/getPublicUser'
import { addProgress } from './resolvers/user/addProgress/addProgress'
import { changePassword } from './resolvers/user/changePassword/changePassword'
import { forgotPassword } from './resolvers/user/forgotPassword/forgotPassword'
import { login } from './resolvers/user/login/login'
import { verifyEmail } from './resolvers/user/verifyEmail/verifyEmail'
import { resetPassword } from './resolvers/user/resetPassword/resetPassword'
import { resendEmailVerification } from './resolvers/user/resendEmailVerification/resendEmailVerification'
import { getPublicUser } from './resolvers/user/getPublicUser/getPublicUser'
import { forgotPassword } from './resolvers/user/forgotPassword/forgotPassword'
import { changePassword } from './resolvers/user/changePassword/changePassword'
import { resetPassword } from './resolvers/user/resetPassword/resetPassword'
import { signUp } from './resolvers/user/signUp/signUp'
import { verifyEmail } from './resolvers/user/verifyEmail/verifyEmail'

const router = new Router()

Expand All @@ -20,9 +21,11 @@ router.post('/user/sign-up', signUp)
router.post('/user/login', login)
router.post('/user/verify-email', verifyEmail)
router.post('/user/resend-email-verification', resendEmailVerification)
router.post('/user/get-public-user', getPublicUser)
router.post('/user/add-progress', addProgress)
router.post('/user/reset-password', resetPassword)
router.post('/user/forgot-password', forgotPassword)
router.post('/user/change-password', changePassword)

router.post('/page/get-user', getPublicUser)

export { router }
2 changes: 1 addition & 1 deletion src/api/src/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ResponseError } from './shared/mongo/ResponseError'

export const sanitize = () => async (ctx: Context, next: Next): Promise<void> => {
const body = JSON.stringify(ctx.request.body)
const forbidden = new RegExp('<|/|>', 'i')
const forbidden = new RegExp('<|>', 'i')
if (forbidden.test(body)) throw new ResponseError(400, 'Forbidden characters')
else await next()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Length, Matches } from 'class-validator'
import { Expose } from 'class-transformer'
import { PublicUser } from './PublicUser'
import { PublicUser } from '../user/PublicUser'

export class GetPublicUserInputs {
@Expose()
Expand Down
15 changes: 15 additions & 0 deletions src/api/src/shared/user/AddProgress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Expose } from 'class-transformer'
import { Length, Matches } from 'class-validator'

import { PublicUser } from '../user/PublicUser'

export class AddProgressInputs {
@Expose()
@Length(2, 100)
@Matches(/^[a-zA-Z0-9-\/]*$/, { message: 'Chapter slug can only contain letters, numbers, dashes and slashes' })
chapterDone!: string
}

export class AddProgressOutputs {
user!: PublicUser
}
5 changes: 4 additions & 1 deletion src/api/src/shared/user/PublicUser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsDate, IsEmail, IsMongoId, Length, Matches } from 'class-validator'
import { IsArray, IsDate, IsEmail, IsMongoId, Length, Matches } from 'class-validator'
import { ObjectId } from 'mongodb'

export class PublicUser {
Expand All @@ -12,6 +12,9 @@ export class PublicUser {
@IsEmail()
emailVerified?: boolean

@IsArray()
progress?: string[]

@IsDate()
createdAt!: Date
}
3 changes: 3 additions & 0 deletions src/api/src/shared/user/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export class User {
@Property({ required: true })
hashedPassword!: string

@Property({ nullable: true, optional: true })
progress?: string[]

@IsDate()
createdAt!: Date

Expand Down
Loading

0 comments on commit 00fcf6c

Please sign in to comment.