Skip to content
2 changes: 1 addition & 1 deletion src/application/service/access-sharing/CreateAccessPass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class CreateAccessPass {
throw new AccessPassFailedError(AccessPassFailureReason.SHARING_CODE_EXPIRED);
}

const accessPass = new AccessPass(new UserId(userId), sharingCode.userId);
const accessPass = new AccessPass(new UserId(userId), sharingCode.userId, sharingCode.accessDuration);

return this.accessPassRepository.save(accessPass);
}
Expand Down
3 changes: 2 additions & 1 deletion src/application/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { DokobitAuthenticationProvider } from '../../infrastructure/idAuthentication/DokobitAuthenticationProvider';
import { GetUser } from './users/GetUser';
import { UpdateUser } from './users/UpdateUser';
import { GetAccessibleUsers } from './users/GetAccessibleUsers';
import { GetCountries } from './users/GetCountries';
import { GetTestTypes } from './tests/GetTestTypes';
import { CreateSharingCode } from './access-sharing/CreateSharingCode';
Expand Down Expand Up @@ -95,8 +96,8 @@ export const createMagicLink = new CreateNewMagicLink(
);

export const getUser = new GetUser(userRepository);

export const updateUser = new UpdateUser(userRepository);
export const getAccessibleUsers = new GetAccessibleUsers(userRepository, accessPassRepository);

