Skip to content

Commit

Permalink
feat: add index of projects
Browse files Browse the repository at this point in the history
  • Loading branch information
TruffeCendree committed Nov 12, 2021
1 parent 2fe69af commit 41b987e
Show file tree
Hide file tree
Showing 11 changed files with 120 additions and 7 deletions.
2 changes: 2 additions & 0 deletions src/lib/fastify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fastify from 'fastify'
import cookie, { FastifyCookieOptions } from 'fastify-cookie'
import fastifySwagger from 'fastify-swagger'
import { UnauthorizedError } from '../policies/policy'
import { projectsRoutes } from '../routes/projects'
import { sessionRoutes } from '../routes/sessions'
import { userRoutes } from '../routes/users'
import { COOKIE_SECRET, FASTIFY_LOGGING } from './dotenv'
Expand All @@ -15,6 +16,7 @@ export const server = fastify({ logger: FASTIFY_LOGGING })
.register(fastifySwagger, swaggerConfig)
.register(sessionRoutes, { prefix: '/sessions' })
.register(userRoutes, { prefix: '/users' })
.register(projectsRoutes, { prefix: '/projects' })
.setErrorHandler((error, request, reply) => {
// based on https://github.com/fastify/fastify/blob/1e94070992d911a81a26597c25f2d35ae65f3d91/fastify.js#L74
if (error instanceof UnauthorizedError) {
Expand Down
6 changes: 5 additions & 1 deletion src/policies/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ export interface PolicyAction<Model> {
(currentSession: Session | null | undefined, record: Model): Promise<boolean | UnauthorizedError>
}

export interface PolicyActionIndex {
(currentSession: Session | null | undefined): Promise<boolean | UnauthorizedError>
}

export class UnauthorizedError extends Error {}

export async function authorizeOfFail<Model>(
policyAction: PolicyAction<Model>,
policyAction: PolicyAction<Model> | PolicyActionIndex,
currentSession: Session | null | undefined,
record: Model
) {
Expand Down
15 changes: 15 additions & 0 deletions src/policies/projects-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getConnection } from 'typeorm'
import { Project } from '../entities/project'
import { Session } from '../entities/session'
import { PolicyActionIndex } from './policy'

export const canIndexProject: PolicyActionIndex = async function canIndexProject(session) {
return !!session
}

export async function projectPolicyScope(session: Session) {
return getConnection()
.createQueryBuilder(Project, 'project')
.innerJoin('project.users', 'user')
.where('user.id = :userId', { userId: session.user.id })
}
20 changes: 20 additions & 0 deletions src/routes/projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { FastifyInstance } from 'fastify'
import { authorizeOfFail } from '../policies/policy'
import { canIndexProject, projectPolicyScope } from '../policies/projects-policy'
import { ProjectsIndexResponse } from '../schemas/types/projects.index.response'
import * as projectsIndexResponseSchema from '../schemas/json/projects.index.response.json'

export async function projectsRoutes(fastify: FastifyInstance) {
fastify.get('/', {
schema: {
response: {
200: projectsIndexResponseSchema
}
},
handler: async function index(request): Promise<ProjectsIndexResponse> {
await authorizeOfFail(canIndexProject, request.session, null)
const projects = (await projectPolicyScope(request.session!)).getMany()
return projects
}
})
}
3 changes: 2 additions & 1 deletion src/routes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { User } from '../entities/user'
import { authorizeOfFail } from '../policies/policy'
import { canShowUser } from '../policies/users-policy'
import { UsersShowParams } from '../schemas/types/users.show.params'
import { UsersShowResponse } from '../schemas/types/users.show.response'
import * as usersShowParamsSchema from '../schemas/json/users.show.params.json'
import * as usersShowResponseSchema from '../schemas/json/users.show.response.json'

Expand All @@ -13,7 +14,7 @@ export async function userRoutes(fastify: FastifyInstance) {
params: usersShowParamsSchema,
response: { 200: usersShowResponseSchema }
},
handler: async function show(request) {
handler: async function show(request): Promise<UsersShowResponse> {
const user =
request.params.id === 'me'
? (request.session?.user as User)
Expand Down
14 changes: 14 additions & 0 deletions src/schemas/json/projects.index.response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/schema",
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "number" },
"name": { "type": "string" },
"instructions": { "type": ["string", "null"] }
},
"additionalProperties": false,
"required": ["id", "name", "instructions"]
}
}
3 changes: 2 additions & 1 deletion src/schemas/json/users.show.response.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"lastname": { "type": "string" },
"email": { "type": "string" }
},
"additionalProperties": false
"additionalProperties": false,
"required": ["id", "firstname", "lastname", "email"]
}
12 changes: 12 additions & 0 deletions src/schemas/types/projects.index.response.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* tslint:disable */
/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/

export type ProjectsIndexResponse = {
id: number;
name: string;
instructions: string | null;
}[];
8 changes: 4 additions & 4 deletions src/schemas/types/users.show.response.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
*/

export interface UsersShowResponse {
id?: number;
firstname?: string;
lastname?: string;
email?: string;
id: number;
firstname: string;
lastname: string;
email: string;
}
18 changes: 18 additions & 0 deletions src/specs/fixtures/projects-fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getConnection } from 'typeorm'
import { Project } from '../../entities/project'
import { User } from '../../entities/user'
import * as faker from 'faker'

type ProjectFixtureOptions = { users?: User[] }

export function buildProjectFixture(opts: ProjectFixtureOptions = {}) {
const project = new Project()
project.name = faker.company.companyName()
project.instructions = faker.lorem.paragraphs(2)
if (opts.users) project.users = Promise.resolve(opts.users)
return project
}

export function createProjectFixture(opts: ProjectFixtureOptions = {}) {
return getConnection().getRepository(Project).save(buildProjectFixture(opts))
}
26 changes: 26 additions & 0 deletions src/specs/routes/projects.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect } from 'chai'
import { sign } from 'cookie-signature'
import { COOKIE_NAME, COOKIE_SECRET } from '../../lib/dotenv'
import { server } from '../../lib/fastify'
import { ProjectsIndexResponse } from '../../schemas/types/projects.index.response'
import { createProjectFixture } from '../fixtures/projects-fixtures'
import { createSessionFixture } from '../fixtures/sessions-fixtures'

describe('/projects', function () {
describe('#index', function () {
it('should return projects of the current user', async function () {
const session = await createSessionFixture()
const project1 = await createProjectFixture({ users: [session.user] })
const project2 = await createProjectFixture({ users: [session.user] })
const project3 = await createProjectFixture()

const cookies = { [COOKIE_NAME]: sign(session.id, COOKIE_SECRET) }
const response = await server.inject({ method: 'GET', url: '/projects', cookies })
expect(response.statusCode).to.eq(200)

const json = response.json<ProjectsIndexResponse>()
expect(json.map(_ => _.id)).to.have.members([project1.id, project2.id])
expect(json.map(_ => _.id)).to.not.include(project3.id)
})
})
})

0 comments on commit 41b987e

Please sign in to comment.