Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 12 additions & 26 deletions e2e/src/api/specs/shared-link.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ describe('/shared-links', () => {
let user1: LoginResponseDto;
let user2: LoginResponseDto;
let album: AlbumResponseDto;
let metadataAlbum: AlbumResponseDto;
let deletedAlbum: AlbumResponseDto;
let linkWithDeletedAlbum: SharedLinkResponseDto;
let linkWithPassword: SharedLinkResponseDto;
Expand All @@ -41,18 +40,9 @@ describe('/shared-links', () => {

[asset1, asset2] = await Promise.all([utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken)]);

[album, deletedAlbum, metadataAlbum] = await Promise.all([
[album, deletedAlbum] = await Promise.all([
createAlbum({ createAlbumDto: { albumName: 'album' } }, { headers: asBearerAuth(user1.accessToken) }),
createAlbum({ createAlbumDto: { albumName: 'deleted album' } }, { headers: asBearerAuth(user2.accessToken) }),
createAlbum(
{
createAlbumDto: {
albumName: 'metadata album',
assetIds: [asset1.id],
},
},
{ headers: asBearerAuth(user1.accessToken) },
),
]);

[linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] =
Expand All @@ -75,14 +65,14 @@ describe('/shared-links', () => {
password: 'foo',
}),
utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: metadataAlbum.id,
type: SharedLinkType.Individual,
assetIds: [asset1.id],
showMetadata: true,
slug: 'metadata-album',
slug: 'metadata-slug',
}),
utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: metadataAlbum.id,
type: SharedLinkType.Individual,
assetIds: [asset1.id],
showMetadata: false,
}),
]);
Expand All @@ -95,9 +85,7 @@ describe('/shared-links', () => {
const resp = await request(shareUrl).get(`/${linkWithMetadata.key}`);
expect(resp.status).toBe(200);
expect(resp.header['content-type']).toContain('text/html');
expect(resp.text).toContain(
`<meta name="description" content="${metadataAlbum.assets.length} shared photos &amp; videos" />`,
);
expect(resp.text).toContain(`<meta name="description" content="1 shared photos &amp; videos" />`);
});

it('should have correct asset count in meta tag for empty album', async () => {
Expand Down Expand Up @@ -144,9 +132,7 @@ describe('/shared-links', () => {
const resp = await request(baseUrl).get(`/s/${linkWithMetadata.slug}`);
expect(resp.status).toBe(200);
expect(resp.header['content-type']).toContain('text/html');
expect(resp.text).toContain(
`<meta name="description" content="${metadataAlbum.assets.length} shared photos &amp; videos" />`,
);
expect(resp.text).toContain(`<meta name="description" content="1 shared photos &amp; videos" />`);
});
});

Expand Down Expand Up @@ -271,20 +257,20 @@ describe('/shared-links', () => {
);
});

it('should return metadata for album shared link', async () => {
it('should return metadata for individual shared link', async () => {
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithMetadata.key });

expect(status).toBe(200);
expect(body.assets).toHaveLength(0);
expect(body.album).toBeDefined();
expect(body.assets).toHaveLength(1);
expect(body.album).not.toBeDefined();
});

it('should not return metadata for album shared link without metadata', async () => {
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithoutMetadata.key });

expect(status).toBe(200);
expect(body.assets).toHaveLength(1);
expect(body.album).toBeDefined();
expect(body.album).not.toBeDefined();

const asset = body.assets[0];
expect(asset).not.toHaveProperty('exifInfo');
Expand Down
37 changes: 10 additions & 27 deletions server/src/dtos/shared-link.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
import _ from 'lodash';
import { SharedLink } from 'src/database';
import { HistoryBuilder, Property } from 'src/decorators';
import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto';
Expand Down Expand Up @@ -118,10 +117,10 @@ export class SharedLinkResponseDto {
slug!: string | null;
}

export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto {
const linkAssets = sharedLink.assets || [];
export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetadata: boolean }): SharedLinkResponseDto {
const assets = sharedLink.assets || [];

return {
const response = {
id: sharedLink.id,
description: sharedLink.description,
password: sharedLink.password,
Expand All @@ -130,35 +129,19 @@ export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto {
type: sharedLink.type,
createdAt: sharedLink.createdAt,
expiresAt: sharedLink.expiresAt,
assets: linkAssets.map((asset) => mapAsset(asset)),
assets: assets.map((asset) => mapAsset(asset, { stripMetadata: options.stripAssetMetadata })),
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
allowUpload: sharedLink.allowUpload,
allowDownload: sharedLink.allowDownload,
showMetadata: sharedLink.showExif,
slug: sharedLink.slug,
};
}

export function mapSharedLinkWithoutMetadata(sharedLink: SharedLink): SharedLinkResponseDto {
const linkAssets = sharedLink.assets || [];
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);

const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
// unless we select sharedLink.album.sharedLinks this will be wrong
if (response.album) {
response.album.hasSharedLink = true;
response.album.shared = true;
}

return {
id: sharedLink.id,
description: sharedLink.description,
password: sharedLink.password,
userId: sharedLink.userId,
key: sharedLink.key.toString('base64url'),
type: sharedLink.type,
createdAt: sharedLink.createdAt,
expiresAt: sharedLink.expiresAt,
assets: assets.map((asset) => mapAsset(asset, { stripMetadata: true })),
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
allowUpload: sharedLink.allowUpload,
allowDownload: sharedLink.allowDownload,
showMetadata: sharedLink.showExif,
slug: sharedLink.slug,
};
return response;
}
3 changes: 2 additions & 1 deletion server/src/services/shared-link.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ describe(SharedLinkService.name, () => {
},
});
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata);
const response = await sut.getMine(authDto, {});
expect(response.assets[0]).toMatchObject({ hasMetadata: false });
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
});

Expand Down
15 changes: 5 additions & 10 deletions server/src/services/shared-link.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import {
mapSharedLink,
mapSharedLinkWithoutMetadata,
SharedLinkCreateDto,
SharedLinkEditDto,
SharedLinkPasswordDto,
Expand All @@ -22,7 +21,7 @@ export class SharedLinkService extends BaseService {
async getAll(auth: AuthDto, { id, albumId }: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {
return this.sharedLinkRepository
.getAll({ userId: auth.user.id, id, albumId })
.then((links) => links.map((link) => mapSharedLink(link)));
.then((links) => links.map((link) => mapSharedLink(link, { stripAssetMetadata: false })));
}

async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> {
Expand All @@ -31,7 +30,7 @@ export class SharedLinkService extends BaseService {
}

const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id);
const response = this.mapToSharedLink(sharedLink, { withExif: sharedLink.showExif });
const response = mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif });
if (sharedLink.password) {
response.token = this.validateAndRefreshToken(sharedLink, dto);
}
Expand All @@ -41,7 +40,7 @@ export class SharedLinkService extends BaseService {

async get(auth: AuthDto, id: string): Promise<SharedLinkResponseDto> {
const sharedLink = await this.findOrFail(auth.user.id, id);
return this.mapToSharedLink(sharedLink, { withExif: true });
return mapSharedLink(sharedLink, { stripAssetMetadata: false });
}

async create(auth: AuthDto, dto: SharedLinkCreateDto): Promise<SharedLinkResponseDto> {
Expand Down Expand Up @@ -81,7 +80,7 @@ export class SharedLinkService extends BaseService {
slug: dto.slug || null,
});

return this.mapToSharedLink(sharedLink, { withExif: true });
return mapSharedLink(sharedLink, { stripAssetMetadata: false });
} catch (error) {
this.handleError(error);
}
Expand All @@ -108,7 +107,7 @@ export class SharedLinkService extends BaseService {
showExif: dto.showMetadata,
slug: dto.slug || null,
});
return this.mapToSharedLink(sharedLink, { withExif: true });
return mapSharedLink(sharedLink, { stripAssetMetadata: false });
} catch (error) {
this.handleError(error);
}
Expand Down Expand Up @@ -214,10 +213,6 @@ export class SharedLinkService extends BaseService {
};
}

private mapToSharedLink(sharedLink: SharedLink, { withExif }: { withExif: boolean }) {
return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink);
}

private validateAndRefreshToken(sharedLink: SharedLink, dto: SharedLinkPasswordDto): string {
const token = this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`);
const sharedLinkTokens = dto.token?.split(',') || [];
Expand Down
Loading
Loading