Skip to content

Commit

Permalink
Merge pull request #150 from codex-team/feat/revoke-invitation-hash
Browse files Browse the repository at this point in the history
Feat: endpoint to revoke hash
  • Loading branch information
kloV148 authored Jan 23, 2024
2 parents c523ed9 + 08581b2 commit 2318569
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 0 deletions.
21 changes: 21 additions & 0 deletions src/domain/service/noteSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,27 @@ export default class NoteSettingsService {
return await this.teamRepository.createTeamMembership(team);
}

/**
* Updates invitation hash in note settings
*
* @param noteId - note internal id
* @returns updated note settings
*/
public async regenerateInvitationHash(noteId: NoteInternalId): Promise<NoteSettings> {
/**
* Generates a new invitation hash
*/
const data = { invitationHash: createInvitationHash() };

const updatedNoteSettings = await this.patchNoteSettingsByNoteId(noteId, data);

if (updatedNoteSettings === null) {
throw new DomainError(`Note settings was not updated`);
}

return updatedNoteSettings;
}

/**
* Patch team member role by user and note id
*
Expand Down
2 changes: 2 additions & 0 deletions src/presentation/http/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import AIRouter from '@presentation/http/router/ai.js';
import EditorToolsRouter from './router/editorTools.js';
import { UserSchema } from './schema/User.js';
import { NoteSchema } from './schema/Note.js';
import { NoteSettingsSchema } from './schema/NoteSettings.js';
import Policies from './policies/index.js';
import type { RequestParams, Response } from '@presentation/api.interface.js';
import NoteSettingsRouter from './router/noteSettings.js';
Expand Down Expand Up @@ -278,6 +279,7 @@ export default class HttpApi implements Api {
this.server?.addSchema(UserSchema);
this.server?.addSchema(NoteSchema);
this.server?.addSchema(EditorToolSchema);
this.server?.addSchema(NoteSettingsSchema);
this.server?.addSchema(JoinSchemaParams);
this.server?.addSchema(JoinSchemaResponse);
}
Expand Down
77 changes: 77 additions & 0 deletions src/presentation/http/router/noteSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,82 @@ describe('NoteSettings API', () => {
test.todo('Return 403 when user authorized, but not member of the team');
});

describe('PATCH /note-settings/:notePublicId/invitation-hash ', () => {
test('Returns status 401 when the user is not authorized', async () => {
const correctID = 'Pq1T9vc23Q';

const response = await global.api?.fakeRequest({
method: 'PATCH',
url: `/note-settings/${correctID}/invitation-hash`,
});

expect(response?.statusCode).toBe(401);

expect(response?.json()).toStrictEqual({ message: 'You must be authenticated to access this resource' });
});

test('Generate invitation hash by public id with 200 status, user is creator of the note', async () => {
const userId = 2;
const accessToken = global.auth(userId);

const userNote = notes.find(newNote => {
return newNote.creator_id === userId;
});

const response = await global.api?.fakeRequest({
method: 'PATCH',
headers: {
authorization: `Bearer ${accessToken}`,
},
url: `/note-settings/${userNote!.public_id}/invitation-hash`,
});

expect(response?.statusCode).toBe(200);

expect(response?.json().invitationHash).not.toBe('');

expect(response?.json().invitationHash).toHaveLength(10);
});

test('Returns status 406 when the public id does not exist', async () => {
const nonexistentId = 'ishvm5qH84';

const response = await global.api?.fakeRequest({
method: 'PATCH',
url: `/note-settings/${nonexistentId}/invitation-hash`,
});

expect(response?.statusCode).toBe(406);

expect(response?.json()).toStrictEqual({ message: 'Note not found' });
});

test.each([
{ id: 'mVz3iHuez',
expectedMessage: 'params/notePublicId must NOT have fewer than 10 characters' },

{ id: 'cR8eqF1mFf0',
expectedMessage: 'params/notePublicId must NOT have more than 10 characters' },

{ id: '+=*&*5%&&^&-',
expectedMessage: '\'/note-settings/+=*&*5%&&^&-/invitation-hash\' is not a valid url component' },
])
('Returns 400 when public id of the note has incorrect characters and length', async ({ id, expectedMessage }) => {
const response = await global.api?.fakeRequest({
method: 'PATCH',
url: `/note-settings/${id}/invitation-hash`,
});

expect(response?.statusCode).toBe(400);

expect(response?.json().message).toStrictEqual(expectedMessage);

test.todo('Return 403 when user in team and have Member Role = read');

test.todo('Return 403 when user authorized, but not member of the team');
});
});

describe('PATCH /note-settings/:notePublicId/team', () => {
test('Update team member role by user id and note id, with status code 200', async () => {
// patch member role of existing team member
Expand Down Expand Up @@ -353,3 +429,4 @@ describe('NoteSettings API', () => {
});
});
});

50 changes: 50 additions & 0 deletions src/presentation/http/router/noteSettings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { FastifyPluginCallback } from 'fastify';
import type NoteSettingsService from '@domain/service/noteSettings.js';
import type NoteSettings from '@domain/entities/noteSettings.js';
import type { InvitationHash } from '@domain/entities/noteSettings.js';
import { isEmpty } from '@infrastructure/utils/empty.js';
import useNoteResolver from '../middlewares/note/useNoteResolver.js';
import type NoteService from '@domain/service/note.js';
Expand Down Expand Up @@ -216,6 +217,55 @@ const NoteSettingsRouter: FastifyPluginCallback<NoteSettingsRouterOptions> = (fa
return reply.send(team);
});

/**
* Generates a new invitation hash for a specific note
* TODO add policy for this route (check if user's role is write)
*/
fastify.patch<{
Params: {
notePublicId: NotePublicId;
},
Reply: {
invitationHash: InvitationHash;
},
}>('/:notePublicId/invitation-hash', {
config: {
policy: [
'authRequired',
'userInTeam',
],
},
schema: {
params: {
notePublicId: {
$ref: 'NoteSchema#/properties/id',
},
},
response: {
'2xx': {
type: 'object',
description: 'New invitation hash',
properties: {
invitationHash: {
$ref: 'NoteSettingsSchema#/properties/invitationHash',
},
},
},
},
},
preHandler: [
noteResolver,
],
}, async (request, reply) => {
const noteId = request.note?.id as number;

const updatedNoteSettings = await noteSettingsService.regenerateInvitationHash(noteId);

return reply.send({
invitationHash: updatedNoteSettings.invitationHash,
});
});

done();
};

Expand Down
28 changes: 28 additions & 0 deletions src/presentation/http/schema/NoteSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Note Settings entity used for validation and serialization
*/
export const NoteSettingsSchema = {
$id: 'NoteSettingsSchema',
type: 'object',
properties: {
id: {
type: 'number',
},
noteId: {
type: 'number',
},
customHostname: {
type: 'string',
},
isPublic: {
type: 'boolean',
default: true,
},
invitationHash: {
type: 'string',
pattern: '[a-zA-Z0-9-_]+',
maxLength: 10,
minLength: 10,
},
},
};

0 comments on commit 2318569

Please sign in to comment.