Merge pull request #388 from ente-io/showErrForPopularAlbums

Show err for popular albums
This commit is contained in:
Abhinav Kumar 2022-02-25 16:01:57 +05:30 committed by GitHub
commit 3400d13058
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 238 additions and 28 deletions

View file

@ -28,6 +28,8 @@ module.exports = {
'base-uri ': "'self'",
'frame-ancestors': " 'none'",
'form-action': "'none'",
'report-uri': ' https://csp-reporter.ente.io',
'report-to': ' https://csp-reporter.ente.io',
},
WORKBOX_CONFIG: {
@ -38,10 +40,9 @@ module.exports = {
ALL_ROUTES: '/(.*)',
buildCSPHeader: (directives) => ({
'Content-Security-Policy': Object.entries(directives).reduce(
(acc, [key, value]) => acc + `${key} ${value};`,
''
),
'Content-Security-Policy-Report-Only': Object.entries(
directives
).reduce((acc, [key, value]) => acc + `${key} ${value};`, ''),
}),
convertToNextHeaderFormat: (headers) =>

View file

@ -8,5 +8,5 @@
X-Frame-Options: deny
X-XSS-Protection: 1; mode=block
Referrer-Policy: same-origin
Content-Security-Policy-Report-Only: default-src 'none'; img-src 'self' blob:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src': 'self'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;
Content-Security-Policy-Report-Only: default-src 'none'; img-src 'self' blob:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;

View file

@ -29,6 +29,11 @@ Sentry.init({
event.request.url = currentURL;
return event;
},
integrations: function (i) {
return i.filter(function (i) {
return i.name !== 'Breadcrumbs';
});
},
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so

View file

@ -410,7 +410,8 @@ const PhotoFrame = ({
url =
await PublicCollectionDownloadManager.getThumbnail(
item,
publicCollectionGalleryContext.token
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken
);
} else {
url = await DownloadManager.getThumbnail(item);
@ -447,6 +448,7 @@ const PhotoFrame = ({
url = await PublicCollectionDownloadManager.getFile(
item,
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken,
true
);
} else {

View file

@ -705,7 +705,8 @@ function PhotoSwipe(props: Iprops) {
await downloadFile(
file,
publicCollectionGalleryContext.accessedThroughSharedURL,
publicCollectionGalleryContext.token
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken
);
galleryContext.finishLoading();

View file

@ -194,7 +194,8 @@ export default function PreviewCard(props: IProps) {
url =
await PublicCollectionDownloadManager.getThumbnail(
file,
publicCollectionGalleryContext.token
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken
);
} else {
url = await DownloadManager.getThumbnail(file);

View file

@ -67,6 +67,7 @@ export default function Credentials() {
keyAttributes.keyDecryptionNonce,
kek
);
if (isFirstLogin()) {
await generateAndSaveIntermediateKeyAttributes(
passphrase,

View file

@ -3,11 +3,14 @@ import PhotoFrame from 'components/PhotoFrame';
import React, { useContext, useEffect, useRef, useState } from 'react';
import {
getLocalPublicCollection,
getLocalPublicCollectionPassword,
getLocalPublicFiles,
getPublicCollection,
getPublicCollectionUID,
removePublicCollectionWithFiles,
savePublicCollectionPassword,
syncPublicFiles,
verifyPublicCollectionPassword,
} from 'services/publicCollectionService';
import { Collection } from 'types/collection';
import { EnteFile } from 'types/file';
@ -28,6 +31,10 @@ import LoadingBar from 'react-top-loading-bar';
import CryptoWorker from 'utils/crypto';
import { PAGES } from 'constants/pages';
import { useRouter } from 'next/router';
import LogoImg from 'components/LogoImg';
import SingleInputForm from 'components/SingleInputForm';
import { Card } from 'react-bootstrap';
import { logError } from 'utils/sentry';
const Loader = () => (
<Container>
@ -39,6 +46,8 @@ const Loader = () => (
const bs58 = require('bs58');
export default function PublicCollectionGallery() {
const token = useRef<string>(null);
// passwordJWTToken refers to the jwt token which is used for album protected by password.
const [passwordJWTToken, setPasswordJWTToken] = useState<string>(null);
const collectionKey = useRef<string>(null);
const url = useRef<string>(null);
const [publicFiles, setPublicFiles] = useState<EnteFile[]>(null);
@ -54,6 +63,8 @@ export default function PublicCollectionGallery() {
const loadingBar = useRef(null);
const isLoadingBarRunning = useRef(false);
const router = useRouter();
const [isPasswordProtected, setIsPasswordProtected] =
useState<boolean>(false);
const openMessageDialog = () => setMessageDialogView(true);
const closeMessageDialog = () => setMessageDialogView(false);
@ -114,7 +125,11 @@ export default function PublicCollectionGallery() {
const localPublicFiles = sortFiles(
mergeMetadata(localFiles)
);
const localPasswordJWTToken =
await getLocalPublicCollectionPassword(collectionUID);
setPublicFiles(localPublicFiles);
setPasswordJWTToken(localPasswordJWTToken);
}
await syncWithRemote();
} finally {
@ -134,13 +149,31 @@ export default function PublicCollectionGallery() {
collectionKey.current
);
setPublicCollection(collection);
await syncPublicFiles(token.current, collection, setPublicFiles);
setErrorMessage(null);
// check if we need to prompt user for the password
if (
(collection?.publicURLs?.[0]?.passwordEnabled ?? false) &&
!passwordJWTToken
) {
setIsPasswordProtected(true);
} else {
await syncPublicFiles(
token.current,
collection,
setPublicFiles
);
}
} catch (e) {
const parsedError = parseSharingErrorCodes(e);
if (parsedError.message === CustomError.TOKEN_EXPIRED) {
setErrorMessage(constants.LINK_EXPIRED);
if (
parsedError.message === CustomError.TOKEN_EXPIRED ||
parsedError.message === CustomError.TOO_MANY_REQUESTS
) {
setErrorMessage(
parsedError.message === CustomError.TOO_MANY_REQUESTS
? constants.LINK_TOO_MANY_REQUESTS
: constants.LINK_EXPIRED
);
// share has been disabled
// local cache should be cleared
removePublicCollectionWithFiles(
@ -155,6 +188,54 @@ export default function PublicCollectionGallery() {
}
};
const verifyLinkPassword = async (password, setFieldError) => {
try {
const cryptoWorker = await new CryptoWorker();
let hashedPassword: string = null;
try {
const publicUrl = publicCollection.publicURLs[0];
hashedPassword = await cryptoWorker.deriveKey(
password,
publicUrl.nonce,
publicUrl.opsLimit,
publicUrl.memLimit
);
} catch (e) {
logError(e, 'failed to derive key for verifyLinkPassword');
setFieldError(
'passphrase',
`${constants.UNKNOWN_ERROR} ${e.message}`
);
return;
}
const collectionUID = getPublicCollectionUID(token.current);
try {
const jwtToken = await verifyPublicCollectionPassword(
token.current,
hashedPassword
);
setPasswordJWTToken(jwtToken);
savePublicCollectionPassword(collectionUID, jwtToken);
} catch (e) {
// reset local password token
logError(e, 'failed to validate password for album');
const parsedError = parseSharingErrorCodes(e);
if (parsedError.message === CustomError.TOKEN_EXPIRED) {
setFieldError('passphrase', constants.INCORRECT_PASSPHRASE);
return;
}
throw e;
}
await syncWithRemote();
finishLoadingBar();
} catch (e) {
setFieldError(
'passphrase',
`${constants.UNKNOWN_ERROR} ${e.message}`
);
}
};
if (!publicFiles && loading) {
return <Loader />;
}
@ -162,6 +243,30 @@ export default function PublicCollectionGallery() {
if (errorMessage && !loading) {
return <Container>{errorMessage}</Container>;
}
if (isPasswordProtected && !passwordJWTToken && !loading) {
return (
<Container>
<Card style={{ width: '332px' }} className="text-center">
<Card.Body style={{ padding: '40px 30px' }}>
<Card.Title style={{ marginBottom: '24px' }}>
<LogoImg src="/icon.svg" />
{constants.PASSWORD}
</Card.Title>
<Card.Subtitle style={{ marginBottom: '2rem' }}>
{/* <LogoImg src="/icon.svg" /> */}
{constants.LINK_PASSWORD}
</Card.Subtitle>
<SingleInputForm
callback={verifyLinkPassword}
placeholder={constants.RETURN_PASSPHRASE_HINT}
buttonText={'unlock'}
fieldType="password"
/>
</Card.Body>
</Card>
</Container>
);
}
if (!publicFiles && !loading) {
return <Container>{constants.NOT_FOUND}</Container>;
@ -172,6 +277,7 @@ export default function PublicCollectionGallery() {
value={{
...defaultPublicCollectionGalleryContext,
token: token.current,
passwordToken: passwordJWTToken,
accessedThroughSharedURL: true,
setDialogMessage,
openReportForm,

View file

@ -19,7 +19,11 @@ class PublicCollectionDownloadManager {
private fileObjectURLPromise = new Map<string, Promise<string>>();
private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
public async getThumbnail(file: EnteFile, token: string) {
public async getThumbnail(
file: EnteFile,
token: string,
passwordToken: string
) {
try {
if (!token) {
return null;
@ -42,7 +46,11 @@ class PublicCollectionDownloadManager {
if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob());
}
const thumb = await this.downloadThumb(token, file);
const thumb = await this.downloadThumb(
token,
passwordToken,
file
);
const thumbBlob = new Blob([thumb]);
try {
await thumbnailCache?.put(
@ -65,11 +73,18 @@ class PublicCollectionDownloadManager {
}
}
private downloadThumb = async (token: string, file: EnteFile) => {
private downloadThumb = async (
token: string,
passwordToken: string,
file: EnteFile
) => {
const resp = await HTTPService.get(
getPublicCollectionThumbnailURL(file.id),
null,
{ 'X-Auth-Access-Token': token },
{
'X-Auth-Access-Token': token,
'X-Auth-Access-Token-JWT': passwordToken,
},
{ responseType: 'arraybuffer' }
);
if (typeof resp.data === 'undefined') {
@ -84,14 +99,23 @@ class PublicCollectionDownloadManager {
return decrypted;
};
getFile = async (file: EnteFile, token: string, forPreview = false) => {
getFile = async (
file: EnteFile,
token: string,
passwordToken: string,
forPreview = false
) => {
const shouldBeConverted = forPreview && needsConversionForPreview(file);
const fileKey = shouldBeConverted
? `${file.id}_converted`
: `${file.id}`;
try {
const getFilePromise = async (convert: boolean) => {
const fileStream = await this.downloadFile(token, file);
const fileStream = await this.downloadFile(
token,
passwordToken,
file
);
let fileBlob = await new Response(fileStream).blob();
if (convert) {
fileBlob = await convertForPreview(file, fileBlob);
@ -117,7 +141,7 @@ class PublicCollectionDownloadManager {
return await this.fileObjectURLPromise.get(file.id.toString());
}
async downloadFile(token: string, file: EnteFile) {
async downloadFile(token: string, passwordToken: string, file: EnteFile) {
const worker = await new CryptoWorker();
if (!token) {
return null;
@ -129,7 +153,10 @@ class PublicCollectionDownloadManager {
const resp = await HTTPService.get(
getPublicCollectionFileURL(file.id),
null,
{ 'X-Auth-Access-Token': token },
{
'X-Auth-Access-Token': token,
'X-Auth-Access-Token-JWT': passwordToken,
},
{ responseType: 'arraybuffer' }
);
if (typeof resp.data === 'undefined') {

View file

@ -12,6 +12,7 @@ import {
} from 'types/publicCollection';
import CryptoWorker from 'utils/crypto';
import { REPORT_REASON } from 'constants/publicCollection';
import { CustomError, parseSharingErrorCodes } from 'utils/error';
const ENDPOINT = getEndpoint();
const PUBLIC_COLLECTION_FILES_TABLE = 'public-collection-files';
@ -22,6 +23,9 @@ export const getPublicCollectionUID = (token: string) => `${token}`;
const getPublicCollectionSyncTimeUID = (collectionUID: string) =>
`public-${collectionUID}-time`;
const getPublicCollectionPasswordKey = (collectionUID: string) =>
`public-${collectionUID}-passkey`;
export const getLocalPublicFiles = async (collectionUID: string) => {
const localSavedPublicCollectionFiles =
(
@ -55,6 +59,26 @@ export const savePublicCollectionFiles = async (
);
};
export const getLocalPublicCollectionPassword = async (
collectionKey: string
): Promise<string> => {
return (
(await localForage.getItem<string>(
getPublicCollectionPasswordKey(collectionKey)
)) || ''
);
};
export const savePublicCollectionPassword = async (
collectionKey: string,
passToken: string
): Promise<string> => {
return await localForage.setItem<string>(
getPublicCollectionPasswordKey(collectionKey),
passToken
);
};
export const getLocalPublicCollection = async (collectionKey: string) => {
const localCollections =
(await localForage.getItem<Collection[]>(PUBLIC_COLLECTIONS_TABLE)) ||
@ -134,11 +158,15 @@ export const syncPublicFiles = async (
const lastSyncTime = await getPublicCollectionLastSyncTime(
collectionUID
);
if (collection.updationTime === lastSyncTime) {
return files;
}
const passwordToken = await getLocalPublicCollectionPassword(
collectionUID
);
// if (collection.updationTime === lastSyncTime) {
// return files;
// }
const fetchedFiles = await getPublicFiles(
token,
passwordToken,
collection,
lastSyncTime,
files,
@ -171,7 +199,12 @@ export const syncPublicFiles = async (
);
setPublicFiles([...sortFiles(mergeMetadata(files))]);
} catch (e) {
const parsedError = parseSharingErrorCodes(e);
logError(e, 'failed to sync shared collection files');
if (parsedError.message === CustomError.TOKEN_EXPIRED) {
console.log('invalid token or password');
throw e;
}
}
return [...sortFiles(mergeMetadata(files))];
} catch (e) {
@ -182,6 +215,7 @@ export const syncPublicFiles = async (
const getPublicFiles = async (
token: string,
passwordToken: string,
collection: Collection,
sinceTime: number,
files: EnteFile[],
@ -203,6 +237,7 @@ const getPublicFiles = async (
{
'Cache-Control': 'no-cache',
'X-Auth-Access-Token': token,
'X-Auth-Access-Token-JWT': passwordToken,
}
);
decryptedFiles.push(
@ -249,7 +284,7 @@ export const getPublicCollection = async (
null,
{ 'Cache-Control': 'no-cache', 'X-Auth-Access-Token': token }
);
const fetchedCollection = resp.data?.collection;
const fetchedCollection = resp.data.collection;
const collectionName = await decryptCollectionName(
fetchedCollection,
collectionKey
@ -267,6 +302,25 @@ export const getPublicCollection = async (
}
};
export const verifyPublicCollectionPassword = async (
token: string,
passwordHash: string
): Promise<string> => {
try {
const resp = await HTTPService.post(
`${ENDPOINT}/public-collection/verify-password`,
{ passHash: passwordHash },
null,
{ 'Cache-Control': 'no-cache', 'X-Auth-Access-Token': token }
);
const jwtToken = resp.data.jwtToken;
return jwtToken;
} catch (e) {
logError(e, 'failed to get public collection');
throw e;
}
};
const decryptCollectionName = async (
collection: Collection,
collectionKey: string
@ -311,6 +365,7 @@ export const removePublicCollectionWithFiles = async (
collectionKey: string
) => {
const collectionUID = getPublicCollectionUID(token);
console.log('remove information about public collection ' + collectionUID);
const publicCollections =
(await localForage.getItem<Collection[]>(PUBLIC_COLLECTIONS_TABLE)) ||
[];
@ -320,7 +375,7 @@ export const removePublicCollectionWithFiles = async (
(collection) => collection.key !== collectionKey
)
);
await localForage.removeItem(getPublicCollectionPasswordKey(collectionUID));
await localForage.removeItem(getPublicCollectionSyncTimeUID(collectionUID));
const publicCollectionFiles =

View file

@ -27,6 +27,8 @@ export interface PublicURL {
enableDownload: boolean;
passwordEnabled: boolean;
nonce: string;
opsLimit: number;
memLimit: number;
}
export interface CreatePublicAccessTokenRequest {

View file

@ -4,6 +4,7 @@ import { EnteFile } from 'types/file';
export interface PublicCollectionGalleryContextType {
token: string;
passwordToken: string;
accessedThroughSharedURL: boolean;
setDialogMessage: SetDialogMessage;
openReportForm: () => void;

View file

@ -35,6 +35,7 @@ export enum CustomError {
REQUEST_CANCELLED = 'request canceled',
REQUEST_FAILED = 'request failed',
TOKEN_EXPIRED = 'token expired',
TOO_MANY_REQUESTS = 'too many requests',
BAD_REQUEST = 'bad request',
SUBSCRIPTION_NEEDED = 'subscription not present',
NOT_FOUND = 'not found ',
@ -121,9 +122,11 @@ export const parseSharingErrorCodes = (error) => {
break;
case ServerErrorCodes.SESSION_EXPIRED:
case ServerErrorCodes.TOKEN_EXPIRED:
case ServerErrorCodes.TOO_MANY_REQUEST:
parsedMessage = CustomError.TOKEN_EXPIRED;
break;
case ServerErrorCodes.TOO_MANY_REQUEST:
parsedMessage = CustomError.TOO_MANY_REQUESTS;
break;
default:
parsedMessage = `${constants.UNKNOWN_ERROR} statusCode:${errorCode}`;
}

View file

@ -44,7 +44,8 @@ export function downloadAsFile(filename: string, content: string) {
export async function downloadFile(
file: EnteFile,
accessedThroughSharedURL: boolean,
token?: string
token?: string,
passwordToken?: string
) {
let fileURL: string;
let tempURL: string;
@ -58,6 +59,7 @@ export async function downloadFile(
await new Response(
await PublicCollectionDownloadManager.downloadFile(
token,
passwordToken,
file
)
).blob()

View file

@ -4,6 +4,7 @@ import { PublicCollectionGalleryContextType } from 'types/publicCollection';
export const defaultPublicCollectionGalleryContext: PublicCollectionGalleryContextType =
{
token: null,
passwordToken: null,
accessedThroughSharedURL: false,
setDialogMessage: () => null,
openReportForm: () => null,

View file

@ -74,6 +74,7 @@ const englishConstants = {
SENDING: 'sending...',
SENT: 'sent!',
PASSWORD: 'password',
LINK_PASSWORD: 'enter password to unlock the album',
ENTER_PASSPHRASE: 'enter your password',
RETURN_PASSPHRASE_HINT: 'password',
SET_PASSPHRASE: 'set password',
@ -627,7 +628,8 @@ const englishConstants = {
ALBUM_URL: 'album url',
PUBLIC_SHARING: 'link sharing',
NOT_FOUND: '404 - not found',
LINK_EXPIRED: 'the link has expired!',
LINK_EXPIRED: 'the link is either disabled or expired!',
LINK_TOO_MANY_REQUESTS: 'this album is too popular for us to handle!',
DISABLE_PUBLIC_SHARING: "'disable public sharing",
DISABLE_PUBLIC_SHARING_MESSAGE:
'are you sure you want to disable public sharing?',