add support for showing password protected albums
This commit is contained in:
parent
7326b3ff19
commit
bb5b37997e
|
@ -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);
|
||||
|
|
|
@ -67,6 +67,7 @@ export default function Credentials() {
|
|||
keyAttributes.keyDecryptionNonce,
|
||||
kek
|
||||
);
|
||||
|
||||
if (isFirstLogin()) {
|
||||
await generateAndSaveIntermediateKeyAttributes(
|
||||
passphrase,
|
||||
|
|
|
@ -5,8 +5,10 @@ import {
|
|||
getLocalPublicCollection,
|
||||
getLocalPublicFiles,
|
||||
getPublicCollection,
|
||||
getPublicCollectionPassword,
|
||||
getPublicCollectionUID,
|
||||
removePublicCollectionWithFiles,
|
||||
setPublicCollectionPassword,
|
||||
syncPublicFiles,
|
||||
} from 'services/publicCollectionService';
|
||||
import { Collection } from 'types/collection';
|
||||
|
@ -28,6 +30,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 +45,7 @@ const Loader = () => (
|
|||
const bs58 = require('bs58');
|
||||
export default function PublicCollectionGallery() {
|
||||
const token = useRef<string>(null);
|
||||
const passwordToken = useRef<string>(null);
|
||||
const collectionKey = useRef<string>(null);
|
||||
const url = useRef<string>(null);
|
||||
const [publicFiles, setPublicFiles] = useState<EnteFile[]>(null);
|
||||
|
@ -54,6 +61,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);
|
||||
|
@ -115,6 +124,9 @@ export default function PublicCollectionGallery() {
|
|||
mergeMetadata(localFiles)
|
||||
);
|
||||
setPublicFiles(localPublicFiles);
|
||||
passwordToken.current = await getPublicCollectionPassword(
|
||||
collectionUID
|
||||
);
|
||||
}
|
||||
await syncWithRemote();
|
||||
} finally {
|
||||
|
@ -133,10 +145,23 @@ export default function PublicCollectionGallery() {
|
|||
token.current,
|
||||
collectionKey.current
|
||||
);
|
||||
const collectionUID = getPublicCollectionUID(token.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) &&
|
||||
(await getPublicCollectionPassword(collectionUID)) === ''
|
||||
) {
|
||||
setIsPasswordProtected(true);
|
||||
return;
|
||||
} else {
|
||||
await syncPublicFiles(
|
||||
token.current,
|
||||
collection,
|
||||
setPublicFiles
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
const parsedError = parseSharingErrorCodes(e);
|
||||
if (
|
||||
|
@ -162,6 +187,47 @@ export default function PublicCollectionGallery() {
|
|||
}
|
||||
};
|
||||
|
||||
const verifyPassphrase = 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) {
|
||||
setFieldError(
|
||||
'passphrase',
|
||||
`${constants.UNKNOWN_ERROR} ${e.message}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const collectionUID = getPublicCollectionUID(token.current);
|
||||
try {
|
||||
setPublicCollectionPassword(collectionUID, hashedPassword);
|
||||
await syncWithRemote();
|
||||
passwordToken.current = hashedPassword;
|
||||
setIsPasswordProtected(false);
|
||||
finishLoadingBar();
|
||||
} catch (e) {
|
||||
// reset local password token
|
||||
passwordToken.current = null;
|
||||
setPublicCollectionPassword(collectionUID, '');
|
||||
logError(e, 'user entered a wrong password for album');
|
||||
setFieldError('passphrase', constants.INCORRECT_PASSPHRASE);
|
||||
}
|
||||
} catch (e) {
|
||||
setFieldError(
|
||||
'passphrase',
|
||||
`${constants.UNKNOWN_ERROR} ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!publicFiles && loading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
@ -169,6 +235,30 @@ export default function PublicCollectionGallery() {
|
|||
if (errorMessage && !loading) {
|
||||
return <Container>{errorMessage}</Container>;
|
||||
}
|
||||
if (isPasswordProtected && !loading) {
|
||||
return (
|
||||
<Container>
|
||||
<Card style={{ minWidth: '320px' }} 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: '32px' }}>
|
||||
{/* <LogoImg src="/icon.svg" /> */}
|
||||
{constants.LINK_PASSWORD}
|
||||
</Card.Subtitle>
|
||||
<SingleInputForm
|
||||
callback={verifyPassphrase}
|
||||
placeholder={constants.RETURN_PASSPHRASE_HINT}
|
||||
buttonText={'unlock'}
|
||||
fieldType="password"
|
||||
/>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (!publicFiles && !loading) {
|
||||
return <Container>{constants.NOT_FOUND}</Container>;
|
||||
|
@ -179,6 +269,7 @@ export default function PublicCollectionGallery() {
|
|||
value={{
|
||||
...defaultPublicCollectionGalleryContext,
|
||||
token: token.current,
|
||||
passwordToken: passwordToken.current,
|
||||
accessedThroughSharedURL: true,
|
||||
setDialogMessage,
|
||||
openReportForm,
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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 getPublicCollectionPassword = async (
|
||||
collectionKey: string
|
||||
): Promise<string> => {
|
||||
return (
|
||||
(await localForage.getItem<string>(
|
||||
getPublicCollectionPasswordKey(collectionKey)
|
||||
)) || ''
|
||||
);
|
||||
};
|
||||
|
||||
export const setPublicCollectionPassword = 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 getPublicCollectionPassword(
|
||||
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(
|
||||
|
@ -311,6 +346,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 +356,7 @@ export const removePublicCollectionWithFiles = async (
|
|||
(collection) => collection.key !== collectionKey
|
||||
)
|
||||
);
|
||||
|
||||
await localForage.removeItem(getPublicCollectionPasswordKey(collectionUID));
|
||||
await localForage.removeItem(getPublicCollectionSyncTimeUID(collectionUID));
|
||||
|
||||
const publicCollectionFiles =
|
||||
|
|
|
@ -27,6 +27,8 @@ export interface PublicURL {
|
|||
enableDownload: boolean;
|
||||
passwordEnabled: boolean;
|
||||
nonce: string;
|
||||
opsLimit: number;
|
||||
memLimit: number;
|
||||
}
|
||||
|
||||
export interface CreatePublicAccessTokenRequest {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { EnteFile } from 'types/file';
|
|||
|
||||
export interface PublicCollectionGalleryContextType {
|
||||
token: string;
|
||||
passwordToken: string;
|
||||
accessedThroughSharedURL: boolean;
|
||||
setDialogMessage: SetDialogMessage;
|
||||
openReportForm: () => void;
|
||||
|
|
|
@ -4,6 +4,7 @@ import { PublicCollectionGalleryContextType } from 'types/publicCollection';
|
|||
export const defaultPublicCollectionGalleryContext: PublicCollectionGalleryContextType =
|
||||
{
|
||||
token: null,
|
||||
passwordToken: null,
|
||||
accessedThroughSharedURL: false,
|
||||
setDialogMessage: () => null,
|
||||
openReportForm: () => null,
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue