Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-7124 - Add delete sessions endpoint #5001

Merged
merged 36 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9ba0f25
Add delete sessions endpoint
bischofmax May 13, 2024
13126b3
Merge branch 'main' into BC-7124-remove-etherpad-cookie
bischofmax May 13, 2024
7809654
Remove duplicate implementation of deleteSession and filter only vali…
bischofmax May 14, 2024
0e08239
Add api test
bischofmax May 14, 2024
cdcde50
Add filter case for list sessions of author
bischofmax May 14, 2024
6af94cb
Adjust more tests
bischofmax May 14, 2024
630331a
Add and fix service test
bischofmax May 14, 2024
e320673
Merge branch 'main' into BC-7124-remove-etherpad-cookie
bischofmax May 14, 2024
bbca049
Remove response decoratos and fix userid typing
bischofmax May 14, 2024
3e03e59
Merge branch 'main' into BC-7124-remove-etherpad-cookie
bischofmax May 15, 2024
fa8f566
Convert expiresMillisconds to seconds
bischofmax May 15, 2024
723cd5a
Add check if session duration is sufficient
bischofmax May 16, 2024
56f5fdb
Move duration threshold to service
bischofmax May 16, 2024
5b26722
BC-7124 - move interface
SevenWaysDP May 16, 2024
45a4b0a
Adjust adapter tests
bischofmax May 16, 2024
e46b390
BC-7124 - refactor EtherpadClientAdapter to use EtherpadResponseMappe…
SevenWaysDP May 16, 2024
07800c7
Add test for mapEtherpadSessionToSession
bischofmax May 16, 2024
ad08ee9
Merge branch 'main' into BC-7124-remove-etherpad-cookie
bischofmax May 16, 2024
fef4cf7
Fix cookie path
bischofmax May 17, 2024
9abd101
Move mapEtherpadSessionsToSessions to mapper
bischofmax May 17, 2024
c30cf91
Fixx error handling
bischofmax May 17, 2024
047b9b1
Add console logs
bischofmax May 17, 2024
fa16002
Add more logs
bischofmax May 17, 2024
6d27ca0
Fix scenario when no session for user exists
bischofmax May 17, 2024
0f48742
Fix adapter test
bischofmax May 17, 2024
32894fb
Fix service test
bischofmax May 17, 2024
44c172e
Add more untit tests
bischofmax May 17, 2024
fa470ea
Merge branch 'main' into BC-7124-remove-etherpad-cookie
bischofmax May 17, 2024
62c677f
Fix null response in legacy etherpad client
bischofmax May 17, 2024
7bbe2da
Fix naming of unix timestamp
bischofmax May 17, 2024
93f060d
Remove typing from client in CollaborativeTextEditor
bischofmax May 17, 2024
295d7fc
Merge branch 'BC-7124-remove-etherpad-cookie' of github.com:hpi-schul…
bischofmax May 17, 2024
017ab2d
Merge branch 'main' into BC-7124-remove-etherpad-cookie
bischofmax May 17, 2024
c3f2c95
Fix null on correct response data object
bischofmax May 17, 2024
488975f
Merge branch 'main' into BC-7124-remove-etherpad-cookie
CeEv May 17, 2024
2ac8355
Merge branch 'main' into BC-7124-remove-etherpad-cookie
CeEv May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,40 @@ describe(EtherpadClientAdapter.name, () => {
return { userId, username };
};

it('should return author id', async () => {
const { userId, username } = setup();
describe('When user name parameter is set', () => {
it('should return author id', async () => {
const { userId, username } = setup();

const result = await service.getOrCreateAuthorId(userId, username);

expect(result).toBe('authorId');
});

it('should call createAuthorIfNotExistsForUsingGET with correct params', async () => {
const { userId, username } = setup();

const result = await service.getOrCreateAuthorId(userId, username);
await service.getOrCreateAuthorId(userId, username);

expect(result).toBe('authorId');
expect(authorApi.createAuthorIfNotExistsForUsingGET).toBeCalledWith(userId, username);
});
});

it('should call createAuthorIfNotExistsForUsingGET with correct params', async () => {
const { userId, username } = setup();
describe('When user name parameter is not set', () => {
it('should return author id', async () => {
const { userId } = setup();

const result = await service.getOrCreateAuthorId(userId);

expect(result).toBe('authorId');
});

it('should call createAuthorIfNotExistsForUsingGET with correct params', async () => {
const { userId } = setup();

await service.getOrCreateAuthorId(userId, username);
await service.getOrCreateAuthorId(userId);

expect(authorApi.createAuthorIfNotExistsForUsingGET).toBeCalledWith(userId, username);
expect(authorApi.createAuthorIfNotExistsForUsingGET).toBeCalledWith(userId, undefined);
});
});
});

Expand Down Expand Up @@ -310,7 +330,7 @@ describe(EtherpadClientAdapter.name, () => {
data: {
code: EtherpadResponseCode.OK,
// @ts-expect-error wrong type mapping
data: { 'session-id-1': { groupID: 'groupId', authorID: authorId } },
data: { 'session-id-1': { groupID: 'groupId', authorID: authorId }, 'session-id-2': null },
},
});

Expand Down
17 changes: 9 additions & 8 deletions apps/server/src/infra/etherpad-client/etherpad-client.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class EtherpadClientAdapter {
private readonly padApi: PadApi
) {}

public async getOrCreateAuthorId(userId: EntityId, username: string): Promise<AuthorId> {
public async getOrCreateAuthorId(userId: EntityId, username?: string): Promise<AuthorId> {
const response = await this.tryCreateAuthor(userId, username);
const user = this.handleEtherpadResponse<InlineResponse2003>(response, { userId });

Expand All @@ -42,7 +42,7 @@ export class EtherpadClientAdapter {
return authorId;
}

private async tryCreateAuthor(userId: string, username: string): Promise<AxiosResponse<InlineResponse2003>> {
private async tryCreateAuthor(userId: string, username?: string): Promise<AxiosResponse<InlineResponse2003>> {
try {
const response = await this.authorApi.createAuthorIfNotExistsForUsingGET(userId, username);

Expand Down Expand Up @@ -149,9 +149,12 @@ export class EtherpadClientAdapter {
throw new InternalServerErrorException('Etherpad session ids response is not an object');
}

const sessionIds = Object.keys(sessions);
const validSessionIds = Object.entries(sessions)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.filter(([key, value]) => value !== null)
.map(([key]) => key);

return sessionIds;
return validSessionIds;
}

private isObject(value: any): value is object {
Expand Down Expand Up @@ -269,11 +272,9 @@ export class EtherpadClientAdapter {
}
}

public async deleteSession(sessionId: SessionId): Promise<InlineResponse2001 | undefined> {
public async deleteSession(sessionId: SessionId): Promise<void> {
const response = await this.tryDeleteSession(sessionId);
const responseData = this.handleEtherpadResponse<InlineResponse2001>(response, { sessionId });

return responseData;
this.handleEtherpadResponse<InlineResponse2001>(response, { sessionId });
}

private async tryDeleteSession(sessionId: SessionId): Promise<AxiosResponse<InlineResponse2001>> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication';
import { Controller, ForbiddenException, Get, NotFoundException, Param, Res } from '@nestjs/common';
import { Controller, Delete, ForbiddenException, Get, NotFoundException, Param, Res } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ApiValidationError } from '@shared/common';
import { Response } from 'express';
Expand Down Expand Up @@ -30,13 +30,24 @@
getCollaborativeTextEditorForParentParams
);

res.cookie('sessionID', textEditor.sessions.toString(), {
res.cookie('sessionID', textEditor.sessionId, {
expires: textEditor.sessionExpiryDate,
secure: true,
path: textEditor.path,
});

Check warning

Code scanning / CodeQL

Sensitive server cookie exposed to the client Medium

Sensitive server cookie is missing 'httpOnly' flag.

const dto = CollaborativeTextEditorMapper.mapCollaborativeTextEditorToResponse(textEditor);

return dto;
}

@ApiOperation({ summary: 'Delete all etherpad sessions for user' })
@ApiResponse({ status: 200, type: CollaborativeTextEditorResponse })
bischofmax marked this conversation as resolved.
Show resolved Hide resolved
@ApiResponse({ status: 400, type: ApiValidationError })
bischofmax marked this conversation as resolved.
Show resolved Hide resolved
@ApiResponse({ status: 403, type: ForbiddenException })
@ApiResponse({ status: 404, type: NotFoundException })
bischofmax marked this conversation as resolved.
Show resolved Hide resolved
@Delete('/delete-sessions')
async deleteSessionsByUser(@CurrentUser() currentUser: ICurrentUser): Promise<void> {
await this.collaborativeTextEditorUc.deleteSessionsByUser(currentUser.userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export class CollaborativeTextEditorUc {
return textEditor;
}

async deleteSessionsByUser(userId: string): Promise<void> {
bischofmax marked this conversation as resolved.
Show resolved Hide resolved
const user = await this.authorizationService.getUserWithPermissions(userId);

await this.collaborativeTextEditorService.deleteSessionsByUser(user.id);
}

private async authorizeByParentType(params: GetCollaborativeTextEditorForParentParams, user: User) {
if (params.parentType === CollaborativeTextEditorParentType.BOARD_CONTENT_ELEMENT) {
await this.authorizeForContentElement(params, user);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { EntityManager } from '@mikro-orm/mongodb';
import { HttpStatus, INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { cleanupCollections, TestApiClient, UserAndAccountTestFactory } from '@shared/testing';
import { EtherpadClientAdapter } from '@src/infra/etherpad-client';
import { ServerTestModule } from '@src/modules/server';

describe('Collaborative Text Editor Controller (API)', () => {
let app: INestApplication;
let em: EntityManager;
let testApiClient: TestApiClient;
let etherpadClientAdapter: DeepMocked<EtherpadClientAdapter>;

beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [ServerTestModule],
})
.overrideProvider(EtherpadClientAdapter)
.useValue(createMock<EtherpadClientAdapter>())
.compile();

app = module.createNestApplication();
await app.init();
em = app.get(EntityManager);
testApiClient = new TestApiClient(app, 'collaborative-text-editor');
etherpadClientAdapter = module.get(EtherpadClientAdapter);
});

beforeEach(async () => {
await cleanupCollections(em);
});

afterAll(async () => {
await app.close();
});

describe('delete sessions by user', () => {
describe('when no user is logged in', () => {
it('should return 401', async () => {
const response = await testApiClient.delete(`delete-sessions`);

expect(response.status).toEqual(HttpStatus.UNAUTHORIZED);
});
});

describe('when user is logged in', () => {
const setup = async () => {
const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent();

await em.persistAndFlush([studentAccount, studentUser]);
em.clear();

const loggedInClient = await testApiClient.login(studentAccount);

const authorId = 'authorId';
etherpadClientAdapter.getOrCreateAuthorId.mockResolvedValueOnce(authorId);

const otherSessionIds = ['otherSessionId1', 'otherSessionId2'];
etherpadClientAdapter.listSessionIdsOfAuthor.mockResolvedValueOnce(otherSessionIds);

etherpadClientAdapter.deleteSession.mockResolvedValueOnce();
etherpadClientAdapter.deleteSession.mockResolvedValueOnce();

return {
loggedInClient,
};
};

it('should resolve successfully', async () => {
const { loggedInClient } = await setup();

await loggedInClient.delete(`delete-sessions`);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,6 @@ describe('Collaborative Text Editor Controller (API)', () => {
const basePath = Configuration.get('ETHERPAD__PAD_URI') as string;
const expectedPath = `${basePath}/${editorId}`;

const expectedSessions = encodeURIComponent([...otherSessionIds, sessionId].join(','));

const cookieExpiresMilliseconds = Number(Configuration.get('ETHERPAD_COOKIE__EXPIRES_SECONDS')) * 1000;
// Remove the last 8 characters from the string to prevent conflict between time of test and code execution
const sessionCookieExpiryDate = new Date(Date.now() + cookieExpiresMilliseconds).toUTCString().slice(0, -8);
Expand All @@ -149,8 +147,9 @@ describe('Collaborative Text Editor Controller (API)', () => {
loggedInClient,
collaborativeTextEditorElement,
expectedPath,
expectedSessions,
sessionId,
sessionCookieExpiryDate,
editorId,
};
};

Expand All @@ -159,8 +158,9 @@ describe('Collaborative Text Editor Controller (API)', () => {
loggedInClient,
collaborativeTextEditorElement,
expectedPath,
expectedSessions,
sessionId,
sessionCookieExpiryDate,
editorId,
} = await setup();

const response = await loggedInClient.get(`content-element/${collaborativeTextEditorElement.id}`);
Expand All @@ -170,7 +170,7 @@ describe('Collaborative Text Editor Controller (API)', () => {
expect(response.body['url']).toEqual(expectedPath);
// eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/no-unsafe-member-access
expect(response.headers['set-cookie'][0]).toContain(
`sessionID=${expectedSessions}; Path=/; Expires=${sessionCookieExpiryDate}`
`sessionID=${sessionId}; Path=/p/${editorId}; Expires=${sessionCookieExpiryDate}`
);
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { SessionId } from '@src/infra/etherpad-client/interface';
bischofmax marked this conversation as resolved.
Show resolved Hide resolved

export interface CollaborativeTextEditor {
url: string;
sessions: string[];
path: string;
sessionId: SessionId;
sessionExpiryDate: Date;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,13 @@ export class CollaborativeTextEditorService {
parentId,
sessionExpiryDate
);
const authorsSessionIds = await this.collaborativeTextEditorAdapter.listSessionIdsOfAuthor(authorId);

const url = this.buildPath(padId);
const uniqueSessionIds = this.removeDuplicateSessions([...authorsSessionIds, sessionId]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we delete the “removeDuplicateSessions” method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was already removed


return {
sessions: uniqueSessionIds,
sessionId,
url,
path: `/p/${padId}`,
sessionExpiryDate,
};
}
Expand All @@ -51,10 +50,13 @@ export class CollaborativeTextEditorService {
await this.collaborativeTextEditorAdapter.deleteGroup(groupId);
}

private removeDuplicateSessions(sessions: string[]): string[] {
const uniqueSessions = [...new Set(sessions)];
async deleteSessionsByUser(userId: string): Promise<void> {
const authorId = await this.collaborativeTextEditorAdapter.getOrCreateAuthorId(userId);
const sessionIds = await this.collaborativeTextEditorAdapter.listSessionIdsOfAuthor(authorId);

return uniqueSessions;
const promises = sessionIds.map((sessionId) => this.collaborativeTextEditorAdapter.deleteSession(sessionId));

await Promise.all(promises);
}

private buildSessionExpiryDate(): Date {
Expand Down
Loading