Skip to content

Commit

Permalink
Implement backend tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
AymericBethencourt committed Jun 22, 2020
1 parent de76488 commit e99a8f7
Show file tree
Hide file tree
Showing 30 changed files with 540 additions and 295 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
45 changes: 45 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,45 @@
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()
}
14 changes: 8 additions & 6 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 @@ -21,6 +22,7 @@ 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)
Expand Down
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
11 changes: 9 additions & 2 deletions src/frontend/src/app/App.components/Header/Header.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { JwtDecoded } from 'shared/user/JwtDecoded'

import { Hamburger } from '../Hamburger/Hamburger.controller'
// prettier-ignore
import { HeaderBg, HeaderLogo, HeaderStyled, HeaderLoggedOut, HeaderLoggedIn, HeaderMenuItem } from "./Header.style";
import { HeaderBg, HeaderLoggedIn, HeaderLoggedOut, HeaderLogo, HeaderMenuItem, HeaderStyled } from "./Header.style";

type HeaderViewProps = {
user?: JwtDecoded | undefined
Expand Down Expand Up @@ -49,7 +49,14 @@ function loggedOutHeader() {
function loggedInHeader({ user, removeAuthUserCallback }: HeaderViewProps) {
return (
<HeaderLoggedIn>
<HeaderMenuItem>{user?.username}</HeaderMenuItem>
<Link
to={`/user/${user?.username}`}
onClick={() => {
removeAuthUserCallback()
}}
>
<HeaderMenuItem>{user?.username}</HeaderMenuItem>
</Link>
<Link
to="/"
onClick={() => {
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/src/app/App.routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Home } from 'pages/Home/Home.controller'
import { Login } from 'pages/Login/Login.controller'
import { ResetPassword } from 'pages/ResetPassword/ResetPassword.controller'
import { SignUp } from 'pages/SignUp/SignUp.controller'
import { User } from 'pages/User/User.controller'
import { VerifyEmail } from 'pages/VerifyEmail/VerifyEmail.controller'
import React from 'react'
import { Route, Switch } from 'react-router-dom'
Expand Down Expand Up @@ -54,6 +55,9 @@ export const AppRoutes = ({ location }: any) => (
<Route path="/camel/chapter-*">
<Chapter />
</Route>
<Route exact path="/user/:username">
<User />
</Route>
<Route>
<Error404 />
</Route>
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/pages/Blank/Blank.actions.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { store } from 'index'
import { GetPublicUserInputs } from 'shared/user/GetPublicUser'
import { GetPublicUserInputs } from 'shared/page/GetPublicUser'

export const GET_BLANK_REQUEST = 'GET_BLANK_REQUEST'
export const GET_BLANK_COMMIT = 'GET_BLANK_COMMIT'
Expand Down
Loading

0 comments on commit e99a8f7

Please sign in to comment.