export const getExistingOrCreateNewUser = new GetExistingOrCreateNewUser(
userRepository,
Expand Down
18 changes: 18 additions & 0 deletions src/application/service/users/GetAccessibleUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { User } from '../../../domain/model/user/User';
import { UserId } from '../../../domain/model/user/UserId';
import { UserRepository } from '../../../domain/model/user/UserRepository';
import { AccessPassRepository } from '../../../domain/model/accessPass/AccessPassRepository';

export class GetAccessibleUsers {
constructor(private userRepository: UserRepository, private accessPassRepository: AccessPassRepository) {}

async byActorId(id: string): Promise<Array<User>> {
const accessPasses = await this.accessPassRepository.findByActorId(new UserId(id));

const userIds = accessPasses
.filter((accessPass) => !accessPass.isExpired())
.map((accessPass) => accessPass.subjectUserId);

return this.userRepository.findByUserIds(userIds);
}
}
21 changes: 21 additions & 0 deletions src/database/migrations/V8__addDurationToAccessPass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import knex from 'knex';

const ACCESS_PASS_TABLE = 'access_pass';

export async function up(db: knex) {
await db.schema.table(ACCESS_PASS_TABLE, (table) => {
table.integer('duration');
});

await db(ACCESS_PASS_TABLE).where({ duration: null }).update({ duration: 60 });

await db.schema.alterTable(ACCESS_PASS_TABLE, (table) => {
table.integer('duration').notNullable().alter();
});
}

export async function down(db: knex) {
await db.schema.alterTable(ACCESS_PASS_TABLE, (table) => {
table.dropColumn('duration');
});
}
10 changes: 7 additions & 3 deletions src/domain/model/accessPass/AccessPass.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('Access Pass', () => {
const actorUserId = new UserId();
const subjectUserId = new UserId();

const accessPass = new AccessPass(actorUserId, subjectUserId);
const accessPass = new AccessPass(actorUserId, subjectUserId, 120);

expect(accessPass.actorUserId).toBe(actorUserId);
expect(accessPass.subjectUserId).toBe(subjectUserId);
Expand All @@ -25,11 +25,15 @@ describe('Access Pass', () => {

MockDate.set('2020-11-03 00:00:00');

const accessPass = new AccessPass(actorUserId, subjectUserId);
const accessPass = new AccessPass(actorUserId, subjectUserId, 120);

// More than default duration
MockDate.set('2020-11-03 01:01:00');

expect(accessPass.isExpired()).toBe(false);

MockDate.set('2020-11-03 01:01:00');
// More than specified duration
MockDate.set('2020-11-03 02:01:00');

expect(accessPass.isExpired()).toBe(true);
});
Expand Down
7 changes: 4 additions & 3 deletions src/domain/model/accessPass/AccessPass.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { v4 as uuidv4 } from 'uuid';
import { UserId } from '../user/UserId';

// 1 hour
const ACCESS_REQUEST_LIFETIME_MSEC = 60 * 60 * 1_000;
const DEFAULT_DURATION_MINUTES = 60;

export class AccessPass {
constructor(
readonly actorUserId: UserId,
readonly subjectUserId: UserId,
readonly duration: number = DEFAULT_DURATION_MINUTES,
readonly id: string = uuidv4(),
readonly creationTime: Date = new Date()
) {}
Expand All @@ -17,6 +17,7 @@ export class AccessPass {
}

public expirationTime(): Date {
return new Date(this.creationTime.getTime() + ACCESS_REQUEST_LIFETIME_MSEC);
const millisecondDuration = this.duration * 60 * 1_000;
return new Date(this.creationTime.getTime() + millisecondDuration);
}
}
2 changes: 2 additions & 0 deletions src/domain/model/accessPass/AccessPassRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ export interface AccessPassRepository {
save(accessPass: AccessPass): Promise<AccessPass>;

findByUserIds(actorUserId: UserId, subjectUserId: UserId): Promise<AccessPass | null>;

findByActorId(actorUserId: UserId): Promise<Array<AccessPass>>;
}
1 change: 1 addition & 0 deletions src/domain/model/user/UserRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface UserRepository {
save(user: User): Promise<User>;

findByUserId(userId: UserId): Promise<User | null>;
findByUserIds(userIds: Array<UserId>): Promise<Array<User>>;

findByAuthenticationDetails(authenticationDetails: AuthenticationDetails): Promise<User | null>;
}
Expand Down
38 changes: 32 additions & 6 deletions src/infrastructure/persistence/PsqlAccessPassRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ import { AccessPass } from '../../domain/model/accessPass/AccessPass';
import { UserId } from '../../domain/model/user/UserId';
import { v4 as uuidv4 } from 'uuid';
import { cleanupDatabase } from '../../test/cleanupDatabase';
import MockDate from 'mockdate';

describe('PsqlAccessPassRepository', () => {
const psqlAccessPassRepository = new PsqlAccessPassRepository(database);

beforeEach(async () => {
await cleanupDatabase();
MockDate.reset();
});

it('inserts new and retrieves a access pass', async () => {
const actorUserId = new UserId();
const subjectUserId = new UserId();

const accessPass = new AccessPass(actorUserId, subjectUserId);
const accessPass = new AccessPass(actorUserId, subjectUserId, 120);
await psqlAccessPassRepository.save(accessPass);

const persistedAccessPass = await psqlAccessPassRepository.findByUserIds(actorUserId, subjectUserId);
Expand All @@ -28,11 +30,9 @@ describe('PsqlAccessPassRepository', () => {
const actorUserId = new UserId();
const subjectUserId = new UserId();

const firstPass = new AccessPass(actorUserId, subjectUserId, uuidv4(), new Date('2020-01-01'));

const secondPass = new AccessPass(actorUserId, subjectUserId, uuidv4(), new Date('2020-01-02'));

const thirdPass = new AccessPass(actorUserId, subjectUserId, uuidv4(), new Date('2020-01-03'));
const firstPass = new AccessPass(actorUserId, subjectUserId, 60, uuidv4(), new Date('2020-01-01'));
const secondPass = new AccessPass(actorUserId, subjectUserId, 120, uuidv4(), new Date('2020-01-02'));
const thirdPass = new AccessPass(actorUserId, subjectUserId, 180, uuidv4(), new Date('2020-01-03'));

await psqlAccessPassRepository.save(firstPass);
await psqlAccessPassRepository.save(thirdPass);
Expand All @@ -42,6 +42,32 @@ describe('PsqlAccessPassRepository', () => {

expect(persistedAccessPass).toEqual(thirdPass);
});

it('retrieves all access passes for the actor with latet first', async () => {
const actorUserId = new UserId();

MockDate.set('2020-01-01T00:00:00Z');
const firstPass = new AccessPass(actorUserId, new UserId(), 60);

MockDate.set('2020-01-01T00:00:01Z');
const secondPass = new AccessPass(actorUserId, new UserId(), 120);

MockDate.set('2020-01-01T00:00:02Z');
const thirdPass = new AccessPass(actorUserId, new UserId(), 180);

await psqlAccessPassRepository.save(firstPass);
await psqlAccessPassRepository.save(thirdPass);
await psqlAccessPassRepository.save(secondPass);

MockDate.set('2020-01-01T01:01:00Z');

const accessPasses = await psqlAccessPassRepository.findByActorId(actorUserId);

expect(accessPasses.length).toBe(3);
expect(accessPasses[0]).toEqual(thirdPass);
expect(accessPasses[1]).toEqual(secondPass);
expect(accessPasses[2]).toEqual(firstPass);
});
});

afterAll(() => {
Expand Down
53 changes: 39 additions & 14 deletions src/infrastructure/persistence/PsqlAccessPassRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import { UserId } from '../../domain/model/user/UserId';

const ACCESS_PASS_TABLE_NAME = 'access_pass';

const COLUMNS = [
'id',
'actor_user_id as actorUserId',
'subject_user_id as subjectUserId',
'duration',
'creation_time as creationTime',
];

export class PsqlAccessPassRepository implements AccessPassRepository {
constructor(private db: knex) {}

async findByUserIds(actorUserId: UserId, subjectUserId: UserId) {
const linkRow: any = await this.db(ACCESS_PASS_TABLE_NAME)
.select([
'id',
'actor_user_id as actorUserId',
'subject_user_id as subjectUserId',
'creation_time as creationTime',
])
.select(COLUMNS)
.where({
actor_user_id: actorUserId.value,
subject_user_id: subjectUserId.value,
Expand All @@ -27,25 +30,37 @@ export class PsqlAccessPassRepository implements AccessPassRepository {
return null;
}

return new AccessPass(
new UserId(linkRow.actorUserId),
new UserId(linkRow.subjectUserId),
linkRow.id,
linkRow.creationTime
);
return convertRowToAccessPass(linkRow);
}

async findByActorId(actorUserId: UserId) {
// TODO group by userId to get latest, but also get latest duration, so can't just use MAX
const rows: any = await this.db(ACCESS_PASS_TABLE_NAME)
.select(COLUMNS)
.where({
actor_user_id: actorUserId.value,
})
.orderBy('creation_time', 'desc');

if (!rows) {
return [];
}

return rows.map(convertRowToAccessPass);
}

async save(accessPass: AccessPass) {
return await this.db
.raw(
`
insert into "${ACCESS_PASS_TABLE_NAME}" (id, actor_user_id, subject_user_id, creation_time)
values (:id, :actor_user_id, :subject_user_id, :creation_time)
insert into "${ACCESS_PASS_TABLE_NAME}" (id, actor_user_id, subject_user_id, duration, creation_time)
values (:id, :actor_user_id, :subject_user_id, :duration, :creation_time)
`,
{
id: accessPass.id,
actor_user_id: accessPass.actorUserId.value,
subject_user_id: accessPass.subjectUserId.value,
duration: accessPass.duration,
creation_time: accessPass.creationTime,
}
)
Expand All @@ -54,3 +69,13 @@ export class PsqlAccessPassRepository implements AccessPassRepository {
});
}
}

function convertRowToAccessPass(linkRow: any) {
return new AccessPass(
new UserId(linkRow.actorUserId),
new UserId(linkRow.subjectUserId),
linkRow.duration,
linkRow.id,
linkRow.creationTime
);
}
34 changes: 22 additions & 12 deletions src/infrastructure/persistence/PsqlUserRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ export class PsqlUserRepository implements UserRepository {
return this.extractUserAndPopulateRoles(userRow);
}

async findByUserIds(userIds: Array<UserId>): Promise<Array<User>> {
const idValues = userIds.map((userId) => userId.value);
const rows: any = await this.db(USER_TABLE_NAME).whereIn('id', idValues).select(USER_TABLE_COLUMNS);
return rows.map(mapRowToUser);
}

private async saveRoleAssignment(roleAssignment: RoleAssignmentAction, context = this.db) {
await context('role_to_user_assignment').insert({
id: roleAssignment.id.value,
Expand All @@ -110,18 +116,7 @@ export class PsqlUserRepository implements UserRepository {
}

private async extractUserAndPopulateRoles(userRow: any) {
const user = new User(
new UserId(userRow.id),
new AuthenticationDetails(
new AuthenticationMethod(userRow.authenticationMethod),
new AuthenticationIdentifier(userRow.authenticationIdentifier)
),
userRow.email ? new Email(userRow.email) : undefined,
fromDbProfile(userRow.profile as DbProfile | undefined),
fromDbAddress(userRow.address as DbAddress | undefined),
userRow.creationTime,
userRow.modificationTime
);
const user = mapRowToUser(userRow);
const roleAssignmentActions = await this.getRoleAssignments(user);
Reflect.set(user.roleAssignments, 'assignmentActions', roleAssignmentActions);
Reflect.set(user.roleAssignments, 'newAssignmentActions', []);
Expand Down Expand Up @@ -188,6 +183,21 @@ export class PsqlUserRepository implements UserRepository {
}
}

function mapRowToUser(userRow: any): User {
return new User(
new UserId(userRow.id),
new AuthenticationDetails(
new AuthenticationMethod(userRow.authenticationMethod),
new AuthenticationIdentifier(userRow.authenticationIdentifier)
),
userRow.email ? new Email(userRow.email) : undefined,
fromDbProfile(userRow.profile as DbProfile | undefined),
fromDbAddress(userRow.address as DbAddress | undefined),
userRow.creationTime,
userRow.modificationTime
);
}

function toDbProfile(profile?: Profile): DbProfile | undefined {
return profile
? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getTokenForUser } from '../../../test/authentication';
import { v4 as uuidv4 } from 'uuid';
import { aUserWithAllInformation } from '../../../test/domainFactories';
import { RootController } from '../RootController';
import MockDate from 'mockdate';

describe('sharing code endpoints', () => {
const app = new RootController().expressApp();
Expand Down Expand Up @@ -68,9 +69,11 @@ describe('sharing code endpoints', () => {
await userRepository.save(user1);
await userRepository.save(user2);

const sharingCode = new SharingCode(user2.id, 60, uuidv4(), new Date());
const sharingCode = new SharingCode(user2.id, 120, uuidv4(), new Date());
await sharingCodeRepository.save(sharingCode);

MockDate.set('2020-05-04T00:00:00Z');

await request(app)
.post(`/api/v1/users/${user1.id.value}/access-passes`)
.send({ code: sharingCode.code })
Expand All @@ -79,7 +82,7 @@ describe('sharing code endpoints', () => {
.expect((response) => {
const accessPass = response.body;
expect(accessPass.userId).toBe(user2.id.value);
expect(accessPass.expiryTime).toBeDefined();
expect(accessPass.expiryTime).toBe('2020-05-04T02:00:00.000Z');
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/presentation/api/tests/TestController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe('TestController', () => {
const subjectUser = await userRepository.save(aNewUser());
await testRepository.save(aTest(subjectUser.id));

const accessPass = new AccessPass(actorUser.id, subjectUser.id, uuidv4(), new Date('1970-01-01'));
const accessPass = new AccessPass(actorUser.id, subjectUser.id, 60, uuidv4(), new Date('1970-01-01'));

await accessPassRepository.save(accessPass);

Expand Down
Loading