Initial cast commit
This commit is contained in:
parent
775fb7f66e
commit
2415fe4cab
13
apps/cast/.eslintrc.js
Normal file
13
apps/cast/.eslintrc.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
module.exports = {
|
||||
// When root is set to true, ESLint will stop looking for configuration files in parent directories.
|
||||
// This is required here to ensure desktop picks the right eslint config, where this app is
|
||||
// packaged as a submodule.
|
||||
root: true,
|
||||
extends: ['@ente/eslint-config'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
};
|
36
apps/cast/.gitignore
vendored
Normal file
36
apps/cast/.gitignore
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
40
apps/cast/README.md
Normal file
40
apps/cast/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
6
apps/cast/next.config.js
Normal file
6
apps/cast/next.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
24
apps/cast/package.json
Normal file
24
apps/cast/package.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "cast",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.0.2"
|
||||
}
|
||||
}
|
BIN
apps/cast/public/favicon.ico
Normal file
BIN
apps/cast/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
1
apps/cast/public/next.svg
Normal file
1
apps/cast/public/next.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
apps/cast/public/vercel.svg
Normal file
1
apps/cast/public/vercel.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
After Width: | Height: | Size: 629 B |
53
apps/cast/sentry.client.config.js
Normal file
53
apps/cast/sentry.client.config.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import * as Sentry from '@sentry/nextjs';
|
||||
import { getSentryTunnelURL } from 'utils/common/apiUtil';
|
||||
import { getSentryUserID } from 'utils/user';
|
||||
import { runningInBrowser } from 'utils/common';
|
||||
import { getHasOptedOutOfCrashReports } from 'utils/storage/index';
|
||||
import {
|
||||
getSentryDSN,
|
||||
getSentryENV,
|
||||
getSentryRelease,
|
||||
getIsSentryEnabled,
|
||||
} from 'constants/sentry';
|
||||
|
||||
const HAS_OPTED_OUT_OF_CRASH_REPORTING =
|
||||
runningInBrowser() && getHasOptedOutOfCrashReports();
|
||||
|
||||
if (!HAS_OPTED_OUT_OF_CRASH_REPORTING) {
|
||||
const SENTRY_DSN = getSentryDSN();
|
||||
const SENTRY_ENV = getSentryENV();
|
||||
const SENTRY_RELEASE = getSentryRelease();
|
||||
const IS_ENABLED = getIsSentryEnabled();
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
enabled: IS_ENABLED,
|
||||
environment: SENTRY_ENV,
|
||||
release: SENTRY_RELEASE,
|
||||
attachStacktrace: true,
|
||||
autoSessionTracking: false,
|
||||
tunnel: getSentryTunnelURL(),
|
||||
beforeSend(event) {
|
||||
event.request = event.request || {};
|
||||
const currentURL = new URL(document.location.href);
|
||||
currentURL.hash = '';
|
||||
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
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
Sentry.setUser({ id: await getSentryUserID() });
|
||||
};
|
||||
|
||||
main();
|
||||
}
|
3
apps/cast/sentry.properties
Normal file
3
apps/cast/sentry.properties
Normal file
|
@ -0,0 +1,3 @@
|
|||
defaults.url=https://sentry.ente.io/
|
||||
defaults.org=ente
|
||||
defaults.project=photos-web
|
28
apps/cast/sentry.server.config.js
Normal file
28
apps/cast/sentry.server.config.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import * as Sentry from '@sentry/nextjs';
|
||||
import {
|
||||
getSentryDSN,
|
||||
getSentryENV,
|
||||
getSentryRelease,
|
||||
getIsSentryEnabled,
|
||||
} from 'constants/sentry';
|
||||
|
||||
import { getSentryUserID } from 'utils/user';
|
||||
|
||||
const SENTRY_DSN = getSentryDSN();
|
||||
const SENTRY_ENV = getSentryENV();
|
||||
const SENTRY_RELEASE = getSentryRelease();
|
||||
const IS_ENABLED = getIsSentryEnabled();
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
enabled: IS_ENABLED,
|
||||
environment: SENTRY_ENV,
|
||||
release: SENTRY_RELEASE,
|
||||
autoSessionTracking: false,
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
Sentry.setUser({ id: await getSentryUserID() });
|
||||
};
|
||||
|
||||
main();
|
11
apps/cast/sentryConfigUtil.js
Normal file
11
apps/cast/sentryConfigUtil.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const ENV_DEVELOPMENT = 'development';
|
||||
|
||||
module.exports.getIsSentryEnabled = () => {
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_ENV === ENV_DEVELOPMENT) {
|
||||
return false;
|
||||
} else if (process.env.NEXT_PUBLIC_DISABLE_SENTRY === 'true') {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
1
apps/cast/src/constants/api.ts
Normal file
1
apps/cast/src/constants/api.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const REQUEST_BATCH_SIZE = 1000;
|
59
apps/cast/src/constants/apps.ts
Normal file
59
apps/cast/src/constants/apps.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { PAGES } from 'constants/pages';
|
||||
import { runningInBrowser } from 'utils/common';
|
||||
import { getAlbumsURL, getAuthURL } from 'utils/common/apiUtil';
|
||||
|
||||
export enum APPS {
|
||||
PHOTOS = 'PHOTOS',
|
||||
AUTH = 'AUTH',
|
||||
ALBUMS = 'ALBUMS',
|
||||
}
|
||||
|
||||
export const ALLOWED_APP_PAGES = new Map([
|
||||
[APPS.ALBUMS, [PAGES.SHARED_ALBUMS, PAGES.ROOT]],
|
||||
[
|
||||
APPS.AUTH,
|
||||
[
|
||||
PAGES.ROOT,
|
||||
PAGES.LOGIN,
|
||||
PAGES.SIGNUP,
|
||||
PAGES.VERIFY,
|
||||
PAGES.CREDENTIALS,
|
||||
PAGES.RECOVER,
|
||||
PAGES.CHANGE_PASSWORD,
|
||||
PAGES.GENERATE,
|
||||
PAGES.AUTH,
|
||||
PAGES.TWO_FACTOR_VERIFY,
|
||||
PAGES.TWO_FACTOR_RECOVER,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
export const CLIENT_PACKAGE_NAMES = new Map([
|
||||
[APPS.ALBUMS, 'io.ente.albums.web'],
|
||||
[APPS.PHOTOS, 'io.ente.photos.web'],
|
||||
[APPS.AUTH, 'io.ente.auth.web'],
|
||||
]);
|
||||
|
||||
export const getAppNameAndTitle = () => {
|
||||
if (!runningInBrowser()) {
|
||||
return {};
|
||||
}
|
||||
const currentURL = new URL(window.location.href);
|
||||
const albumsURL = new URL(getAlbumsURL());
|
||||
const authURL = new URL(getAuthURL());
|
||||
if (currentURL.origin === albumsURL.origin) {
|
||||
return { name: APPS.ALBUMS, title: 'ente Photos' };
|
||||
} else if (currentURL.origin === authURL.origin) {
|
||||
return { name: APPS.AUTH, title: 'ente Auth' };
|
||||
} else {
|
||||
return { name: APPS.PHOTOS, title: 'ente Photos' };
|
||||
}
|
||||
};
|
||||
|
||||
export const getAppTitle = () => {
|
||||
return getAppNameAndTitle().title;
|
||||
};
|
||||
|
||||
export const getAppName = () => {
|
||||
return getAppNameAndTitle().name;
|
||||
};
|
5
apps/cast/src/constants/cache.ts
Normal file
5
apps/cast/src/constants/cache.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export enum CACHES {
|
||||
THUMBS = 'thumbs',
|
||||
FACE_CROPS = 'face-crops',
|
||||
FILES = 'files',
|
||||
}
|
100
apps/cast/src/constants/collection.ts
Normal file
100
apps/cast/src/constants/collection.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
export const ARCHIVE_SECTION = -1;
|
||||
export const TRASH_SECTION = -2;
|
||||
export const DUMMY_UNCATEGORIZED_COLLECTION = -3;
|
||||
export const HIDDEN_ITEMS_SECTION = -4;
|
||||
export const ALL_SECTION = 0;
|
||||
export const DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME = 'Hidden';
|
||||
|
||||
export enum CollectionType {
|
||||
folder = 'folder',
|
||||
favorites = 'favorites',
|
||||
album = 'album',
|
||||
uncategorized = 'uncategorized',
|
||||
}
|
||||
|
||||
export enum CollectionSummaryType {
|
||||
folder = 'folder',
|
||||
favorites = 'favorites',
|
||||
album = 'album',
|
||||
archive = 'archive',
|
||||
trash = 'trash',
|
||||
uncategorized = 'uncategorized',
|
||||
all = 'all',
|
||||
outgoingShare = 'outgoingShare',
|
||||
incomingShareViewer = 'incomingShareViewer',
|
||||
incomingShareCollaborator = 'incomingShareCollaborator',
|
||||
sharedOnlyViaLink = 'sharedOnlyViaLink',
|
||||
archived = 'archived',
|
||||
defaultHidden = 'defaultHidden',
|
||||
hiddenItems = 'hiddenItems',
|
||||
pinned = 'pinned',
|
||||
}
|
||||
export enum COLLECTION_LIST_SORT_BY {
|
||||
NAME,
|
||||
CREATION_TIME_ASCENDING,
|
||||
UPDATION_TIME_DESCENDING,
|
||||
}
|
||||
|
||||
export const COLLECTION_SHARE_DEFAULT_VALID_DURATION =
|
||||
10 * 24 * 60 * 60 * 1000 * 1000;
|
||||
export const COLLECTION_SHARE_DEFAULT_DEVICE_LIMIT = 4;
|
||||
|
||||
export const COLLECTION_SORT_ORDER = new Map([
|
||||
[CollectionSummaryType.all, 0],
|
||||
[CollectionSummaryType.hiddenItems, 0],
|
||||
[CollectionSummaryType.uncategorized, 1],
|
||||
[CollectionSummaryType.favorites, 2],
|
||||
[CollectionSummaryType.pinned, 3],
|
||||
[CollectionSummaryType.album, 4],
|
||||
[CollectionSummaryType.folder, 4],
|
||||
[CollectionSummaryType.incomingShareViewer, 4],
|
||||
[CollectionSummaryType.incomingShareCollaborator, 4],
|
||||
[CollectionSummaryType.outgoingShare, 4],
|
||||
[CollectionSummaryType.sharedOnlyViaLink, 4],
|
||||
[CollectionSummaryType.archived, 4],
|
||||
[CollectionSummaryType.archive, 5],
|
||||
[CollectionSummaryType.trash, 6],
|
||||
[CollectionSummaryType.defaultHidden, 7],
|
||||
]);
|
||||
|
||||
export const SYSTEM_COLLECTION_TYPES = new Set([
|
||||
CollectionSummaryType.all,
|
||||
CollectionSummaryType.archive,
|
||||
CollectionSummaryType.trash,
|
||||
CollectionSummaryType.uncategorized,
|
||||
CollectionSummaryType.hiddenItems,
|
||||
CollectionSummaryType.defaultHidden,
|
||||
]);
|
||||
|
||||
export const ADD_TO_NOT_ALLOWED_COLLECTION = new Set([
|
||||
CollectionSummaryType.all,
|
||||
CollectionSummaryType.archive,
|
||||
CollectionSummaryType.incomingShareViewer,
|
||||
CollectionSummaryType.trash,
|
||||
CollectionSummaryType.uncategorized,
|
||||
CollectionSummaryType.defaultHidden,
|
||||
CollectionSummaryType.hiddenItems,
|
||||
]);
|
||||
|
||||
export const MOVE_TO_NOT_ALLOWED_COLLECTION = new Set([
|
||||
CollectionSummaryType.all,
|
||||
CollectionSummaryType.archive,
|
||||
CollectionSummaryType.incomingShareViewer,
|
||||
CollectionSummaryType.incomingShareCollaborator,
|
||||
CollectionSummaryType.trash,
|
||||
CollectionSummaryType.uncategorized,
|
||||
CollectionSummaryType.defaultHidden,
|
||||
CollectionSummaryType.hiddenItems,
|
||||
]);
|
||||
|
||||
export const OPTIONS_NOT_HAVING_COLLECTION_TYPES = new Set([
|
||||
CollectionSummaryType.all,
|
||||
CollectionSummaryType.archive,
|
||||
]);
|
||||
|
||||
export const HIDE_FROM_COLLECTION_BAR_TYPES = new Set([
|
||||
CollectionSummaryType.trash,
|
||||
CollectionSummaryType.archive,
|
||||
CollectionSummaryType.uncategorized,
|
||||
CollectionSummaryType.defaultHidden,
|
||||
]);
|
7
apps/cast/src/constants/crypto.ts
Normal file
7
apps/cast/src/constants/crypto.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024;
|
||||
|
||||
export enum PasswordStrength {
|
||||
WEAK = 'WEAK',
|
||||
MODERATE = 'MODERATE',
|
||||
STRONG = 'STRONG',
|
||||
}
|
43
apps/cast/src/constants/file.ts
Normal file
43
apps/cast/src/constants/file.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
|
||||
export const MAX_EDITED_CREATION_TIME = new Date();
|
||||
|
||||
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
|
||||
export const MAX_CAPTION_SIZE = 5000;
|
||||
|
||||
export const TYPE_HEIC = 'heic';
|
||||
export const TYPE_HEIF = 'heif';
|
||||
export const TYPE_JPEG = 'jpeg';
|
||||
export const TYPE_JPG = 'jpg';
|
||||
|
||||
export enum FILE_TYPE {
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
LIVE_PHOTO,
|
||||
OTHERS,
|
||||
}
|
||||
|
||||
export const RAW_FORMATS = [
|
||||
'heic',
|
||||
'rw2',
|
||||
'tiff',
|
||||
'arw',
|
||||
'cr3',
|
||||
'cr2',
|
||||
'raf',
|
||||
'nef',
|
||||
'psd',
|
||||
'dng',
|
||||
'tif',
|
||||
];
|
||||
export const SUPPORTED_RAW_FORMATS = [
|
||||
'heic',
|
||||
'rw2',
|
||||
'tiff',
|
||||
'arw',
|
||||
'cr3',
|
||||
'cr2',
|
||||
'nef',
|
||||
'psd',
|
||||
'dng',
|
||||
'tif',
|
||||
];
|
15
apps/cast/src/constants/gallery.ts
Normal file
15
apps/cast/src/constants/gallery.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export const GAP_BTW_TILES = 4;
|
||||
export const DATE_CONTAINER_HEIGHT = 48;
|
||||
export const SIZE_AND_COUNT_CONTAINER_HEIGHT = 72;
|
||||
export const IMAGE_CONTAINER_MAX_HEIGHT = 180;
|
||||
export const IMAGE_CONTAINER_MAX_WIDTH = 180;
|
||||
export const MIN_COLUMNS = 4;
|
||||
export const SPACE_BTW_DATES = 44;
|
||||
export const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244;
|
||||
|
||||
export enum PLAN_PERIOD {
|
||||
MONTH = 'month',
|
||||
YEAR = 'year',
|
||||
}
|
||||
|
||||
export const SYNC_INTERVAL_IN_MICROSECONDS = 1000 * 60 * 5; // 5 minutes
|
8
apps/cast/src/constants/locale.ts
Normal file
8
apps/cast/src/constants/locale.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/** Enums of supported locale */
|
||||
export enum Language {
|
||||
en = 'en',
|
||||
fr = 'fr',
|
||||
zh = 'zh',
|
||||
nl = 'nl',
|
||||
es = 'es',
|
||||
}
|
20
apps/cast/src/constants/pages.ts
Normal file
20
apps/cast/src/constants/pages.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
export enum PAGES {
|
||||
CHANGE_EMAIL = '/change-email',
|
||||
CHANGE_PASSWORD = '/change-password',
|
||||
CREDENTIALS = '/credentials',
|
||||
GALLERY = '/gallery',
|
||||
GENERATE = '/generate',
|
||||
LOGIN = '/login',
|
||||
RECOVER = '/recover',
|
||||
SIGNUP = '/signup',
|
||||
TWO_FACTOR_SETUP = '/two-factor/setup',
|
||||
TWO_FACTOR_VERIFY = '/two-factor/verify',
|
||||
TWO_FACTOR_RECOVER = '/two-factor/recover',
|
||||
VERIFY = '/verify',
|
||||
ROOT = '/',
|
||||
SHARED_ALBUMS = '/shared-albums',
|
||||
// ML_DEBUG = '/ml-debug',
|
||||
DEDUPLICATE = '/deduplicate',
|
||||
// AUTH page is used to show (auth)enticator codes
|
||||
AUTH = '/auth',
|
||||
}
|
15
apps/cast/src/constants/sentry.ts
Normal file
15
apps/cast/src/constants/sentry.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export const ENV_DEVELOPMENT = 'development';
|
||||
export const ENV_PRODUCTION = 'production';
|
||||
|
||||
export const getSentryDSN = () =>
|
||||
process.env.NEXT_PUBLIC_SENTRY_DSN ??
|
||||
'https://bd3656fc40d74d5e8f278132817963a3@sentry.ente.io/2';
|
||||
|
||||
export const getSentryENV = () =>
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENV ?? ENV_PRODUCTION;
|
||||
|
||||
export const getSentryRelease = () => process.env.SENTRY_RELEASE;
|
||||
|
||||
export { getIsSentryEnabled } from '../../sentryConfigUtil';
|
||||
|
||||
export const isDEVSentryENV = () => getSentryENV() === ENV_DEVELOPMENT;
|
142
apps/cast/src/constants/upload.ts
Normal file
142
apps/cast/src/constants/upload.ts
Normal file
|
@ -0,0 +1,142 @@
|
|||
import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
import {
|
||||
FileTypeInfo,
|
||||
ImportSuggestion,
|
||||
Location,
|
||||
ParsedExtractedMetadata,
|
||||
} from 'types/upload';
|
||||
|
||||
// list of format that were missed by type-detection for some files.
|
||||
export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [
|
||||
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpeg', mimeType: 'image/jpeg' },
|
||||
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpg', mimeType: 'image/jpeg' },
|
||||
{ fileType: FILE_TYPE.VIDEO, exactType: 'webm', mimeType: 'video/webm' },
|
||||
{ fileType: FILE_TYPE.VIDEO, exactType: 'mod', mimeType: 'video/mpeg' },
|
||||
{ fileType: FILE_TYPE.VIDEO, exactType: 'mp4', mimeType: 'video/mp4' },
|
||||
{ fileType: FILE_TYPE.IMAGE, exactType: 'gif', mimeType: 'image/gif' },
|
||||
{ fileType: FILE_TYPE.VIDEO, exactType: 'dv', mimeType: 'video/x-dv' },
|
||||
{
|
||||
fileType: FILE_TYPE.VIDEO,
|
||||
exactType: 'wmv',
|
||||
mimeType: 'video/x-ms-asf',
|
||||
},
|
||||
{
|
||||
fileType: FILE_TYPE.VIDEO,
|
||||
exactType: 'hevc',
|
||||
mimeType: 'video/hevc',
|
||||
},
|
||||
{
|
||||
fileType: FILE_TYPE.IMAGE,
|
||||
exactType: 'raf',
|
||||
mimeType: 'image/x-fuji-raf',
|
||||
},
|
||||
{
|
||||
fileType: FILE_TYPE.IMAGE,
|
||||
exactType: 'orf',
|
||||
mimeType: 'image/x-olympus-orf',
|
||||
},
|
||||
|
||||
{
|
||||
fileType: FILE_TYPE.IMAGE,
|
||||
exactType: 'crw',
|
||||
mimeType: 'image/x-canon-crw',
|
||||
},
|
||||
];
|
||||
|
||||
export const KNOWN_NON_MEDIA_FORMATS = ['xmp', 'html', 'txt'];
|
||||
|
||||
export const EXIFLESS_FORMATS = ['gif', 'bmp'];
|
||||
|
||||
// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part.
|
||||
export const MULTIPART_PART_SIZE = 20 * 1024 * 1024;
|
||||
|
||||
export const FILE_READER_CHUNK_SIZE = ENCRYPTION_CHUNK_SIZE;
|
||||
|
||||
export const FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART = Math.floor(
|
||||
MULTIPART_PART_SIZE / FILE_READER_CHUNK_SIZE
|
||||
);
|
||||
|
||||
export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random();
|
||||
|
||||
export const NULL_LOCATION: Location = { latitude: null, longitude: null };
|
||||
|
||||
export enum UPLOAD_STAGES {
|
||||
START,
|
||||
READING_GOOGLE_METADATA_FILES,
|
||||
EXTRACTING_METADATA,
|
||||
UPLOADING,
|
||||
CANCELLING,
|
||||
FINISH,
|
||||
}
|
||||
|
||||
export enum UPLOAD_STRATEGY {
|
||||
SINGLE_COLLECTION,
|
||||
COLLECTION_PER_FOLDER,
|
||||
}
|
||||
|
||||
export enum UPLOAD_RESULT {
|
||||
FAILED,
|
||||
ALREADY_UPLOADED,
|
||||
UNSUPPORTED,
|
||||
BLOCKED,
|
||||
TOO_LARGE,
|
||||
LARGER_THAN_AVAILABLE_STORAGE,
|
||||
UPLOADED,
|
||||
UPLOADED_WITH_STATIC_THUMBNAIL,
|
||||
ADDED_SYMLINK,
|
||||
}
|
||||
|
||||
export enum PICKED_UPLOAD_TYPE {
|
||||
FILES = 'files',
|
||||
FOLDERS = 'folders',
|
||||
ZIPS = 'zips',
|
||||
}
|
||||
|
||||
export const MAX_FILE_SIZE_SUPPORTED = 4 * 1024 * 1024 * 1024; // 4 GB
|
||||
|
||||
export const LIVE_PHOTO_ASSET_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB
|
||||
|
||||
export const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = {
|
||||
location: NULL_LOCATION,
|
||||
creationTime: null,
|
||||
width: null,
|
||||
height: null,
|
||||
};
|
||||
|
||||
export const A_SEC_IN_MICROSECONDS = 1e6;
|
||||
|
||||
export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = {
|
||||
rootFolderName: '',
|
||||
hasNestedFolders: false,
|
||||
hasRootLevelFileWithFolder: false,
|
||||
};
|
||||
|
||||
export const BLACK_THUMBNAIL_BASE64 =
|
||||
'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' +
|
||||
'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ' +
|
||||
'EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC' +
|
||||
'ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF' +
|
||||
'BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk' +
|
||||
'6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL' +
|
||||
'W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA' +
|
||||
'AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY' +
|
||||
'nLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImK' +
|
||||
'kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oAD' +
|
||||
'AMBAAIRAxEAPwD/AD/6ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
|
||||
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
|
||||
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC' +
|
||||
'gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
|
||||
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
|
||||
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
|
||||
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
|
||||
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
|
||||
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA' +
|
||||
'KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg' +
|
||||
'AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
|
||||
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA' +
|
||||
'CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAK' +
|
||||
'ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA' +
|
||||
'KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
|
||||
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo' +
|
||||
'AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k=';
|
19
apps/cast/src/constants/urls.ts
Normal file
19
apps/cast/src/constants/urls.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
export const ENTE_WEBSITE_LINK = 'https://ente.io';
|
||||
|
||||
export const ML_BLOG_LINK = 'https://ente.io/blog/desktop-ml-beta';
|
||||
|
||||
export const FACE_SEARCH_PRIVACY_POLICY_LINK =
|
||||
'https://ente.io/privacy#8-biometric-information-privacy-policy';
|
||||
|
||||
export const SUPPORT_EMAIL = 'support@ente.io';
|
||||
|
||||
export const APP_DOWNLOAD_URL = 'https://ente.io/download/desktop';
|
||||
|
||||
export const FEEDBACK_EMAIL = 'feedback@ente.io';
|
||||
|
||||
export const DELETE_ACCOUNT_EMAIL = 'account-deletion@ente.io';
|
||||
|
||||
export const WEB_ROADMAP_URL = 'https://github.com/ente-io/photos-web/issues';
|
||||
|
||||
export const DESKTOP_ROADMAP_URL =
|
||||
'https://github.com/ente-io/photos-desktop/issues';
|
6
apps/cast/src/pages/_app.tsx
Normal file
6
apps/cast/src/pages/_app.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import '/styles/globals.css';
|
||||
import type { AppProps } from 'next/app';
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />;
|
||||
}
|
13
apps/cast/src/pages/_document.tsx
Normal file
13
apps/cast/src/pages/_document.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Html, Head, Main, NextScript } from 'next/document';
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
118
apps/cast/src/pages/index.tsx
Normal file
118
apps/cast/src/pages/index.tsx
Normal file
|
@ -0,0 +1,118 @@
|
|||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import { Inter } from 'next/font/google';
|
||||
import styles from '/styles/Home.module.css';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Generated by create next app"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className={`${styles.main} ${inter.className}`}>
|
||||
<div className={styles.description}>
|
||||
<p>
|
||||
Get started by editing
|
||||
<code className={styles.code}>src/pages/index.tsx</code>
|
||||
</p>
|
||||
<div>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
By{' '}
|
||||
<Image
|
||||
src="/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
className={styles.vercelLogo}
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.center}>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/next.svg"
|
||||
alt="Next.js Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<h2>
|
||||
Docs <span>-></span>
|
||||
</h2>
|
||||
<p>
|
||||
Find in-depth information about Next.js features
|
||||
and API.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<h2>
|
||||
Learn <span>-></span>
|
||||
</h2>
|
||||
<p>
|
||||
Learn about Next.js in an interactive course
|
||||
with quizzes!
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<h2>
|
||||
Templates <span>-></span>
|
||||
</h2>
|
||||
<p>
|
||||
Discover and deploy boilerplate example
|
||||
Next.js projects.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<h2>
|
||||
Deploy <span>-></span>
|
||||
</h2>
|
||||
<p>
|
||||
Instantly deploy your Next.js site to a shareable
|
||||
URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
241
apps/cast/src/services/HTTPService.ts
Normal file
241
apps/cast/src/services/HTTPService.ts
Normal file
|
@ -0,0 +1,241 @@
|
|||
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { ApiError, CustomError, isApiErrorResponse } from 'utils/error';
|
||||
|
||||
interface IHTTPHeaders {
|
||||
[headerKey: string]: any;
|
||||
}
|
||||
|
||||
interface IQueryPrams {
|
||||
[paramName: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service to manage all HTTP calls.
|
||||
*/
|
||||
class HTTPService {
|
||||
constructor() {
|
||||
axios.interceptors.response.use(
|
||||
(response) => Promise.resolve(response),
|
||||
(error) => {
|
||||
const config = error.config as AxiosRequestConfig;
|
||||
if (error.response) {
|
||||
const response = error.response as AxiosResponse;
|
||||
let apiError: ApiError;
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if (isApiErrorResponse(response.data)) {
|
||||
const responseData = response.data;
|
||||
logError(error, 'HTTP Service Error', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
xRequestId: response.headers['x-request-id'],
|
||||
httpStatus: response.status,
|
||||
errMessage: responseData.message,
|
||||
errCode: responseData.code,
|
||||
});
|
||||
apiError = new ApiError(
|
||||
responseData.message,
|
||||
responseData.code,
|
||||
response.status
|
||||
);
|
||||
} else {
|
||||
if (response.status >= 400 && response.status < 500) {
|
||||
apiError = new ApiError(
|
||||
CustomError.CLIENT_ERROR,
|
||||
'',
|
||||
response.status
|
||||
);
|
||||
} else {
|
||||
apiError = new ApiError(
|
||||
CustomError.ServerError,
|
||||
'',
|
||||
response.status
|
||||
);
|
||||
}
|
||||
}
|
||||
logError(apiError, 'HTTP Service Error', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
cfRay: response.headers['cf-ray'],
|
||||
xRequestId: response.headers['x-request-id'],
|
||||
httpStatus: response.status,
|
||||
});
|
||||
throw apiError;
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
addLogLine(
|
||||
'request failed- no response',
|
||||
`url: ${config.url}`,
|
||||
`method: ${config.method}`
|
||||
);
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
addLogLine(
|
||||
'request failed- axios error',
|
||||
`url: ${config.url}`,
|
||||
`method: ${config.method}`
|
||||
);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* header object to be append to all api calls.
|
||||
*/
|
||||
private headers: IHTTPHeaders = {
|
||||
'content-type': 'application/json',
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the headers to the given object.
|
||||
*/
|
||||
public setHeaders(headers: IHTTPHeaders) {
|
||||
this.headers = {
|
||||
...this.headers,
|
||||
...headers,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a header to list of headers.
|
||||
*/
|
||||
public appendHeader(key: string, value: string) {
|
||||
this.headers = {
|
||||
...this.headers,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given header.
|
||||
*/
|
||||
public removeHeader(key: string) {
|
||||
this.headers[key] = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns axios interceptors.
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
public getInterceptors() {
|
||||
return axios.interceptors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic HTTP request.
|
||||
* This is done so that developer can use any functionality
|
||||
* provided by axios. Here, only the set headers are spread
|
||||
* over what was sent in config.
|
||||
*/
|
||||
public async request(config: AxiosRequestConfig, customConfig?: any) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.headers = {
|
||||
...this.headers,
|
||||
...config.headers,
|
||||
};
|
||||
if (customConfig?.cancel) {
|
||||
config.cancelToken = new axios.CancelToken(
|
||||
(c) => (customConfig.cancel.exec = c)
|
||||
);
|
||||
}
|
||||
return await axios({ ...config, ...customConfig });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request.
|
||||
*/
|
||||
public get(
|
||||
url: string,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
headers,
|
||||
method: 'GET',
|
||||
params,
|
||||
url,
|
||||
},
|
||||
customConfig
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post request
|
||||
*/
|
||||
public post(
|
||||
url: string,
|
||||
data?: any,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
data,
|
||||
headers,
|
||||
method: 'POST',
|
||||
params,
|
||||
url,
|
||||
},
|
||||
customConfig
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put request
|
||||
*/
|
||||
public put(
|
||||
url: string,
|
||||
data: any,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
data,
|
||||
headers,
|
||||
method: 'PUT',
|
||||
params,
|
||||
url,
|
||||
},
|
||||
customConfig
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete request
|
||||
*/
|
||||
public delete(
|
||||
url: string,
|
||||
data: any,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
data,
|
||||
headers,
|
||||
method: 'DELETE',
|
||||
params,
|
||||
url,
|
||||
},
|
||||
customConfig
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a Singleton Service.
|
||||
// This will help me maintain common headers / functionality
|
||||
// at a central place.
|
||||
export default new HTTPService();
|
32
apps/cast/src/services/InMemoryStore.ts
Normal file
32
apps/cast/src/services/InMemoryStore.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
export enum MS_KEYS {
|
||||
OPT_OUT_OF_CRASH_REPORTS = 'optOutOfCrashReports',
|
||||
SRP_CONFIGURE_IN_PROGRESS = 'srpConfigureInProgress',
|
||||
REDIRECT_URL = 'redirectUrl',
|
||||
}
|
||||
|
||||
type StoreType = Map<Partial<MS_KEYS>, any>;
|
||||
|
||||
class InMemoryStore {
|
||||
private store: StoreType = new Map();
|
||||
|
||||
get(key: MS_KEYS) {
|
||||
return this.store.get(key);
|
||||
}
|
||||
|
||||
set(key: MS_KEYS, value: any) {
|
||||
this.store.set(key, value);
|
||||
}
|
||||
|
||||
delete(key: MS_KEYS) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
|
||||
has(key: MS_KEYS) {
|
||||
return this.store.has(key);
|
||||
}
|
||||
clear() {
|
||||
this.store.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export default new InMemoryStore();
|
1467
apps/cast/src/services/collectionService.ts
Normal file
1467
apps/cast/src/services/collectionService.ts
Normal file
File diff suppressed because it is too large
Load diff
12
apps/cast/src/services/events.ts
Normal file
12
apps/cast/src/services/events.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { EventEmitter } from 'eventemitter3';
|
||||
|
||||
// When registering event handlers,
|
||||
// handle errors to avoid unhandled rejection or propagation to emit call
|
||||
|
||||
export enum Events {
|
||||
LOGOUT = 'logout',
|
||||
FILE_UPLOADED = 'fileUploaded',
|
||||
LOCAL_FILES_UPDATED = 'localFilesUpdated',
|
||||
}
|
||||
|
||||
export const eventBus = new EventEmitter<Events>();
|
311
apps/cast/src/services/fileService.ts
Normal file
311
apps/cast/src/services/fileService.ts
Normal file
|
@ -0,0 +1,311 @@
|
|||
import { getEndpoint } from 'utils/common/apiUtil';
|
||||
import localForage from 'utils/storage/localForage';
|
||||
|
||||
import { getToken } from 'utils/common/key';
|
||||
import { Collection } from 'types/collection';
|
||||
import HTTPService from './HTTPService';
|
||||
import { logError } from 'utils/sentry';
|
||||
import {
|
||||
decryptFile,
|
||||
getLatestVersionFiles,
|
||||
mergeMetadata,
|
||||
sortFiles,
|
||||
} from 'utils/file';
|
||||
import { eventBus, Events } from './events';
|
||||
import {
|
||||
EnteFile,
|
||||
EncryptedEnteFile,
|
||||
TrashRequest,
|
||||
FileWithUpdatedMagicMetadata,
|
||||
FileWithUpdatedPublicMagicMetadata,
|
||||
} from 'types/file';
|
||||
import { SetFiles } from 'types/gallery';
|
||||
import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
import {
|
||||
getCollectionLastSyncTime,
|
||||
setCollectionLastSyncTime,
|
||||
} from './collectionService';
|
||||
import { REQUEST_BATCH_SIZE } from 'constants/api';
|
||||
import { batch } from 'utils/common';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
const FILES_TABLE = 'files';
|
||||
const HIDDEN_FILES_TABLE = 'hidden-files';
|
||||
|
||||
export const getLocalFiles = async (type: 'normal' | 'hidden' = 'normal') => {
|
||||
const tableName = type === 'normal' ? FILES_TABLE : HIDDEN_FILES_TABLE;
|
||||
const files: Array<EnteFile> =
|
||||
(await localForage.getItem<EnteFile[]>(tableName)) || [];
|
||||
return files;
|
||||
};
|
||||
|
||||
const setLocalFiles = async (type: 'normal' | 'hidden', files: EnteFile[]) => {
|
||||
try {
|
||||
const tableName = type === 'normal' ? FILES_TABLE : HIDDEN_FILES_TABLE;
|
||||
await localForage.setItem(tableName, files);
|
||||
try {
|
||||
eventBus.emit(Events.LOCAL_FILES_UPDATED);
|
||||
} catch (e) {
|
||||
logError(e, 'Error in localFileUpdated handlers');
|
||||
}
|
||||
} catch (e1) {
|
||||
try {
|
||||
const storageEstimate = await navigator.storage.estimate();
|
||||
logError(e1, 'failed to save files to indexedDB', {
|
||||
storageEstimate,
|
||||
});
|
||||
addLogLine(`storage estimate ${JSON.stringify(storageEstimate)}`);
|
||||
} catch (e2) {
|
||||
logError(e1, 'failed to save files to indexedDB');
|
||||
logError(e2, 'failed to get storage stats');
|
||||
}
|
||||
throw e1;
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllLocalFiles = async () => {
|
||||
const normalFiles = await getLocalFiles('normal');
|
||||
const hiddenFiles = await getLocalFiles('hidden');
|
||||
return [...normalFiles, ...hiddenFiles];
|
||||
};
|
||||
|
||||
export const syncFiles = async (
|
||||
type: 'normal' | 'hidden',
|
||||
collections: Collection[],
|
||||
setFiles: SetFiles
|
||||
) => {
|
||||
const localFiles = await getLocalFiles(type);
|
||||
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
||||
if (files.length !== localFiles.length) {
|
||||
await setLocalFiles(type, files);
|
||||
setFiles(sortFiles(mergeMetadata(files)));
|
||||
}
|
||||
for (const collection of collections) {
|
||||
if (!getToken()) {
|
||||
continue;
|
||||
}
|
||||
const lastSyncTime = await getCollectionLastSyncTime(collection);
|
||||
if (collection.updationTime === lastSyncTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newFiles = await getFiles(collection, lastSyncTime, setFiles);
|
||||
files = getLatestVersionFiles([...files, ...newFiles]);
|
||||
await setLocalFiles(type, files);
|
||||
setCollectionLastSyncTime(collection, collection.updationTime);
|
||||
}
|
||||
return files;
|
||||
};
|
||||
|
||||
export const getFiles = async (
|
||||
collection: Collection,
|
||||
sinceTime: number,
|
||||
setFiles: SetFiles
|
||||
): Promise<EnteFile[]> => {
|
||||
try {
|
||||
let decryptedFiles: EnteFile[] = [];
|
||||
let time = sinceTime;
|
||||
let resp;
|
||||
do {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
break;
|
||||
}
|
||||
resp = await HTTPService.get(
|
||||
`${ENDPOINT}/collections/v2/diff`,
|
||||
{
|
||||
collectionID: collection.id,
|
||||
sinceTime: time,
|
||||
},
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
|
||||
const newDecryptedFilesBatch = await Promise.all(
|
||||
resp.data.diff.map(async (file: EncryptedEnteFile) => {
|
||||
if (!file.isDeleted) {
|
||||
return await decryptFile(file, collection.key);
|
||||
} else {
|
||||
return file;
|
||||
}
|
||||
}) as Promise<EnteFile>[]
|
||||
);
|
||||
decryptedFiles = [...decryptedFiles, ...newDecryptedFilesBatch];
|
||||
|
||||
setFiles((files) =>
|
||||
sortFiles(
|
||||
mergeMetadata(
|
||||
getLatestVersionFiles([
|
||||
...(files || []),
|
||||
...decryptedFiles,
|
||||
])
|
||||
)
|
||||
)
|
||||
);
|
||||
if (resp.data.diff.length) {
|
||||
time = resp.data.diff.slice(-1)[0].updationTime;
|
||||
}
|
||||
} while (resp.data.hasMore);
|
||||
return decryptedFiles;
|
||||
} catch (e) {
|
||||
logError(e, 'Get files failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const removeDeletedCollectionFiles = async (
|
||||
collections: Collection[],
|
||||
files: EnteFile[]
|
||||
) => {
|
||||
const syncedCollectionIds = new Set<number>();
|
||||
for (const collection of collections) {
|
||||
syncedCollectionIds.add(collection.id);
|
||||
}
|
||||
files = files.filter((file) => syncedCollectionIds.has(file.collectionID));
|
||||
return files;
|
||||
};
|
||||
|
||||
export const trashFiles = async (filesToTrash: EnteFile[]) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const batchedFilesToTrash = batch(filesToTrash, REQUEST_BATCH_SIZE);
|
||||
for (const batch of batchedFilesToTrash) {
|
||||
const trashRequest: TrashRequest = {
|
||||
items: batch.map((file) => ({
|
||||
fileID: file.id,
|
||||
collectionID: file.collectionID,
|
||||
})),
|
||||
};
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/files/trash`,
|
||||
trashRequest,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'trash file failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteFromTrash = async (filesToDelete: number[]) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const batchedFilesToDelete = batch(filesToDelete, REQUEST_BATCH_SIZE);
|
||||
|
||||
for (const batch of batchedFilesToDelete) {
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/trash/delete`,
|
||||
{ fileIDs: batch },
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'deleteFromTrash failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateFileMagicMetadata = async (
|
||||
fileWithUpdatedMagicMetadataList: FileWithUpdatedMagicMetadata[]
|
||||
) => {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] };
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
for (const {
|
||||
file,
|
||||
updatedMagicMetadata,
|
||||
} of fileWithUpdatedMagicMetadataList) {
|
||||
const { file: encryptedMagicMetadata } =
|
||||
await cryptoWorker.encryptMetadata(
|
||||
updatedMagicMetadata.data,
|
||||
file.key
|
||||
);
|
||||
reqBody.metadataList.push({
|
||||
id: file.id,
|
||||
magicMetadata: {
|
||||
version: updatedMagicMetadata.version,
|
||||
count: updatedMagicMetadata.count,
|
||||
data: encryptedMagicMetadata.encryptedData,
|
||||
header: encryptedMagicMetadata.decryptionHeader,
|
||||
},
|
||||
});
|
||||
}
|
||||
await HTTPService.put(`${ENDPOINT}/files/magic-metadata`, reqBody, null, {
|
||||
'X-Auth-Token': token,
|
||||
});
|
||||
return fileWithUpdatedMagicMetadataList.map(
|
||||
({ file, updatedMagicMetadata }): EnteFile => ({
|
||||
...file,
|
||||
magicMetadata: {
|
||||
...updatedMagicMetadata,
|
||||
version: updatedMagicMetadata.version + 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const updateFilePublicMagicMetadata = async (
|
||||
fileWithUpdatedPublicMagicMetadataList: FileWithUpdatedPublicMagicMetadata[]
|
||||
): Promise<EnteFile[]> => {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] };
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
for (const {
|
||||
file,
|
||||
updatedPublicMagicMetadata: updatePublicMagicMetadata,
|
||||
} of fileWithUpdatedPublicMagicMetadataList) {
|
||||
const { file: encryptedPubMagicMetadata } =
|
||||
await cryptoWorker.encryptMetadata(
|
||||
updatePublicMagicMetadata.data,
|
||||
file.key
|
||||
);
|
||||
reqBody.metadataList.push({
|
||||
id: file.id,
|
||||
magicMetadata: {
|
||||
version: updatePublicMagicMetadata.version,
|
||||
count: updatePublicMagicMetadata.count,
|
||||
data: encryptedPubMagicMetadata.encryptedData,
|
||||
header: encryptedPubMagicMetadata.decryptionHeader,
|
||||
},
|
||||
});
|
||||
}
|
||||
await HTTPService.put(
|
||||
`${ENDPOINT}/files/public-magic-metadata`,
|
||||
reqBody,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return fileWithUpdatedPublicMagicMetadataList.map(
|
||||
({ file, updatedPublicMagicMetadata }): EnteFile => ({
|
||||
...file,
|
||||
pubMagicMetadata: {
|
||||
...updatedPublicMagicMetadata,
|
||||
version: updatedPublicMagicMetadata.version + 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
757
apps/cast/src/services/userService.ts
Normal file
757
apps/cast/src/services/userService.ts
Normal file
|
@ -0,0 +1,757 @@
|
|||
import { PAGES } from 'constants/pages';
|
||||
import {
|
||||
getEndpoint,
|
||||
getFamilyPortalURL,
|
||||
isDevDeployment,
|
||||
} from 'utils/common/apiUtil';
|
||||
import { clearKeys } from 'utils/storage/sessionStorage';
|
||||
import router from 'next/router';
|
||||
import { clearData, getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import localForage from 'utils/storage/localForage';
|
||||
import { getToken } from 'utils/common/key';
|
||||
import HTTPService from './HTTPService';
|
||||
import {
|
||||
computeVerifierHelper,
|
||||
generateLoginSubKey,
|
||||
generateSRPClient,
|
||||
getRecoveryKey,
|
||||
} from 'utils/crypto';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { eventBus, Events } from './events';
|
||||
import {
|
||||
KeyAttributes,
|
||||
RecoveryKey,
|
||||
TwoFactorSecret,
|
||||
TwoFactorVerificationResponse,
|
||||
TwoFactorRecoveryResponse,
|
||||
UserDetails,
|
||||
DeleteChallengeResponse,
|
||||
GetRemoteStoreValueResponse,
|
||||
SetupSRPRequest,
|
||||
CreateSRPSessionResponse,
|
||||
UserVerificationResponse,
|
||||
GetFeatureFlagResponse,
|
||||
SetupSRPResponse,
|
||||
CompleteSRPSetupRequest,
|
||||
CompleteSRPSetupResponse,
|
||||
SRPSetupAttributes,
|
||||
SRPAttributes,
|
||||
UpdateSRPAndKeysRequest,
|
||||
UpdateSRPAndKeysResponse,
|
||||
GetSRPAttributesResponse,
|
||||
} from 'types/user';
|
||||
import { ApiError, CustomError } from 'utils/error';
|
||||
import isElectron from 'is-electron';
|
||||
// import safeStorageService from './electron/safeStorage';
|
||||
// import { deleteAllCache } from 'utils/storage/cache';
|
||||
import { B64EncryptionResult } from 'types/crypto';
|
||||
import { getLocalFamilyData, isPartOfFamily } from 'utils/user/family';
|
||||
import { AxiosResponse, HttpStatusCode } from 'axios';
|
||||
import { APPS, getAppName } from 'constants/apps';
|
||||
import { addLocalLog } from 'utils/logging';
|
||||
import { convertBase64ToBuffer, convertBufferToBase64 } from 'utils/user';
|
||||
import { setLocalMapEnabled } from 'utils/storage';
|
||||
import InMemoryStore, { MS_KEYS } from './InMemoryStore';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
const HAS_SET_KEYS = 'hasSetKeys';
|
||||
|
||||
export const sendOtt = (email: string) => {
|
||||
const appName = getAppName();
|
||||
return HTTPService.post(`${ENDPOINT}/users/ott`, {
|
||||
email,
|
||||
client: appName === APPS.AUTH ? 'totp' : 'web',
|
||||
});
|
||||
};
|
||||
|
||||
export const getPublicKey = async (email: string) => {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/public-key`,
|
||||
{ email },
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return resp.data.publicKey;
|
||||
};
|
||||
|
||||
export const getPaymentToken = async () => {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/payment-token`,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return resp.data['paymentToken'];
|
||||
};
|
||||
|
||||
export const getFamiliesToken = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/families-token`,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return resp.data['familiesToken'];
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get family token');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getRoadmapRedirectURL = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/roadmap/v2`,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return resp.data['url'];
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get roadmap url');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyOtt = (email: string, ott: string) =>
|
||||
HTTPService.post(`${ENDPOINT}/users/verify-email`, { email, ott });
|
||||
|
||||
export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
|
||||
HTTPService.put(`${ENDPOINT}/users/attributes`, { keyAttributes }, null, {
|
||||
'X-Auth-Token': token,
|
||||
});
|
||||
|
||||
export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) =>
|
||||
HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, null, {
|
||||
'X-Auth-Token': token,
|
||||
});
|
||||
|
||||
export const logoutUser = async () => {
|
||||
try {
|
||||
try {
|
||||
// ignore server logout result as logoutUser can be triggered before sign up or on token expiry
|
||||
await _logout();
|
||||
} catch (e) {
|
||||
//ignore
|
||||
}
|
||||
try {
|
||||
InMemoryStore.clear();
|
||||
} catch (e) {
|
||||
logError(e, 'clear InMemoryStore failed');
|
||||
}
|
||||
try {
|
||||
clearKeys();
|
||||
} catch (e) {
|
||||
logError(e, 'clearKeys failed');
|
||||
}
|
||||
try {
|
||||
clearData();
|
||||
} catch (e) {
|
||||
logError(e, 'clearData failed');
|
||||
}
|
||||
try {
|
||||
// await deleteAllCache();
|
||||
} catch (e) {
|
||||
logError(e, 'deleteAllCache failed');
|
||||
}
|
||||
try {
|
||||
await clearFiles();
|
||||
} catch (e) {
|
||||
logError(e, 'clearFiles failed');
|
||||
}
|
||||
if (isElectron()) {
|
||||
try {
|
||||
// safeStorageService.clearElectronStore();
|
||||
} catch (e) {
|
||||
logError(e, 'clearElectronStore failed');
|
||||
}
|
||||
}
|
||||
try {
|
||||
eventBus.emit(Events.LOGOUT);
|
||||
} catch (e) {
|
||||
logError(e, 'Error in logout handlers');
|
||||
}
|
||||
router.push(PAGES.ROOT);
|
||||
} catch (e) {
|
||||
logError(e, 'logoutUser failed');
|
||||
}
|
||||
};
|
||||
|
||||
export const clearFiles = async () => {
|
||||
await localForage.clear();
|
||||
};
|
||||
|
||||
export const isTokenValid = async (token: string) => {
|
||||
try {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/session-validity/v2`,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
try {
|
||||
if (resp.data[HAS_SET_KEYS] === undefined) {
|
||||
throw Error('resp.data.hasSetKey undefined');
|
||||
}
|
||||
if (!resp.data['hasSetKeys']) {
|
||||
try {
|
||||
await putAttributes(
|
||||
token,
|
||||
getData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES)
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'put attribute failed');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'hasSetKeys not set in session validity response');
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
logError(e, 'session-validity api call failed');
|
||||
if (
|
||||
e instanceof ApiError &&
|
||||
e.httpStatusCode === HttpStatusCode.Unauthorized
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const setupTwoFactor = async () => {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/two-factor/setup`,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
}
|
||||
);
|
||||
return resp.data as TwoFactorSecret;
|
||||
};
|
||||
|
||||
export const enableTwoFactor = async (
|
||||
code: string,
|
||||
recoveryEncryptedTwoFactorSecret: B64EncryptionResult
|
||||
) => {
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/users/two-factor/enable`,
|
||||
{
|
||||
code,
|
||||
encryptedTwoFactorSecret:
|
||||
recoveryEncryptedTwoFactorSecret.encryptedData,
|
||||
twoFactorSecretDecryptionNonce:
|
||||
recoveryEncryptedTwoFactorSecret.nonce,
|
||||
},
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const verifyTwoFactor = async (code: string, sessionID: string) => {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/two-factor/verify`,
|
||||
{
|
||||
code,
|
||||
sessionID,
|
||||
},
|
||||
null
|
||||
);
|
||||
return resp.data as TwoFactorVerificationResponse;
|
||||
};
|
||||
|
||||
export const recoverTwoFactor = async (sessionID: string) => {
|
||||
const resp = await HTTPService.get(`${ENDPOINT}/users/two-factor/recover`, {
|
||||
sessionID,
|
||||
});
|
||||
return resp.data as TwoFactorRecoveryResponse;
|
||||
};
|
||||
|
||||
export const removeTwoFactor = async (sessionID: string, secret: string) => {
|
||||
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, {
|
||||
sessionID,
|
||||
secret,
|
||||
});
|
||||
return resp.data as TwoFactorVerificationResponse;
|
||||
};
|
||||
|
||||
export const disableTwoFactor = async () => {
|
||||
await HTTPService.post(`${ENDPOINT}/users/two-factor/disable`, null, null, {
|
||||
'X-Auth-Token': getToken(),
|
||||
});
|
||||
};
|
||||
|
||||
export const getTwoFactorStatus = async () => {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/two-factor/status`,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
}
|
||||
);
|
||||
return resp.data['status'];
|
||||
};
|
||||
|
||||
export const _logout = async () => {
|
||||
if (!getToken()) return true;
|
||||
try {
|
||||
await HTTPService.post(`${ENDPOINT}/users/logout`, null, null, {
|
||||
'X-Auth-Token': getToken(),
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
logError(e, '/users/logout failed');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const sendOTTForEmailChange = async (email: string) => {
|
||||
if (!getToken()) {
|
||||
return null;
|
||||
}
|
||||
await HTTPService.post(`${ENDPOINT}/users/ott`, {
|
||||
email,
|
||||
client: 'web',
|
||||
purpose: 'change',
|
||||
});
|
||||
};
|
||||
|
||||
export const changeEmail = async (email: string, ott: string) => {
|
||||
if (!getToken()) {
|
||||
return null;
|
||||
}
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/users/change-email`,
|
||||
{
|
||||
email,
|
||||
ott,
|
||||
},
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const getUserDetailsV2 = async (): Promise<UserDetails> => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/details/v2`,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return resp.data;
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get user details v2');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFamilyPortalRedirectURL = async () => {
|
||||
try {
|
||||
const jwtToken = await getFamiliesToken();
|
||||
const isFamilyCreated = isPartOfFamily(getLocalFamilyData());
|
||||
return `${getFamilyPortalURL()}?token=${jwtToken}&isFamilyCreated=${isFamilyCreated}&redirectURL=${
|
||||
window.location.origin
|
||||
}/gallery`;
|
||||
} catch (e) {
|
||||
logError(e, 'unable to generate to family portal URL');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getAccountDeleteChallenge = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/delete-challenge`,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return resp.data as DeleteChallengeResponse;
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get account delete challenge');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAccount = async (
|
||||
challenge: string,
|
||||
reason: string,
|
||||
feedback: string
|
||||
) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
await HTTPService.delete(
|
||||
`${ENDPOINT}/users/delete`,
|
||||
{ challenge, reason, feedback },
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'deleteAccount api call failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure that the keys in local storage are not malformed by verifying that the
|
||||
// recoveryKey can be decrypted with the masterKey.
|
||||
// Note: This is not bullet-proof.
|
||||
export const validateKey = async () => {
|
||||
try {
|
||||
await getRecoveryKey();
|
||||
return true;
|
||||
} catch (e) {
|
||||
await logoutUser();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFaceSearchEnabledStatus = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
const resp: AxiosResponse<GetRemoteStoreValueResponse> =
|
||||
await HTTPService.get(
|
||||
`${ENDPOINT}/remote-store`,
|
||||
{
|
||||
key: 'faceSearchEnabled',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return resp.data.value === 'true';
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get face search enabled status');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateFaceSearchEnabledStatus = async (newStatus: boolean) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/remote-store/update`,
|
||||
{
|
||||
key: 'faceSearchEnabled',
|
||||
value: newStatus.toString(),
|
||||
},
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'failed to update face search enabled status');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const syncMapEnabled = async () => {
|
||||
try {
|
||||
const status = await getMapEnabledStatus();
|
||||
setLocalMapEnabled(status);
|
||||
} catch (e) {
|
||||
logError(e, 'failed to sync map enabled status');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getMapEnabledStatus = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
const resp: AxiosResponse<GetRemoteStoreValueResponse> =
|
||||
await HTTPService.get(
|
||||
`${ENDPOINT}/remote-store`,
|
||||
{
|
||||
key: 'mapEnabled',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return resp.data.value === 'true';
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get map enabled status');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateMapEnabledStatus = async (newStatus: boolean) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/remote-store/update`,
|
||||
{
|
||||
key: 'mapEnabled',
|
||||
value: newStatus.toString(),
|
||||
},
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'failed to update map enabled status');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export async function getDisableCFUploadProxyFlag(): Promise<boolean> {
|
||||
try {
|
||||
const disableCFUploadProxy =
|
||||
process.env.NEXT_PUBLIC_DISABLE_CF_UPLOAD_PROXY;
|
||||
if (isDevDeployment() && typeof disableCFUploadProxy !== 'undefined') {
|
||||
return disableCFUploadProxy === 'true';
|
||||
}
|
||||
const featureFlags = (
|
||||
await fetch('https://static.ente.io/feature_flags.json')
|
||||
).json() as GetFeatureFlagResponse;
|
||||
return featureFlags.disableCFUploadProxy;
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get feature flags');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const getSRPAttributes = async (
|
||||
email: string
|
||||
): Promise<SRPAttributes | null> => {
|
||||
try {
|
||||
const resp = await HTTPService.get(`${ENDPOINT}/users/srp/attributes`, {
|
||||
email,
|
||||
});
|
||||
return (resp.data as GetSRPAttributesResponse).attributes;
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get SRP attributes');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const configureSRP = async ({
|
||||
srpSalt,
|
||||
srpUserID,
|
||||
srpVerifier,
|
||||
loginSubKey,
|
||||
}: SRPSetupAttributes) => {
|
||||
try {
|
||||
const srpConfigureInProgress = InMemoryStore.get(
|
||||
MS_KEYS.SRP_CONFIGURE_IN_PROGRESS
|
||||
);
|
||||
if (srpConfigureInProgress) {
|
||||
throw Error('SRP configure already in progress');
|
||||
}
|
||||
InMemoryStore.set(MS_KEYS.SRP_CONFIGURE_IN_PROGRESS, true);
|
||||
const srpClient = await generateSRPClient(
|
||||
srpSalt,
|
||||
srpUserID,
|
||||
loginSubKey
|
||||
);
|
||||
|
||||
const srpA = convertBufferToBase64(srpClient.computeA());
|
||||
|
||||
addLocalLog(() => `srp a: ${srpA}`);
|
||||
const token = getToken();
|
||||
const { setupID, srpB } = await startSRPSetup(token, {
|
||||
srpA,
|
||||
srpUserID,
|
||||
srpSalt,
|
||||
srpVerifier,
|
||||
});
|
||||
|
||||
srpClient.setB(convertBase64ToBuffer(srpB));
|
||||
|
||||
const srpM1 = convertBufferToBase64(srpClient.computeM1());
|
||||
|
||||
const { srpM2 } = await completeSRPSetup(token, {
|
||||
srpM1,
|
||||
setupID,
|
||||
});
|
||||
|
||||
srpClient.checkM2(convertBase64ToBuffer(srpM2));
|
||||
} catch (e) {
|
||||
logError(e, 'srp configure failed');
|
||||
throw e;
|
||||
} finally {
|
||||
InMemoryStore.set(MS_KEYS.SRP_CONFIGURE_IN_PROGRESS, false);
|
||||
}
|
||||
};
|
||||
|
||||
export const startSRPSetup = async (
|
||||
token: string,
|
||||
setupSRPRequest: SetupSRPRequest
|
||||
): Promise<SetupSRPResponse> => {
|
||||
try {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/srp/setup`,
|
||||
setupSRPRequest,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
|
||||
return resp.data as SetupSRPResponse;
|
||||
} catch (e) {
|
||||
logError(e, 'failed to post SRP attributes');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const completeSRPSetup = async (
|
||||
token: string,
|
||||
completeSRPSetupRequest: CompleteSRPSetupRequest
|
||||
) => {
|
||||
try {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/srp/complete`,
|
||||
completeSRPSetupRequest,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return resp.data as CompleteSRPSetupResponse;
|
||||
} catch (e) {
|
||||
logError(e, 'failed to complete SRP setup');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const loginViaSRP = async (
|
||||
srpAttributes: SRPAttributes,
|
||||
kek: string
|
||||
): Promise<UserVerificationResponse> => {
|
||||
try {
|
||||
const loginSubKey = await generateLoginSubKey(kek);
|
||||
const srpClient = await generateSRPClient(
|
||||
srpAttributes.srpSalt,
|
||||
srpAttributes.srpUserID,
|
||||
loginSubKey
|
||||
);
|
||||
const srpVerifier = computeVerifierHelper(
|
||||
srpAttributes.srpSalt,
|
||||
srpAttributes.srpUserID,
|
||||
loginSubKey
|
||||
);
|
||||
addLocalLog(() => `srp verifier: ${srpVerifier}`);
|
||||
const srpA = srpClient.computeA();
|
||||
const { srpB, sessionID } = await createSRPSession(
|
||||
srpAttributes.srpUserID,
|
||||
convertBufferToBase64(srpA)
|
||||
);
|
||||
srpClient.setB(convertBase64ToBuffer(srpB));
|
||||
|
||||
const m1 = srpClient.computeM1();
|
||||
addLocalLog(() => `srp m1: ${convertBufferToBase64(m1)}`);
|
||||
const { srpM2, ...rest } = await verifySRPSession(
|
||||
sessionID,
|
||||
srpAttributes.srpUserID,
|
||||
convertBufferToBase64(m1)
|
||||
);
|
||||
addLocalLog(() => `srp verify session successful,srpM2: ${srpM2}`);
|
||||
|
||||
srpClient.checkM2(convertBase64ToBuffer(srpM2));
|
||||
|
||||
addLocalLog(() => `srp server verify successful`);
|
||||
|
||||
return rest;
|
||||
} catch (e) {
|
||||
logError(e, 'srp verify failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const createSRPSession = async (srpUserID: string, srpA: string) => {
|
||||
try {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/srp/create-session`,
|
||||
{
|
||||
srpUserID,
|
||||
srpA,
|
||||
}
|
||||
);
|
||||
return resp.data as CreateSRPSessionResponse;
|
||||
} catch (e) {
|
||||
logError(e, 'createSRPSession failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const verifySRPSession = async (
|
||||
sessionID: string,
|
||||
srpUserID: string,
|
||||
srpM1: string
|
||||
) => {
|
||||
try {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/srp/verify-session`,
|
||||
{
|
||||
sessionID,
|
||||
srpUserID,
|
||||
srpM1,
|
||||
},
|
||||
null
|
||||
);
|
||||
return resp.data as UserVerificationResponse;
|
||||
} catch (e) {
|
||||
logError(e, 'verifySRPSession failed');
|
||||
if (
|
||||
e instanceof ApiError &&
|
||||
e.httpStatusCode === HttpStatusCode.Forbidden
|
||||
) {
|
||||
throw Error(CustomError.INCORRECT_PASSWORD);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSRPAndKeys = async (
|
||||
token: string,
|
||||
updateSRPAndKeyRequest: UpdateSRPAndKeysRequest
|
||||
): Promise<UpdateSRPAndKeysResponse> => {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/srp/update`,
|
||||
updateSRPAndKeyRequest,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return resp.data as UpdateSRPAndKeysResponse;
|
||||
};
|
229
apps/cast/src/styles/Home.module.css
Normal file
229
apps/cast/src/styles/Home.module.css
Normal file
|
@ -0,0 +1,229 @@
|
|||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inherit;
|
||||
justify-content: inherit;
|
||||
align-items: inherit;
|
||||
font-size: 0.85rem;
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.description a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.description p {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background-color: rgba(var(--callout-rgb), 0.5);
|
||||
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.code {
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(25%, auto));
|
||||
max-width: 100%;
|
||||
width: var(--max-width);
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
background: rgba(var(--card-rgb), 0);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0);
|
||||
transition: background 200ms, border 200ms;
|
||||
}
|
||||
|
||||
.card span {
|
||||
display: inline-block;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
max-width: 30ch;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
background: var(--secondary-glow);
|
||||
border-radius: 50%;
|
||||
width: 480px;
|
||||
height: 360px;
|
||||
margin-left: -400px;
|
||||
}
|
||||
|
||||
.center::after {
|
||||
background: var(--primary-glow);
|
||||
width: 240px;
|
||||
height: 180px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.center::before,
|
||||
.center::after {
|
||||
content: '';
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
filter: blur(45px);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: relative;
|
||||
}
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.card:hover {
|
||||
background: rgba(var(--card-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0.15);
|
||||
}
|
||||
|
||||
.card:hover span {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.card:hover span {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 700px) {
|
||||
.content {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 120px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 2.5rem;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
padding: 8rem 0 6rem;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
transform: none;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.description a {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.description p,
|
||||
.description div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description p {
|
||||
align-items: center;
|
||||
inset: 0 0 auto;
|
||||
padding: 2rem 1rem 1.4rem;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--background-start-rgb), 1),
|
||||
rgba(var(--callout-rgb), 0.5)
|
||||
);
|
||||
background-clip: padding-box;
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
|
||||
.description div {
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
inset: auto 0 0;
|
||||
padding: 2rem;
|
||||
height: 200px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
rgb(var(--background-end-rgb)) 40%
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet and Smaller Desktop */
|
||||
@media (min-width: 701px) and (max-width: 1120px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.vercelLogo {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
110
apps/cast/src/styles/globals.css
Normal file
110
apps/cast/src/styles/globals.css
Normal file
|
@ -0,0 +1,110 @@
|
|||
:root {
|
||||
--max-width: 1100px;
|
||||
--border-radius: 12px;
|
||||
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
|
||||
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
|
||||
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
|
||||
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
|
||||
--primary-glow: conic-gradient(
|
||||
from 180deg at 50% 50%,
|
||||
#16abff33 0deg,
|
||||
#0885ff33 55deg,
|
||||
#54d6ff33 120deg,
|
||||
#0071ff33 160deg,
|
||||
transparent 360deg
|
||||
);
|
||||
--secondary-glow: radial-gradient(
|
||||
rgba(255, 255, 255, 1),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
|
||||
--tile-start-rgb: 239, 245, 249;
|
||||
--tile-end-rgb: 228, 232, 233;
|
||||
--tile-border: conic-gradient(
|
||||
#00000080,
|
||||
#00000040,
|
||||
#00000030,
|
||||
#00000020,
|
||||
#00000010,
|
||||
#00000010,
|
||||
#00000080
|
||||
);
|
||||
|
||||
--callout-rgb: 238, 240, 241;
|
||||
--callout-border-rgb: 172, 175, 176;
|
||||
--card-rgb: 180, 185, 188;
|
||||
--card-border-rgb: 131, 134, 135;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
|
||||
--primary-glow: radial-gradient(
|
||||
rgba(1, 65, 255, 0.4),
|
||||
rgba(1, 65, 255, 0)
|
||||
);
|
||||
--secondary-glow: linear-gradient(
|
||||
to bottom right,
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0.3)
|
||||
);
|
||||
|
||||
--tile-start-rgb: 2, 13, 46;
|
||||
--tile-end-rgb: 2, 5, 19;
|
||||
--tile-border: conic-gradient(
|
||||
#ffffff80,
|
||||
#ffffff40,
|
||||
#ffffff30,
|
||||
#ffffff20,
|
||||
#ffffff10,
|
||||
#ffffff10,
|
||||
#ffffff80
|
||||
);
|
||||
|
||||
--callout-rgb: 20, 20, 20;
|
||||
--callout-border-rgb: 108, 108, 108;
|
||||
--card-rgb: 100, 100, 100;
|
||||
--card-border-rgb: 200, 200, 200;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
25
apps/cast/src/types/billing/index.ts
Normal file
25
apps/cast/src/types/billing/index.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { PLAN_PERIOD } from 'constants/gallery';
|
||||
|
||||
export interface Subscription {
|
||||
id: number;
|
||||
userID: number;
|
||||
productID: string;
|
||||
storage: number;
|
||||
originalTransactionID: string;
|
||||
expiryTime: number;
|
||||
paymentProvider: string;
|
||||
attributes: {
|
||||
isCancelled: boolean;
|
||||
};
|
||||
price: string;
|
||||
period: PLAN_PERIOD;
|
||||
}
|
||||
export interface Plan {
|
||||
id: string;
|
||||
androidID: string;
|
||||
iosID: string;
|
||||
storage: number;
|
||||
price: string;
|
||||
period: PLAN_PERIOD;
|
||||
stripeID: string;
|
||||
}
|
20
apps/cast/src/types/cache/index.ts
vendored
Normal file
20
apps/cast/src/types/cache/index.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
export interface LimitedCacheStorage {
|
||||
open: (cacheName: string) => Promise<LimitedCache>;
|
||||
delete: (cacheName: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface LimitedCache {
|
||||
match: (key: string) => Promise<Response>;
|
||||
put: (key: string, data: Response) => Promise<void>;
|
||||
delete: (key: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface ProxiedLimitedCacheStorage {
|
||||
open: (cacheName: string) => Promise<ProxiedWorkerLimitedCache>;
|
||||
delete: (cacheName: string) => Promise<boolean>;
|
||||
}
|
||||
export interface ProxiedWorkerLimitedCache {
|
||||
match: (key: string) => Promise<ArrayBuffer>;
|
||||
put: (key: string, data: ArrayBuffer) => Promise<void>;
|
||||
delete: (key: string) => Promise<boolean>;
|
||||
}
|
156
apps/cast/src/types/collection/index.ts
Normal file
156
apps/cast/src/types/collection/index.ts
Normal file
|
@ -0,0 +1,156 @@
|
|||
import { EnteFile } from 'types/file';
|
||||
import { CollectionSummaryType, CollectionType } from 'constants/collection';
|
||||
import {
|
||||
EncryptedMagicMetadata,
|
||||
MagicMetadataCore,
|
||||
SUB_TYPE,
|
||||
VISIBILITY_STATE,
|
||||
} from 'types/magicMetadata';
|
||||
|
||||
export enum COLLECTION_ROLE {
|
||||
VIEWER = 'VIEWER',
|
||||
OWNER = 'OWNER',
|
||||
COLLABORATOR = 'COLLABORATOR',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
}
|
||||
|
||||
export interface CollectionUser {
|
||||
id: number;
|
||||
email: string;
|
||||
role: COLLECTION_ROLE;
|
||||
}
|
||||
|
||||
export interface EncryptedCollection {
|
||||
id: number;
|
||||
owner: CollectionUser;
|
||||
// collection name was unencrypted in the past, so we need to keep it as optional
|
||||
name?: string;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
encryptedName: string;
|
||||
nameDecryptionNonce: string;
|
||||
type: CollectionType;
|
||||
attributes: collectionAttributes;
|
||||
sharees: CollectionUser[];
|
||||
publicURLs?: PublicURL[];
|
||||
updationTime: number;
|
||||
isDeleted: boolean;
|
||||
magicMetadata: EncryptedMagicMetadata;
|
||||
pubMagicMetadata: EncryptedMagicMetadata;
|
||||
sharedMagicMetadata: EncryptedMagicMetadata;
|
||||
}
|
||||
|
||||
export interface Collection
|
||||
extends Omit<
|
||||
EncryptedCollection,
|
||||
| 'encryptedKey'
|
||||
| 'keyDecryptionNonce'
|
||||
| 'encryptedName'
|
||||
| 'nameDecryptionNonce'
|
||||
| 'magicMetadata'
|
||||
| 'pubMagicMetadata'
|
||||
| 'sharedMagicMetadata'
|
||||
> {
|
||||
key: string;
|
||||
name: string;
|
||||
magicMetadata: CollectionMagicMetadata;
|
||||
pubMagicMetadata: CollectionPublicMagicMetadata;
|
||||
sharedMagicMetadata: CollectionShareeMagicMetadata;
|
||||
}
|
||||
|
||||
export interface PublicURL {
|
||||
url: string;
|
||||
deviceLimit: number;
|
||||
validTill: number;
|
||||
enableDownload: boolean;
|
||||
enableCollect: boolean;
|
||||
passwordEnabled: boolean;
|
||||
nonce?: string;
|
||||
opsLimit?: number;
|
||||
memLimit?: number;
|
||||
}
|
||||
|
||||
export interface UpdatePublicURL {
|
||||
collectionID: number;
|
||||
disablePassword?: boolean;
|
||||
enableDownload?: boolean;
|
||||
enableCollect?: boolean;
|
||||
validTill?: number;
|
||||
deviceLimit?: number;
|
||||
passHash?: string;
|
||||
nonce?: string;
|
||||
opsLimit?: number;
|
||||
memLimit?: number;
|
||||
}
|
||||
|
||||
export interface CreatePublicAccessTokenRequest {
|
||||
collectionID: number;
|
||||
validTill?: number;
|
||||
deviceLimit?: number;
|
||||
}
|
||||
|
||||
export interface EncryptedFileKey {
|
||||
id: number;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
}
|
||||
|
||||
export interface AddToCollectionRequest {
|
||||
collectionID: number;
|
||||
files: EncryptedFileKey[];
|
||||
}
|
||||
|
||||
export interface MoveToCollectionRequest {
|
||||
fromCollectionID: number;
|
||||
toCollectionID: number;
|
||||
files: EncryptedFileKey[];
|
||||
}
|
||||
|
||||
export interface collectionAttributes {
|
||||
encryptedPath?: string;
|
||||
pathDecryptionNonce?: string;
|
||||
}
|
||||
|
||||
export type CollectionToFileMap = Map<number, EnteFile>;
|
||||
|
||||
export interface RemoveFromCollectionRequest {
|
||||
collectionID: number;
|
||||
fileIDs: number[];
|
||||
}
|
||||
|
||||
export interface CollectionMagicMetadataProps {
|
||||
visibility?: VISIBILITY_STATE;
|
||||
subType?: SUB_TYPE;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
export type CollectionMagicMetadata =
|
||||
MagicMetadataCore<CollectionMagicMetadataProps>;
|
||||
|
||||
export interface CollectionShareeMetadataProps {
|
||||
visibility?: VISIBILITY_STATE;
|
||||
}
|
||||
export type CollectionShareeMagicMetadata =
|
||||
MagicMetadataCore<CollectionShareeMetadataProps>;
|
||||
|
||||
export interface CollectionPublicMagicMetadataProps {
|
||||
asc?: boolean;
|
||||
coverID?: number;
|
||||
}
|
||||
|
||||
export type CollectionPublicMagicMetadata =
|
||||
MagicMetadataCore<CollectionPublicMagicMetadataProps>;
|
||||
|
||||
export interface CollectionSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
type: CollectionSummaryType;
|
||||
coverFile: EnteFile;
|
||||
latestFile: EnteFile;
|
||||
fileCount: number;
|
||||
updationTime: number;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
export type CollectionSummaries = Map<number, CollectionSummary>;
|
||||
export type CollectionFilesCount = Map<number, number>;
|
19
apps/cast/src/types/crypto/index.ts
Normal file
19
apps/cast/src/types/crypto/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { DataStream } from 'types/upload';
|
||||
|
||||
export interface LocalFileAttributes<
|
||||
T extends string | Uint8Array | DataStream
|
||||
> {
|
||||
encryptedData: T;
|
||||
decryptionHeader: string;
|
||||
}
|
||||
|
||||
export interface EncryptionResult<T extends string | Uint8Array | DataStream> {
|
||||
file: LocalFileAttributes<T>;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface B64EncryptionResult {
|
||||
encryptedData: string;
|
||||
key: string;
|
||||
nonce: string;
|
||||
}
|
422
apps/cast/src/types/crypto/libsodium.ts
Normal file
422
apps/cast/src/types/crypto/libsodium.ts
Normal file
|
@ -0,0 +1,422 @@
|
|||
import sodium, { StateAddress } from 'libsodium-wrappers';
|
||||
import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto';
|
||||
import { B64EncryptionResult } from 'types/crypto';
|
||||
import { CustomError } from 'utils/error';
|
||||
|
||||
export async function decryptChaChaOneShot(
|
||||
data: Uint8Array,
|
||||
header: Uint8Array,
|
||||
key: string
|
||||
) {
|
||||
await sodium.ready;
|
||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
|
||||
header,
|
||||
await fromB64(key)
|
||||
);
|
||||
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||
pullState,
|
||||
data,
|
||||
null
|
||||
);
|
||||
return pullResult.message;
|
||||
}
|
||||
|
||||
export async function decryptChaCha(
|
||||
data: Uint8Array,
|
||||
header: Uint8Array,
|
||||
key: string
|
||||
) {
|
||||
await sodium.ready;
|
||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
|
||||
header,
|
||||
await fromB64(key)
|
||||
);
|
||||
const decryptionChunkSize =
|
||||
ENCRYPTION_CHUNK_SIZE +
|
||||
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
||||
let bytesRead = 0;
|
||||
const decryptedData = [];
|
||||
let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
while (tag !== sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
|
||||
let chunkSize = decryptionChunkSize;
|
||||
if (bytesRead + chunkSize > data.length) {
|
||||
chunkSize = data.length - bytesRead;
|
||||
}
|
||||
const buffer = data.slice(bytesRead, bytesRead + chunkSize);
|
||||
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||
pullState,
|
||||
buffer
|
||||
);
|
||||
if (!pullResult.message) {
|
||||
throw new Error(CustomError.PROCESSING_FAILED);
|
||||
}
|
||||
for (let index = 0; index < pullResult.message.length; index++) {
|
||||
decryptedData.push(pullResult.message[index]);
|
||||
}
|
||||
tag = pullResult.tag;
|
||||
bytesRead += chunkSize;
|
||||
}
|
||||
return Uint8Array.from(decryptedData);
|
||||
}
|
||||
|
||||
export async function initChunkDecryption(header: Uint8Array, key: Uint8Array) {
|
||||
await sodium.ready;
|
||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
|
||||
header,
|
||||
key
|
||||
);
|
||||
const decryptionChunkSize =
|
||||
ENCRYPTION_CHUNK_SIZE +
|
||||
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
||||
const tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
return { pullState, decryptionChunkSize, tag };
|
||||
}
|
||||
|
||||
export async function decryptFileChunk(
|
||||
data: Uint8Array,
|
||||
pullState: StateAddress
|
||||
) {
|
||||
await sodium.ready;
|
||||
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||
pullState,
|
||||
data
|
||||
);
|
||||
if (!pullResult.message) {
|
||||
throw new Error(CustomError.PROCESSING_FAILED);
|
||||
}
|
||||
const newTag = pullResult.tag;
|
||||
return { decryptedData: pullResult.message, newTag };
|
||||
}
|
||||
|
||||
export async function encryptChaChaOneShot(data: Uint8Array, key: string) {
|
||||
await sodium.ready;
|
||||
|
||||
const uintkey: Uint8Array = await fromB64(key);
|
||||
const initPushResult =
|
||||
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
|
||||
const [pushState, header] = [initPushResult.state, initPushResult.header];
|
||||
|
||||
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
|
||||
pushState,
|
||||
data,
|
||||
null,
|
||||
sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
|
||||
);
|
||||
return {
|
||||
key: await toB64(uintkey),
|
||||
file: {
|
||||
encryptedData: pushResult,
|
||||
decryptionHeader: await toB64(header),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptChaCha(data: Uint8Array) {
|
||||
await sodium.ready;
|
||||
|
||||
const uintkey: Uint8Array =
|
||||
sodium.crypto_secretstream_xchacha20poly1305_keygen();
|
||||
|
||||
const initPushResult =
|
||||
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
|
||||
const [pushState, header] = [initPushResult.state, initPushResult.header];
|
||||
let bytesRead = 0;
|
||||
let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
|
||||
const encryptedData = [];
|
||||
|
||||
while (tag !== sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
|
||||
let chunkSize = ENCRYPTION_CHUNK_SIZE;
|
||||
if (bytesRead + chunkSize >= data.length) {
|
||||
chunkSize = data.length - bytesRead;
|
||||
tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;
|
||||
}
|
||||
|
||||
const buffer = data.slice(bytesRead, bytesRead + chunkSize);
|
||||
bytesRead += chunkSize;
|
||||
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
|
||||
pushState,
|
||||
buffer,
|
||||
null,
|
||||
tag
|
||||
);
|
||||
for (let index = 0; index < pushResult.length; index++) {
|
||||
encryptedData.push(pushResult[index]);
|
||||
}
|
||||
}
|
||||
return {
|
||||
key: await toB64(uintkey),
|
||||
file: {
|
||||
encryptedData: new Uint8Array(encryptedData),
|
||||
decryptionHeader: await toB64(header),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function initChunkEncryption() {
|
||||
await sodium.ready;
|
||||
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
|
||||
const initPushResult =
|
||||
sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
|
||||
const [pushState, header] = [initPushResult.state, initPushResult.header];
|
||||
return {
|
||||
key: await toB64(key),
|
||||
decryptionHeader: await toB64(header),
|
||||
pushState,
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptFileChunk(
|
||||
data: Uint8Array,
|
||||
pushState: sodium.StateAddress,
|
||||
isFinalChunk: boolean
|
||||
) {
|
||||
await sodium.ready;
|
||||
const tag = isFinalChunk
|
||||
? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
|
||||
: sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
|
||||
pushState,
|
||||
data,
|
||||
null,
|
||||
tag
|
||||
);
|
||||
|
||||
return pushResult;
|
||||
}
|
||||
export async function encryptToB64(data: string, key: string) {
|
||||
await sodium.ready;
|
||||
const encrypted = await encrypt(await fromB64(data), await fromB64(key));
|
||||
|
||||
return {
|
||||
encryptedData: await toB64(encrypted.encryptedData),
|
||||
key: await toB64(encrypted.key),
|
||||
nonce: await toB64(encrypted.nonce),
|
||||
} as B64EncryptionResult;
|
||||
}
|
||||
|
||||
export async function generateKeyAndEncryptToB64(data: string) {
|
||||
await sodium.ready;
|
||||
const key = sodium.crypto_secretbox_keygen();
|
||||
return await encryptToB64(data, await toB64(key));
|
||||
}
|
||||
|
||||
export async function encryptUTF8(data: string, key: string) {
|
||||
const b64Data = await toB64(await fromUTF8(data));
|
||||
return await encryptToB64(b64Data, key);
|
||||
}
|
||||
|
||||
export async function decryptB64(data: string, nonce: string, key: string) {
|
||||
await sodium.ready;
|
||||
const decrypted = await decrypt(
|
||||
await fromB64(data),
|
||||
await fromB64(nonce),
|
||||
await fromB64(key)
|
||||
);
|
||||
|
||||
return await toB64(decrypted);
|
||||
}
|
||||
|
||||
export async function decryptToUTF8(data: string, nonce: string, key: string) {
|
||||
await sodium.ready;
|
||||
const decrypted = await decrypt(
|
||||
await fromB64(data),
|
||||
await fromB64(nonce),
|
||||
await fromB64(key)
|
||||
);
|
||||
|
||||
return sodium.to_string(decrypted);
|
||||
}
|
||||
|
||||
async function encrypt(data: Uint8Array, key: Uint8Array) {
|
||||
await sodium.ready;
|
||||
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const encryptedData = sodium.crypto_secretbox_easy(data, nonce, key);
|
||||
return {
|
||||
encryptedData,
|
||||
key,
|
||||
nonce,
|
||||
};
|
||||
}
|
||||
|
||||
async function decrypt(data: Uint8Array, nonce: Uint8Array, key: Uint8Array) {
|
||||
await sodium.ready;
|
||||
return sodium.crypto_secretbox_open_easy(data, nonce, key);
|
||||
}
|
||||
|
||||
export async function initChunkHashing() {
|
||||
await sodium.ready;
|
||||
const hashState = sodium.crypto_generichash_init(
|
||||
null,
|
||||
sodium.crypto_generichash_BYTES_MAX
|
||||
);
|
||||
return hashState;
|
||||
}
|
||||
|
||||
export async function hashFileChunk(
|
||||
hashState: sodium.StateAddress,
|
||||
chunk: Uint8Array
|
||||
) {
|
||||
await sodium.ready;
|
||||
sodium.crypto_generichash_update(hashState, chunk);
|
||||
}
|
||||
|
||||
export async function completeChunkHashing(hashState: sodium.StateAddress) {
|
||||
await sodium.ready;
|
||||
const hash = sodium.crypto_generichash_final(
|
||||
hashState,
|
||||
sodium.crypto_generichash_BYTES_MAX
|
||||
);
|
||||
const hashString = toB64(hash);
|
||||
return hashString;
|
||||
}
|
||||
|
||||
export async function deriveKey(
|
||||
passphrase: string,
|
||||
salt: string,
|
||||
opsLimit: number,
|
||||
memLimit: number
|
||||
) {
|
||||
await sodium.ready;
|
||||
return await toB64(
|
||||
sodium.crypto_pwhash(
|
||||
sodium.crypto_secretbox_KEYBYTES,
|
||||
await fromUTF8(passphrase),
|
||||
await fromB64(salt),
|
||||
opsLimit,
|
||||
memLimit,
|
||||
sodium.crypto_pwhash_ALG_ARGON2ID13
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function deriveSensitiveKey(passphrase: string, salt: string) {
|
||||
await sodium.ready;
|
||||
const minMemLimit = sodium.crypto_pwhash_MEMLIMIT_MIN;
|
||||
let opsLimit = sodium.crypto_pwhash_OPSLIMIT_SENSITIVE;
|
||||
let memLimit = sodium.crypto_pwhash_MEMLIMIT_SENSITIVE;
|
||||
while (memLimit > minMemLimit) {
|
||||
try {
|
||||
const key = await deriveKey(passphrase, salt, opsLimit, memLimit);
|
||||
return {
|
||||
key,
|
||||
opsLimit,
|
||||
memLimit,
|
||||
};
|
||||
} catch (e) {
|
||||
opsLimit *= 2;
|
||||
memLimit /= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deriveInteractiveKey(passphrase: string, salt: string) {
|
||||
await sodium.ready;
|
||||
const key = await toB64(
|
||||
sodium.crypto_pwhash(
|
||||
sodium.crypto_secretbox_KEYBYTES,
|
||||
await fromUTF8(passphrase),
|
||||
await fromB64(salt),
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_ARGON2ID13
|
||||
)
|
||||
);
|
||||
return {
|
||||
key,
|
||||
opsLimit: sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
memLimit: sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateEncryptionKey() {
|
||||
await sodium.ready;
|
||||
return await toB64(sodium.crypto_kdf_keygen());
|
||||
}
|
||||
|
||||
export async function generateSaltToDeriveKey() {
|
||||
await sodium.ready;
|
||||
return await toB64(sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES));
|
||||
}
|
||||
|
||||
export async function generateKeyPair() {
|
||||
await sodium.ready;
|
||||
const keyPair: sodium.KeyPair = sodium.crypto_box_keypair();
|
||||
return {
|
||||
privateKey: await toB64(keyPair.privateKey),
|
||||
publicKey: await toB64(keyPair.publicKey),
|
||||
};
|
||||
}
|
||||
|
||||
export async function boxSealOpen(
|
||||
input: string,
|
||||
publicKey: string,
|
||||
secretKey: string
|
||||
) {
|
||||
await sodium.ready;
|
||||
return await toB64(
|
||||
sodium.crypto_box_seal_open(
|
||||
await fromB64(input),
|
||||
await fromB64(publicKey),
|
||||
await fromB64(secretKey)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function boxSeal(input: string, publicKey: string) {
|
||||
await sodium.ready;
|
||||
return await toB64(
|
||||
sodium.crypto_box_seal(await fromB64(input), await fromB64(publicKey))
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateSubKey(
|
||||
key: string,
|
||||
subKeyLength: number,
|
||||
subKeyID: number,
|
||||
context: string
|
||||
) {
|
||||
await sodium.ready;
|
||||
return await toB64(
|
||||
sodium.crypto_kdf_derive_from_key(
|
||||
subKeyLength,
|
||||
subKeyID,
|
||||
context,
|
||||
await fromB64(key)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function fromB64(input: string) {
|
||||
await sodium.ready;
|
||||
return sodium.from_base64(input, sodium.base64_variants.ORIGINAL);
|
||||
}
|
||||
|
||||
export async function toB64(input: Uint8Array) {
|
||||
await sodium.ready;
|
||||
return sodium.to_base64(input, sodium.base64_variants.ORIGINAL);
|
||||
}
|
||||
|
||||
export async function toURLSafeB64(input: Uint8Array) {
|
||||
await sodium.ready;
|
||||
return sodium.to_base64(input, sodium.base64_variants.URLSAFE);
|
||||
}
|
||||
|
||||
export async function fromUTF8(input: string) {
|
||||
await sodium.ready;
|
||||
return sodium.from_string(input);
|
||||
}
|
||||
|
||||
export async function toUTF8(input: string) {
|
||||
await sodium.ready;
|
||||
return sodium.to_string(await fromB64(input));
|
||||
}
|
||||
export async function toHex(input: string) {
|
||||
await sodium.ready;
|
||||
return sodium.to_hex(await fromB64(input));
|
||||
}
|
||||
|
||||
export async function fromHex(input: string) {
|
||||
await sodium.ready;
|
||||
return await toB64(sodium.from_hex(input));
|
||||
}
|
103
apps/cast/src/types/file/index.ts
Normal file
103
apps/cast/src/types/file/index.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import {
|
||||
EncryptedMagicMetadata,
|
||||
MagicMetadataCore,
|
||||
VISIBILITY_STATE,
|
||||
} from 'types/magicMetadata';
|
||||
import { Metadata } from 'types/upload';
|
||||
|
||||
export interface MetadataFileAttributes {
|
||||
encryptedData: string;
|
||||
decryptionHeader: string;
|
||||
}
|
||||
export interface S3FileAttributes {
|
||||
objectKey: string;
|
||||
decryptionHeader: string;
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
fileSize: number;
|
||||
thumbSize: number;
|
||||
}
|
||||
|
||||
export interface EncryptedEnteFile {
|
||||
id: number;
|
||||
collectionID: number;
|
||||
ownerID: number;
|
||||
file: S3FileAttributes;
|
||||
thumbnail: S3FileAttributes;
|
||||
metadata: MetadataFileAttributes;
|
||||
info: FileInfo;
|
||||
magicMetadata: EncryptedMagicMetadata;
|
||||
pubMagicMetadata: EncryptedMagicMetadata;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
isDeleted: boolean;
|
||||
updationTime: number;
|
||||
}
|
||||
|
||||
export interface EnteFile
|
||||
extends Omit<
|
||||
EncryptedEnteFile,
|
||||
| 'metadata'
|
||||
| 'pubMagicMetadata'
|
||||
| 'magicMetadata'
|
||||
| 'encryptedKey'
|
||||
| 'keyDecryptionNonce'
|
||||
> {
|
||||
metadata: Metadata;
|
||||
magicMetadata: FileMagicMetadata;
|
||||
pubMagicMetadata: FilePublicMagicMetadata;
|
||||
isTrashed?: boolean;
|
||||
key: string;
|
||||
src?: string;
|
||||
msrc?: string;
|
||||
html?: string;
|
||||
w?: number;
|
||||
h?: number;
|
||||
title?: string;
|
||||
deleteBy?: number;
|
||||
isSourceLoaded?: boolean;
|
||||
originalVideoURL?: string;
|
||||
originalImageURL?: string;
|
||||
dataIndex?: number;
|
||||
conversionFailed?: boolean;
|
||||
isConverted?: boolean;
|
||||
}
|
||||
|
||||
export interface TrashRequest {
|
||||
items: TrashRequestItems[];
|
||||
}
|
||||
|
||||
export interface TrashRequestItems {
|
||||
fileID: number;
|
||||
collectionID: number;
|
||||
}
|
||||
|
||||
export interface FileWithUpdatedMagicMetadata {
|
||||
file: EnteFile;
|
||||
updatedMagicMetadata: FileMagicMetadata;
|
||||
}
|
||||
|
||||
export interface FileWithUpdatedPublicMagicMetadata {
|
||||
file: EnteFile;
|
||||
updatedPublicMagicMetadata: FilePublicMagicMetadata;
|
||||
}
|
||||
|
||||
export interface FileMagicMetadataProps {
|
||||
visibility?: VISIBILITY_STATE;
|
||||
filePaths?: string[];
|
||||
}
|
||||
|
||||
export type FileMagicMetadata = MagicMetadataCore<FileMagicMetadataProps>;
|
||||
|
||||
export interface FilePublicMagicMetadataProps {
|
||||
editedTime?: number;
|
||||
editedName?: string;
|
||||
caption?: string;
|
||||
uploaderName?: string;
|
||||
w?: number;
|
||||
h?: number;
|
||||
}
|
||||
|
||||
export type FilePublicMagicMetadata =
|
||||
MagicMetadataCore<FilePublicMagicMetadataProps>;
|
57
apps/cast/src/types/gallery/index.ts
Normal file
57
apps/cast/src/types/gallery/index.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
// import { CollectionDownloadProgressAttributes } from 'components/Collections/CollectionDownloadProgress';
|
||||
// import { CollectionSelectorAttributes } from 'components/Collections/CollectionSelector';
|
||||
// import { TimeStampListItem } from 'components/PhotoList';
|
||||
import { Collection } from 'types/collection';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { User } from 'types/user';
|
||||
|
||||
export type SelectedState = {
|
||||
[k: number]: boolean;
|
||||
ownCount: number;
|
||||
count: number;
|
||||
collectionID: number;
|
||||
};
|
||||
export type SetFiles = React.Dispatch<React.SetStateAction<EnteFile[]>>;
|
||||
export type SetCollections = React.Dispatch<React.SetStateAction<Collection[]>>;
|
||||
export type SetLoading = React.Dispatch<React.SetStateAction<boolean>>;
|
||||
// export type SetCollectionSelectorAttributes = React.Dispatch<
|
||||
// React.SetStateAction<CollectionSelectorAttributes>
|
||||
// >;
|
||||
// export type SetCollectionDownloadProgressAttributes = React.Dispatch<
|
||||
// React.SetStateAction<CollectionDownloadProgressAttributes>
|
||||
// >;
|
||||
|
||||
export type MergedSourceURL = {
|
||||
original: string;
|
||||
converted: string;
|
||||
};
|
||||
export enum UploadTypeSelectorIntent {
|
||||
normalUpload,
|
||||
import,
|
||||
collectPhotos,
|
||||
}
|
||||
export type GalleryContextType = {
|
||||
thumbs: Map<number, string>;
|
||||
files: Map<number, MergedSourceURL>;
|
||||
showPlanSelectorModal: () => void;
|
||||
setActiveCollectionID: (collectionID: number) => void;
|
||||
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
|
||||
setBlockingLoad: (value: boolean) => void;
|
||||
setIsInSearchMode: (value: boolean) => void;
|
||||
// photoListHeader: TimeStampListItem;
|
||||
openExportModal: () => void;
|
||||
authenticateUser: (callback: () => void) => void;
|
||||
user: User;
|
||||
userIDToEmailMap: Map<number, string>;
|
||||
emailList: string[];
|
||||
openHiddenSection: (callback?: () => void) => void;
|
||||
isClipSearchResult: boolean;
|
||||
};
|
||||
|
||||
export enum CollectionSelectorIntent {
|
||||
upload,
|
||||
add,
|
||||
move,
|
||||
restore,
|
||||
unhide,
|
||||
}
|
29
apps/cast/src/types/magicMetadata/index.ts
Normal file
29
apps/cast/src/types/magicMetadata/index.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
export interface MagicMetadataCore<T> {
|
||||
version: number;
|
||||
count: number;
|
||||
header: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export type EncryptedMagicMetadata = MagicMetadataCore<string>;
|
||||
|
||||
export enum VISIBILITY_STATE {
|
||||
VISIBLE = 0,
|
||||
ARCHIVED = 1,
|
||||
HIDDEN = 2,
|
||||
}
|
||||
|
||||
export enum SUB_TYPE {
|
||||
DEFAULT = 0,
|
||||
DEFAULT_HIDDEN = 1,
|
||||
QUICK_LINK_COLLECTION = 2,
|
||||
}
|
||||
|
||||
export interface BulkUpdateMagicMetadataRequest {
|
||||
metadataList: UpdateMagicMetadataRequest[];
|
||||
}
|
||||
|
||||
export interface UpdateMagicMetadataRequest {
|
||||
id: number;
|
||||
magicMetadata: EncryptedMagicMetadata;
|
||||
}
|
167
apps/cast/src/types/upload/index.ts
Normal file
167
apps/cast/src/types/upload/index.ts
Normal file
|
@ -0,0 +1,167 @@
|
|||
import { FILE_TYPE } from 'constants/file';
|
||||
import { Collection } from 'types/collection';
|
||||
import { B64EncryptionResult, LocalFileAttributes } from 'types/crypto';
|
||||
import {
|
||||
MetadataFileAttributes,
|
||||
S3FileAttributes,
|
||||
FilePublicMagicMetadata,
|
||||
FilePublicMagicMetadataProps,
|
||||
} from 'types/file';
|
||||
import { EncryptedMagicMetadata } from 'types/magicMetadata';
|
||||
|
||||
export interface DataStream {
|
||||
stream: ReadableStream<Uint8Array>;
|
||||
chunkCount: number;
|
||||
}
|
||||
|
||||
export function isDataStream(object: any): object is DataStream {
|
||||
return 'stream' in object;
|
||||
}
|
||||
|
||||
export type Logger = (message: string) => void;
|
||||
|
||||
export interface Metadata {
|
||||
title: string;
|
||||
creationTime: number;
|
||||
modificationTime: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
fileType: FILE_TYPE;
|
||||
hasStaticThumbnail?: boolean;
|
||||
hash?: string;
|
||||
imageHash?: string;
|
||||
videoHash?: string;
|
||||
localID?: number;
|
||||
version?: number;
|
||||
deviceFolder?: string;
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface ParsedMetadataJSON {
|
||||
creationTime: number;
|
||||
modificationTime: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface MultipartUploadURLs {
|
||||
objectKey: string;
|
||||
partURLs: string[];
|
||||
completeURL: string;
|
||||
}
|
||||
|
||||
export interface FileTypeInfo {
|
||||
fileType: FILE_TYPE;
|
||||
exactType: string;
|
||||
mimeType?: string;
|
||||
imageType?: string;
|
||||
videoType?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* ElectronFile is a custom interface that is used to represent
|
||||
* any file on disk as a File-like object in the Electron desktop app.
|
||||
*
|
||||
* This was added to support the auto-resuming of failed uploads
|
||||
* which needed absolute paths to the files which the
|
||||
* normal File interface does not provide.
|
||||
*/
|
||||
export interface ElectronFile {
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
lastModified: number;
|
||||
stream: () => Promise<ReadableStream<Uint8Array>>;
|
||||
blob: () => Promise<Blob>;
|
||||
arrayBuffer: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
export interface UploadAsset {
|
||||
isLivePhoto?: boolean;
|
||||
file?: File | ElectronFile;
|
||||
livePhotoAssets?: LivePhotoAssets;
|
||||
isElectron?: boolean;
|
||||
}
|
||||
export interface LivePhotoAssets {
|
||||
image: globalThis.File | ElectronFile;
|
||||
video: globalThis.File | ElectronFile;
|
||||
}
|
||||
|
||||
export interface FileWithCollection extends UploadAsset {
|
||||
localID: number;
|
||||
collection?: Collection;
|
||||
collectionID?: number;
|
||||
}
|
||||
|
||||
export type ParsedMetadataJSONMap = Map<string, ParsedMetadataJSON>;
|
||||
|
||||
export interface UploadURL {
|
||||
url: string;
|
||||
objectKey: string;
|
||||
}
|
||||
|
||||
export interface FileInMemory {
|
||||
filedata: Uint8Array | DataStream;
|
||||
thumbnail: Uint8Array;
|
||||
hasStaticThumbnail: boolean;
|
||||
}
|
||||
|
||||
export interface FileWithMetadata
|
||||
extends Omit<FileInMemory, 'hasStaticThumbnail'> {
|
||||
metadata: Metadata;
|
||||
localID: number;
|
||||
pubMagicMetadata: FilePublicMagicMetadata;
|
||||
}
|
||||
|
||||
export interface EncryptedFile {
|
||||
file: ProcessedFile;
|
||||
fileKey: B64EncryptionResult;
|
||||
}
|
||||
export interface ProcessedFile {
|
||||
file: LocalFileAttributes<Uint8Array | DataStream>;
|
||||
thumbnail: LocalFileAttributes<Uint8Array>;
|
||||
metadata: LocalFileAttributes<string>;
|
||||
pubMagicMetadata: EncryptedMagicMetadata;
|
||||
localID: number;
|
||||
}
|
||||
export interface BackupedFile {
|
||||
file: S3FileAttributes;
|
||||
thumbnail: S3FileAttributes;
|
||||
metadata: MetadataFileAttributes;
|
||||
pubMagicMetadata: EncryptedMagicMetadata;
|
||||
}
|
||||
|
||||
export interface UploadFile extends BackupedFile {
|
||||
collectionID: number;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
}
|
||||
|
||||
export interface ParsedExtractedMetadata {
|
||||
location: Location;
|
||||
creationTime: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
// This is used to prompt the user the make upload strategy choice
|
||||
export interface ImportSuggestion {
|
||||
rootFolderName: string;
|
||||
hasNestedFolders: boolean;
|
||||
hasRootLevelFileWithFolder: boolean;
|
||||
}
|
||||
|
||||
export interface PublicUploadProps {
|
||||
token: string;
|
||||
passwordToken: string;
|
||||
accessedThroughSharedURL: boolean;
|
||||
}
|
||||
|
||||
export interface ExtractMetadataResult {
|
||||
metadata: Metadata;
|
||||
publicMagicMetadata: FilePublicMagicMetadataProps;
|
||||
}
|
43
apps/cast/src/types/upload/ui.ts
Normal file
43
apps/cast/src/types/upload/ui.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { UPLOAD_RESULT, UPLOAD_STAGES } from 'constants/upload';
|
||||
|
||||
export type FileID = number;
|
||||
export type FileName = string;
|
||||
|
||||
export type PercentageUploaded = number;
|
||||
export type UploadFileNames = Map<FileID, FileName>;
|
||||
|
||||
export interface UploadCounter {
|
||||
finished: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface InProgressUpload {
|
||||
localFileID: FileID;
|
||||
progress: PercentageUploaded;
|
||||
}
|
||||
|
||||
export interface FinishedUpload {
|
||||
localFileID: FileID;
|
||||
result: UPLOAD_RESULT;
|
||||
}
|
||||
|
||||
export type InProgressUploads = Map<FileID, PercentageUploaded>;
|
||||
|
||||
export type FinishedUploads = Map<FileID, UPLOAD_RESULT>;
|
||||
|
||||
export type SegregatedFinishedUploads = Map<UPLOAD_RESULT, FileID[]>;
|
||||
|
||||
export interface ProgressUpdater {
|
||||
setPercentComplete: React.Dispatch<React.SetStateAction<number>>;
|
||||
setUploadCounter: React.Dispatch<React.SetStateAction<UploadCounter>>;
|
||||
setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>;
|
||||
setInProgressUploads: React.Dispatch<
|
||||
React.SetStateAction<InProgressUpload[]>
|
||||
>;
|
||||
setFinishedUploads: React.Dispatch<
|
||||
React.SetStateAction<SegregatedFinishedUploads>
|
||||
>;
|
||||
setUploadFilenames: React.Dispatch<React.SetStateAction<UploadFileNames>>;
|
||||
setHasLivePhotos: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setUploadProgressView: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
168
apps/cast/src/types/user/index.ts
Normal file
168
apps/cast/src/types/user/index.ts
Normal file
|
@ -0,0 +1,168 @@
|
|||
import { Subscription } from 'types/billing';
|
||||
|
||||
export interface KeyAttributes {
|
||||
kekSalt: string;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
opsLimit: number;
|
||||
memLimit: number;
|
||||
publicKey: string;
|
||||
encryptedSecretKey: string;
|
||||
secretKeyDecryptionNonce: string;
|
||||
masterKeyEncryptedWithRecoveryKey: string;
|
||||
masterKeyDecryptionNonce: string;
|
||||
recoveryKeyEncryptedWithMasterKey: string;
|
||||
recoveryKeyDecryptionNonce: string;
|
||||
}
|
||||
export interface KEK {
|
||||
key: string;
|
||||
opsLimit: number;
|
||||
memLimit: number;
|
||||
}
|
||||
|
||||
export interface UpdatedKey {
|
||||
kekSalt: string;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
memLimit: number;
|
||||
opsLimit: number;
|
||||
}
|
||||
|
||||
export interface UpdateSRPAndKeysRequest {
|
||||
srpM1: string;
|
||||
setupID: string;
|
||||
updatedKeyAttr: UpdatedKey;
|
||||
}
|
||||
|
||||
export interface UpdateSRPAndKeysResponse {
|
||||
srpM2: string;
|
||||
setupID: string;
|
||||
}
|
||||
|
||||
export interface RecoveryKey {
|
||||
masterKeyEncryptedWithRecoveryKey: string;
|
||||
masterKeyDecryptionNonce: string;
|
||||
recoveryKeyEncryptedWithMasterKey: string;
|
||||
recoveryKeyDecryptionNonce: string;
|
||||
}
|
||||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
token: string;
|
||||
encryptedToken: string;
|
||||
isTwoFactorEnabled: boolean;
|
||||
twoFactorSessionID: string;
|
||||
}
|
||||
export interface UserVerificationResponse {
|
||||
id: number;
|
||||
keyAttributes?: KeyAttributes;
|
||||
encryptedToken?: string;
|
||||
token?: string;
|
||||
twoFactorSessionID: string;
|
||||
srpM2?: string;
|
||||
}
|
||||
|
||||
export interface TwoFactorVerificationResponse {
|
||||
id: number;
|
||||
keyAttributes: KeyAttributes;
|
||||
encryptedToken?: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export interface TwoFactorSecret {
|
||||
secretCode: string;
|
||||
qrCode: string;
|
||||
}
|
||||
|
||||
export interface TwoFactorRecoveryResponse {
|
||||
encryptedSecret: string;
|
||||
secretDecryptionNonce: string;
|
||||
}
|
||||
|
||||
export interface FamilyMember {
|
||||
email: string;
|
||||
usage: number;
|
||||
id: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export interface FamilyData {
|
||||
storage: number;
|
||||
expiry: number;
|
||||
members: FamilyMember[];
|
||||
}
|
||||
|
||||
export interface UserDetails {
|
||||
email: string;
|
||||
usage: number;
|
||||
fileCount: number;
|
||||
sharedCollectionCount: number;
|
||||
subscription: Subscription;
|
||||
familyData?: FamilyData;
|
||||
storageBonus?: number;
|
||||
}
|
||||
|
||||
export interface DeleteChallengeResponse {
|
||||
allowDelete: boolean;
|
||||
encryptedChallenge: string;
|
||||
}
|
||||
|
||||
export interface GetRemoteStoreValueResponse {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface UpdateRemoteStoreValueRequest {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SRPAttributes {
|
||||
srpUserID: string;
|
||||
srpSalt: string;
|
||||
memLimit: number;
|
||||
opsLimit: number;
|
||||
kekSalt: string;
|
||||
isEmailMFAEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface GetSRPAttributesResponse {
|
||||
attributes: SRPAttributes;
|
||||
}
|
||||
|
||||
export interface SRPSetupAttributes {
|
||||
srpSalt: string;
|
||||
srpVerifier: string;
|
||||
srpUserID: string;
|
||||
loginSubKey: string;
|
||||
}
|
||||
|
||||
export interface SetupSRPRequest {
|
||||
srpUserID: string;
|
||||
srpSalt: string;
|
||||
srpVerifier: string;
|
||||
srpA: string;
|
||||
}
|
||||
|
||||
export interface SetupSRPResponse {
|
||||
setupID: string;
|
||||
srpB: string;
|
||||
}
|
||||
|
||||
export interface CompleteSRPSetupRequest {
|
||||
setupID: string;
|
||||
srpM1: string;
|
||||
}
|
||||
|
||||
export interface CompleteSRPSetupResponse {
|
||||
setupID: string;
|
||||
srpM2: string;
|
||||
}
|
||||
|
||||
export interface CreateSRPSessionResponse {
|
||||
sessionID: string;
|
||||
srpB: string;
|
||||
}
|
||||
|
||||
export interface GetFeatureFlagResponse {
|
||||
disableCFUploadProxy?: boolean;
|
||||
}
|
617
apps/cast/src/utils/collection/index.ts
Normal file
617
apps/cast/src/utils/collection/index.ts
Normal file
|
@ -0,0 +1,617 @@
|
|||
import {
|
||||
addToCollection,
|
||||
createAlbum,
|
||||
getAllLocalCollections,
|
||||
getLocalCollections,
|
||||
getNonEmptyCollections,
|
||||
moveToCollection,
|
||||
removeFromCollection,
|
||||
restoreToCollection,
|
||||
unhideToCollection,
|
||||
updateCollectionMagicMetadata,
|
||||
updatePublicCollectionMagicMetadata,
|
||||
updateSharedCollectionMagicMetadata,
|
||||
} from 'services/collectionService';
|
||||
import { downloadFiles, downloadFilesDesktop } from 'utils/file';
|
||||
import { getAllLocalFiles, getLocalFiles } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { User } from 'types/user';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { logError } from 'utils/sentry';
|
||||
import {
|
||||
COLLECTION_ROLE,
|
||||
Collection,
|
||||
CollectionMagicMetadataProps,
|
||||
CollectionPublicMagicMetadataProps,
|
||||
CollectionSummaries,
|
||||
} from 'types/collection';
|
||||
import {
|
||||
CollectionSummaryType,
|
||||
CollectionType,
|
||||
HIDE_FROM_COLLECTION_BAR_TYPES,
|
||||
OPTIONS_NOT_HAVING_COLLECTION_TYPES,
|
||||
SYSTEM_COLLECTION_TYPES,
|
||||
MOVE_TO_NOT_ALLOWED_COLLECTION,
|
||||
ADD_TO_NOT_ALLOWED_COLLECTION,
|
||||
HIDDEN_ITEMS_SECTION,
|
||||
DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME,
|
||||
} from 'constants/collection';
|
||||
import { getUnixTimeInMicroSecondsWithDelta } from 'utils/time';
|
||||
import { SUB_TYPE, VISIBILITY_STATE } from 'types/magicMetadata';
|
||||
import { isArchivedCollection, updateMagicMetadata } from 'utils/magicMetadata';
|
||||
import { getAlbumsURL } from 'utils/common/apiUtil';
|
||||
import bs58 from 'bs58';
|
||||
import { t } from 'i18next';
|
||||
import isElectron from 'is-electron';
|
||||
import { SetCollectionDownloadProgressAttributes } from 'types/gallery';
|
||||
import ElectronService from 'services/electron/common';
|
||||
import {
|
||||
getCollectionExportPath,
|
||||
getUniqueCollectionExportName,
|
||||
} from 'utils/export';
|
||||
import exportService from 'services/export';
|
||||
import { CollectionDownloadProgressAttributes } from 'components/Collections/CollectionDownloadProgress';
|
||||
|
||||
export enum COLLECTION_OPS_TYPE {
|
||||
ADD,
|
||||
MOVE,
|
||||
REMOVE,
|
||||
RESTORE,
|
||||
UNHIDE,
|
||||
}
|
||||
export async function handleCollectionOps(
|
||||
type: COLLECTION_OPS_TYPE,
|
||||
collection: Collection,
|
||||
selectedFiles: EnteFile[],
|
||||
selectedCollectionID: number
|
||||
) {
|
||||
switch (type) {
|
||||
case COLLECTION_OPS_TYPE.ADD:
|
||||
await addToCollection(collection, selectedFiles);
|
||||
break;
|
||||
case COLLECTION_OPS_TYPE.MOVE:
|
||||
await moveToCollection(
|
||||
selectedCollectionID,
|
||||
collection,
|
||||
selectedFiles
|
||||
);
|
||||
break;
|
||||
case COLLECTION_OPS_TYPE.REMOVE:
|
||||
await removeFromCollection(collection.id, selectedFiles);
|
||||
break;
|
||||
case COLLECTION_OPS_TYPE.RESTORE:
|
||||
await restoreToCollection(collection, selectedFiles);
|
||||
break;
|
||||
case COLLECTION_OPS_TYPE.UNHIDE:
|
||||
await unhideToCollection(collection, selectedFiles);
|
||||
break;
|
||||
default:
|
||||
throw Error(CustomError.INVALID_COLLECTION_OPERATION);
|
||||
}
|
||||
}
|
||||
|
||||
export function getSelectedCollection(
|
||||
collectionID: number,
|
||||
collections: Collection[]
|
||||
) {
|
||||
return collections.find((collection) => collection.id === collectionID);
|
||||
}
|
||||
|
||||
export async function downloadCollectionHelper(
|
||||
collectionID: number,
|
||||
setCollectionDownloadProgressAttributes: SetCollectionDownloadProgressAttributes
|
||||
) {
|
||||
try {
|
||||
const allFiles = await getAllLocalFiles();
|
||||
const collectionFiles = allFiles.filter(
|
||||
(file) => file.collectionID === collectionID
|
||||
);
|
||||
const allCollections = await getAllLocalCollections();
|
||||
const collection = allCollections.find(
|
||||
(collection) => collection.id === collectionID
|
||||
);
|
||||
if (!collection) {
|
||||
throw Error('collection not found');
|
||||
}
|
||||
await downloadCollectionFiles(
|
||||
collection.name,
|
||||
collection.id,
|
||||
isHiddenCollection(collection),
|
||||
collectionFiles,
|
||||
setCollectionDownloadProgressAttributes
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'download collection failed ');
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadDefaultHiddenCollectionHelper(
|
||||
setCollectionDownloadProgressAttributes: SetCollectionDownloadProgressAttributes
|
||||
) {
|
||||
try {
|
||||
const hiddenCollections = await getLocalCollections('hidden');
|
||||
const defaultHiddenCollectionsIds =
|
||||
getDefaultHiddenCollectionIDs(hiddenCollections);
|
||||
const hiddenFiles = await getLocalFiles('hidden');
|
||||
const defaultHiddenCollectionFiles = hiddenFiles.filter((file) =>
|
||||
defaultHiddenCollectionsIds.has(file.collectionID)
|
||||
);
|
||||
await downloadCollectionFiles(
|
||||
DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME,
|
||||
HIDDEN_ITEMS_SECTION,
|
||||
true,
|
||||
defaultHiddenCollectionFiles,
|
||||
setCollectionDownloadProgressAttributes
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'download hidden files failed ');
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadCollectionFiles(
|
||||
collectionName: string,
|
||||
collectionID: number,
|
||||
isHidden: boolean,
|
||||
collectionFiles: EnteFile[],
|
||||
setCollectionDownloadProgressAttributes: SetCollectionDownloadProgressAttributes
|
||||
) {
|
||||
if (!collectionFiles.length) {
|
||||
return;
|
||||
}
|
||||
const canceller = new AbortController();
|
||||
const increaseSuccess = () => {
|
||||
if (canceller.signal.aborted) return;
|
||||
setCollectionDownloadProgressAttributes((prev) => ({
|
||||
...prev,
|
||||
success: prev.success + 1,
|
||||
}));
|
||||
};
|
||||
const increaseFailed = () => {
|
||||
if (canceller.signal.aborted) return;
|
||||
setCollectionDownloadProgressAttributes((prev) => ({
|
||||
...prev,
|
||||
failed: prev.failed + 1,
|
||||
}));
|
||||
};
|
||||
const isCancelled = () => canceller.signal.aborted;
|
||||
const initialProgressAttributes: CollectionDownloadProgressAttributes = {
|
||||
collectionName,
|
||||
collectionID,
|
||||
isHidden,
|
||||
canceller,
|
||||
total: collectionFiles.length,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
downloadDirPath: null,
|
||||
};
|
||||
if (isElectron()) {
|
||||
const selectedDir = await ElectronService.selectDirectory();
|
||||
if (!selectedDir) {
|
||||
return;
|
||||
}
|
||||
const downloadDirPath = await createCollectionDownloadFolder(
|
||||
selectedDir,
|
||||
collectionName
|
||||
);
|
||||
setCollectionDownloadProgressAttributes({
|
||||
...initialProgressAttributes,
|
||||
downloadDirPath,
|
||||
});
|
||||
await downloadFilesDesktop(
|
||||
collectionFiles,
|
||||
{ increaseSuccess, increaseFailed, isCancelled },
|
||||
downloadDirPath
|
||||
);
|
||||
} else {
|
||||
setCollectionDownloadProgressAttributes(initialProgressAttributes);
|
||||
await downloadFiles(collectionFiles, {
|
||||
increaseSuccess,
|
||||
increaseFailed,
|
||||
isCancelled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createCollectionDownloadFolder(
|
||||
downloadDirPath: string,
|
||||
collectionName: string
|
||||
) {
|
||||
const collectionDownloadName = getUniqueCollectionExportName(
|
||||
downloadDirPath,
|
||||
collectionName
|
||||
);
|
||||
const collectionDownloadPath = getCollectionExportPath(
|
||||
downloadDirPath,
|
||||
collectionDownloadName
|
||||
);
|
||||
await exportService.checkExistsAndCreateDir(collectionDownloadPath);
|
||||
return collectionDownloadPath;
|
||||
}
|
||||
|
||||
export function appendCollectionKeyToShareURL(
|
||||
url: string,
|
||||
collectionKey: string
|
||||
) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sharableURL = new URL(url);
|
||||
const albumsURL = new URL(getAlbumsURL());
|
||||
|
||||
sharableURL.protocol = albumsURL.protocol;
|
||||
sharableURL.host = albumsURL.host;
|
||||
sharableURL.pathname = albumsURL.pathname;
|
||||
|
||||
const bytes = Buffer.from(collectionKey, 'base64');
|
||||
sharableURL.hash = bs58.encode(bytes);
|
||||
return sharableURL.href;
|
||||
}
|
||||
|
||||
const _intSelectOption = (i: number) => {
|
||||
const label = i === 0 ? t('NO_DEVICE_LIMIT') : i.toString();
|
||||
return { label, value: i };
|
||||
};
|
||||
|
||||
export function getDeviceLimitOptions() {
|
||||
return [0, 2, 5, 10, 25, 50].map((i) => _intSelectOption(i));
|
||||
}
|
||||
|
||||
export const shareExpiryOptions = () => [
|
||||
{ label: t('NEVER'), value: () => 0 },
|
||||
{
|
||||
label: t('AFTER_TIME.HOUR'),
|
||||
value: () => getUnixTimeInMicroSecondsWithDelta({ hours: 1 }),
|
||||
},
|
||||
{
|
||||
label: t('AFTER_TIME.DAY'),
|
||||
value: () => getUnixTimeInMicroSecondsWithDelta({ days: 1 }),
|
||||
},
|
||||
{
|
||||
label: t('AFTER_TIME.WEEK'),
|
||||
value: () => getUnixTimeInMicroSecondsWithDelta({ days: 7 }),
|
||||
},
|
||||
{
|
||||
label: t('AFTER_TIME.MONTH'),
|
||||
value: () => getUnixTimeInMicroSecondsWithDelta({ months: 1 }),
|
||||
},
|
||||
{
|
||||
label: t('AFTER_TIME.YEAR'),
|
||||
value: () => getUnixTimeInMicroSecondsWithDelta({ years: 1 }),
|
||||
},
|
||||
];
|
||||
|
||||
export const changeCollectionVisibility = async (
|
||||
collection: Collection,
|
||||
visibility: VISIBILITY_STATE
|
||||
) => {
|
||||
try {
|
||||
const updatedMagicMetadataProps: CollectionMagicMetadataProps = {
|
||||
visibility,
|
||||
};
|
||||
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
if (collection.owner.id === user.id) {
|
||||
const updatedMagicMetadata = await updateMagicMetadata(
|
||||
updatedMagicMetadataProps,
|
||||
collection.magicMetadata,
|
||||
collection.key
|
||||
);
|
||||
|
||||
await updateCollectionMagicMetadata(
|
||||
collection,
|
||||
updatedMagicMetadata
|
||||
);
|
||||
} else {
|
||||
const updatedMagicMetadata = await updateMagicMetadata(
|
||||
updatedMagicMetadataProps,
|
||||
collection.sharedMagicMetadata,
|
||||
collection.key
|
||||
);
|
||||
await updateSharedCollectionMagicMetadata(
|
||||
collection,
|
||||
updatedMagicMetadata
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'change collection visibility failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const changeCollectionSortOrder = async (
|
||||
collection: Collection,
|
||||
asc: boolean
|
||||
) => {
|
||||
try {
|
||||
const updatedPublicMagicMetadataProps: CollectionPublicMagicMetadataProps =
|
||||
{
|
||||
asc,
|
||||
};
|
||||
|
||||
const updatedPubMagicMetadata = await updateMagicMetadata(
|
||||
updatedPublicMagicMetadataProps,
|
||||
collection.pubMagicMetadata,
|
||||
collection.key
|
||||
);
|
||||
|
||||
await updatePublicCollectionMagicMetadata(
|
||||
collection,
|
||||
updatedPubMagicMetadata
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'change collection sort order failed');
|
||||
}
|
||||
};
|
||||
|
||||
export const changeCollectionOrder = async (
|
||||
collection: Collection,
|
||||
order: number
|
||||
) => {
|
||||
try {
|
||||
const updatedMagicMetadataProps: CollectionMagicMetadataProps = {
|
||||
order,
|
||||
};
|
||||
|
||||
const updatedMagicMetadata = await updateMagicMetadata(
|
||||
updatedMagicMetadataProps,
|
||||
collection.magicMetadata,
|
||||
collection.key
|
||||
);
|
||||
|
||||
await updateCollectionMagicMetadata(collection, updatedMagicMetadata);
|
||||
} catch (e) {
|
||||
logError(e, 'change collection order failed');
|
||||
}
|
||||
};
|
||||
|
||||
export const changeCollectionSubType = async (
|
||||
collection: Collection,
|
||||
subType: SUB_TYPE
|
||||
) => {
|
||||
try {
|
||||
const updatedMagicMetadataProps: CollectionMagicMetadataProps = {
|
||||
subType: subType,
|
||||
};
|
||||
|
||||
const updatedMagicMetadata = await updateMagicMetadata(
|
||||
updatedMagicMetadataProps,
|
||||
collection.magicMetadata,
|
||||
collection.key
|
||||
);
|
||||
await updateCollectionMagicMetadata(collection, updatedMagicMetadata);
|
||||
} catch (e) {
|
||||
logError(e, 'change collection subType failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getArchivedCollections = (collections: Collection[]) => {
|
||||
return new Set<number>(
|
||||
collections
|
||||
.filter(isArchivedCollection)
|
||||
.map((collection) => collection.id)
|
||||
);
|
||||
};
|
||||
|
||||
export const getDefaultHiddenCollectionIDs = (collections: Collection[]) => {
|
||||
return new Set<number>(
|
||||
collections
|
||||
.filter(isDefaultHiddenCollection)
|
||||
.map((collection) => collection.id)
|
||||
);
|
||||
};
|
||||
|
||||
export const hasNonSystemCollections = (
|
||||
collectionSummaries: CollectionSummaries
|
||||
) => {
|
||||
for (const collectionSummary of collectionSummaries.values()) {
|
||||
if (!isSystemCollection(collectionSummary.type)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isMoveToAllowedCollection = (type: CollectionSummaryType) => {
|
||||
return !MOVE_TO_NOT_ALLOWED_COLLECTION.has(type);
|
||||
};
|
||||
|
||||
export const isAddToAllowedCollection = (type: CollectionSummaryType) => {
|
||||
return !ADD_TO_NOT_ALLOWED_COLLECTION.has(type);
|
||||
};
|
||||
|
||||
export const isSystemCollection = (type: CollectionSummaryType) => {
|
||||
return SYSTEM_COLLECTION_TYPES.has(type);
|
||||
};
|
||||
|
||||
export const shouldShowOptions = (type: CollectionSummaryType) => {
|
||||
return !OPTIONS_NOT_HAVING_COLLECTION_TYPES.has(type);
|
||||
};
|
||||
export const showEmptyTrashQuickOption = (type: CollectionSummaryType) => {
|
||||
return type === CollectionSummaryType.trash;
|
||||
};
|
||||
export const showDownloadQuickOption = (type: CollectionSummaryType) => {
|
||||
return (
|
||||
type === CollectionSummaryType.folder ||
|
||||
type === CollectionSummaryType.favorites ||
|
||||
type === CollectionSummaryType.album ||
|
||||
type === CollectionSummaryType.uncategorized ||
|
||||
type === CollectionSummaryType.hiddenItems ||
|
||||
type === CollectionSummaryType.incomingShareViewer ||
|
||||
type === CollectionSummaryType.incomingShareCollaborator ||
|
||||
type === CollectionSummaryType.outgoingShare ||
|
||||
type === CollectionSummaryType.sharedOnlyViaLink ||
|
||||
type === CollectionSummaryType.archived ||
|
||||
type === CollectionSummaryType.pinned
|
||||
);
|
||||
};
|
||||
export const showShareQuickOption = (type: CollectionSummaryType) => {
|
||||
return (
|
||||
type === CollectionSummaryType.folder ||
|
||||
type === CollectionSummaryType.album ||
|
||||
type === CollectionSummaryType.outgoingShare ||
|
||||
type === CollectionSummaryType.sharedOnlyViaLink ||
|
||||
type === CollectionSummaryType.archived ||
|
||||
type === CollectionSummaryType.incomingShareViewer ||
|
||||
type === CollectionSummaryType.incomingShareCollaborator ||
|
||||
type === CollectionSummaryType.pinned
|
||||
);
|
||||
};
|
||||
export const shouldBeShownOnCollectionBar = (type: CollectionSummaryType) => {
|
||||
return !HIDE_FROM_COLLECTION_BAR_TYPES.has(type);
|
||||
};
|
||||
|
||||
export const getUserOwnedCollections = (collections: Collection[]) => {
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
if (!user?.id) {
|
||||
throw Error('user missing');
|
||||
}
|
||||
return collections.filter((collection) => collection.owner.id === user.id);
|
||||
};
|
||||
|
||||
export const isDefaultHiddenCollection = (collection: Collection) =>
|
||||
collection.magicMetadata?.data.subType === SUB_TYPE.DEFAULT_HIDDEN;
|
||||
|
||||
export const isHiddenCollection = (collection: Collection) =>
|
||||
collection.magicMetadata?.data.visibility === VISIBILITY_STATE.HIDDEN;
|
||||
|
||||
export const isQuickLinkCollection = (collection: Collection) =>
|
||||
collection.magicMetadata?.data.subType === SUB_TYPE.QUICK_LINK_COLLECTION;
|
||||
|
||||
export function isOutgoingShare(collection: Collection, user: User): boolean {
|
||||
return collection.owner.id === user.id && collection.sharees?.length > 0;
|
||||
}
|
||||
|
||||
export function isIncomingShare(collection: Collection, user: User) {
|
||||
return collection.owner.id !== user.id;
|
||||
}
|
||||
|
||||
export function isIncomingViewerShare(collection: Collection, user: User) {
|
||||
const sharee = collection.sharees?.find((sharee) => sharee.id === user.id);
|
||||
return sharee?.role === COLLECTION_ROLE.VIEWER;
|
||||
}
|
||||
|
||||
export function isIncomingCollabShare(collection: Collection, user: User) {
|
||||
const sharee = collection.sharees?.find((sharee) => sharee.id === user.id);
|
||||
return sharee?.role === COLLECTION_ROLE.COLLABORATOR;
|
||||
}
|
||||
|
||||
export function isSharedOnlyViaLink(collection: Collection) {
|
||||
return collection.publicURLs?.length && !collection.sharees?.length;
|
||||
}
|
||||
|
||||
export function isValidMoveTarget(
|
||||
sourceCollectionID: number,
|
||||
targetCollection: Collection,
|
||||
user: User
|
||||
) {
|
||||
return (
|
||||
sourceCollectionID !== targetCollection.id &&
|
||||
!isHiddenCollection(targetCollection) &&
|
||||
!isQuickLinkCollection(targetCollection) &&
|
||||
!isIncomingShare(targetCollection, user)
|
||||
);
|
||||
}
|
||||
|
||||
export function isValidReplacementAlbum(
|
||||
collection: Collection,
|
||||
user: User,
|
||||
wantedCollectionName: string
|
||||
) {
|
||||
return (
|
||||
collection.name === wantedCollectionName &&
|
||||
(collection.type === CollectionType.album ||
|
||||
collection.type === CollectionType.folder) &&
|
||||
!isHiddenCollection(collection) &&
|
||||
!isQuickLinkCollection(collection) &&
|
||||
!isIncomingShare(collection, user)
|
||||
);
|
||||
}
|
||||
|
||||
export function getCollectionNameMap(
|
||||
collections: Collection[]
|
||||
): Map<number, string> {
|
||||
return new Map<number, string>(
|
||||
collections.map((collection) => [collection.id, collection.name])
|
||||
);
|
||||
}
|
||||
|
||||
export function getNonEmptyPersonalCollections(
|
||||
collections: Collection[],
|
||||
personalFiles: EnteFile[],
|
||||
user: User
|
||||
): Collection[] {
|
||||
if (!user?.id) {
|
||||
throw Error('user missing');
|
||||
}
|
||||
const nonEmptyCollections = getNonEmptyCollections(
|
||||
collections,
|
||||
personalFiles
|
||||
);
|
||||
const personalCollections = nonEmptyCollections.filter(
|
||||
(collection) => collection.owner.id === user?.id
|
||||
);
|
||||
return personalCollections;
|
||||
}
|
||||
|
||||
export function getNonHiddenCollections(
|
||||
collections: Collection[]
|
||||
): Collection[] {
|
||||
return collections.filter((collection) => !isHiddenCollection(collection));
|
||||
}
|
||||
|
||||
export function getHiddenCollections(collections: Collection[]): Collection[] {
|
||||
return collections.filter((collection) => isHiddenCollection(collection));
|
||||
}
|
||||
|
||||
export async function splitNormalAndHiddenCollections(
|
||||
collections: Collection[]
|
||||
): Promise<{
|
||||
normalCollections: Collection[];
|
||||
hiddenCollections: Collection[];
|
||||
}> {
|
||||
const normalCollections = [];
|
||||
const hiddenCollections = [];
|
||||
for (const collection of collections) {
|
||||
if (isHiddenCollection(collection)) {
|
||||
hiddenCollections.push(collection);
|
||||
} else {
|
||||
normalCollections.push(collection);
|
||||
}
|
||||
}
|
||||
return { normalCollections, hiddenCollections };
|
||||
}
|
||||
|
||||
export function constructCollectionNameMap(
|
||||
collections: Collection[]
|
||||
): Map<number, string> {
|
||||
return new Map<number, string>(
|
||||
(collections ?? []).map((collection) => [
|
||||
collection.id,
|
||||
getCollectionUserFacingName(collection),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
export const getCollectionUserFacingName = (collection: Collection) => {
|
||||
if (isDefaultHiddenCollection(collection)) {
|
||||
return DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME;
|
||||
}
|
||||
return collection.name;
|
||||
};
|
||||
|
||||
export const getOrCreateAlbum = async (
|
||||
albumName: string,
|
||||
existingCollections: Collection[]
|
||||
) => {
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
if (!user?.id) {
|
||||
throw Error('user missing');
|
||||
}
|
||||
for (const collection of existingCollections) {
|
||||
if (isValidReplacementAlbum(collection, user, albumName)) {
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
return createAlbum(albumName);
|
||||
};
|
25
apps/cast/src/utils/comlink/ComlinkCryptoWorker.ts
Normal file
25
apps/cast/src/utils/comlink/ComlinkCryptoWorker.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Remote } from 'comlink';
|
||||
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
|
||||
import { ComlinkWorker } from './comlinkWorker';
|
||||
|
||||
class ComlinkCryptoWorker {
|
||||
private comlinkWorkerInstance: Promise<Remote<DedicatedCryptoWorker>>;
|
||||
|
||||
async getInstance() {
|
||||
if (!this.comlinkWorkerInstance) {
|
||||
const comlinkWorker = getDedicatedCryptoWorker();
|
||||
this.comlinkWorkerInstance = comlinkWorker.remote;
|
||||
}
|
||||
return this.comlinkWorkerInstance;
|
||||
}
|
||||
}
|
||||
|
||||
export const getDedicatedCryptoWorker = () => {
|
||||
const cryptoComlinkWorker = new ComlinkWorker<typeof DedicatedCryptoWorker>(
|
||||
'ente-crypto-worker',
|
||||
new Worker(new URL('worker/crypto.worker.ts', import.meta.url))
|
||||
);
|
||||
return cryptoComlinkWorker;
|
||||
};
|
||||
|
||||
export default new ComlinkCryptoWorker();
|
27
apps/cast/src/utils/comlink/comlinkWorker.ts
Normal file
27
apps/cast/src/utils/comlink/comlinkWorker.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Remote, wrap } from 'comlink';
|
||||
// import { WorkerElectronCacheStorageClient } from 'services/workerElectronCache/client';
|
||||
import { addLocalLog } from 'utils/logging';
|
||||
|
||||
export class ComlinkWorker<T extends new () => InstanceType<T>> {
|
||||
public remote: Promise<Remote<InstanceType<T>>>;
|
||||
private worker: Worker;
|
||||
private name: string;
|
||||
|
||||
constructor(name: string, worker: Worker) {
|
||||
this.name = name;
|
||||
this.worker = worker;
|
||||
|
||||
this.worker.onerror = (errorEvent) => {
|
||||
console.error('Got error event from worker', errorEvent);
|
||||
};
|
||||
addLocalLog(() => `Initiated ${this.name}`);
|
||||
const comlink = wrap<T>(this.worker);
|
||||
this.remote = new comlink() as Promise<Remote<InstanceType<T>>>;
|
||||
// expose(WorkerElectronCacheStorageClient, this.worker);
|
||||
}
|
||||
|
||||
public terminate() {
|
||||
this.worker.terminate();
|
||||
addLocalLog(() => `Terminated ${this.name}`);
|
||||
}
|
||||
}
|
113
apps/cast/src/utils/common/apiUtil.ts
Normal file
113
apps/cast/src/utils/common/apiUtil.ts
Normal file
|
@ -0,0 +1,113 @@
|
|||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
|
||||
export const getEndpoint = () => {
|
||||
let endpoint = getData(LS_KEYS.API_ENDPOINT);
|
||||
if (endpoint) {
|
||||
return endpoint;
|
||||
}
|
||||
endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
|
||||
if (isDevDeployment() && endpoint) {
|
||||
return endpoint;
|
||||
}
|
||||
return 'https://api.ente.io';
|
||||
};
|
||||
|
||||
export const getFileURL = (id: number) => {
|
||||
const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
|
||||
if (isDevDeployment() && endpoint) {
|
||||
return `${endpoint}/files/download/${id}`;
|
||||
}
|
||||
return `https://files.ente.io/?fileID=${id}`;
|
||||
};
|
||||
|
||||
export const getPublicCollectionFileURL = (id: number) => {
|
||||
const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
|
||||
if (isDevDeployment() && endpoint) {
|
||||
return `${endpoint}/public-collection/files/download/${id}`;
|
||||
}
|
||||
return `https://public-albums.ente.io/download/?fileID=${id}`;
|
||||
};
|
||||
|
||||
export const getThumbnailURL = (id: number) => {
|
||||
const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
|
||||
if (isDevDeployment() && endpoint) {
|
||||
return `${endpoint}/files/preview/${id}`;
|
||||
}
|
||||
return `https://thumbnails.ente.io/?fileID=${id}`;
|
||||
};
|
||||
|
||||
export const getPublicCollectionThumbnailURL = (id: number) => {
|
||||
const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
|
||||
if (isDevDeployment() && endpoint) {
|
||||
return `${endpoint}/public-collection/files/preview/${id}`;
|
||||
}
|
||||
return `https://public-albums.ente.io/preview/?fileID=${id}`;
|
||||
};
|
||||
|
||||
export const getUploadEndpoint = () => {
|
||||
const endpoint = process.env.NEXT_PUBLIC_ENTE_UPLOAD_ENDPOINT;
|
||||
if (isDevDeployment() && endpoint) {
|
||||
return endpoint;
|
||||
}
|
||||
return `https://uploader.ente.io`;
|
||||
};
|
||||
|
||||
export const getPaymentsURL = () => {
|
||||
const paymentsURL = process.env.NEXT_PUBLIC_ENTE_PAYMENT_ENDPOINT;
|
||||
if (isDevDeployment() && paymentsURL) {
|
||||
return paymentsURL;
|
||||
}
|
||||
return `https://payments.ente.io`;
|
||||
};
|
||||
|
||||
export const getAlbumsURL = () => {
|
||||
const albumsURL = process.env.NEXT_PUBLIC_ENTE_ALBUM_ENDPOINT;
|
||||
if (isDevDeployment() && albumsURL) {
|
||||
return albumsURL;
|
||||
}
|
||||
return `https://albums.ente.io`;
|
||||
};
|
||||
|
||||
// getFamilyPortalURL returns the endpoint for the family dashboard which can be used to
|
||||
// create or manage family.
|
||||
export const getFamilyPortalURL = () => {
|
||||
const familyURL = process.env.NEXT_PUBLIC_ENTE_FAMILY_PORTAL_ENDPOINT;
|
||||
if (isDevDeployment() && familyURL) {
|
||||
return familyURL;
|
||||
}
|
||||
return `https://family.ente.io`;
|
||||
};
|
||||
|
||||
// getAuthenticatorURL returns the endpoint for the authenticator which can be used to
|
||||
// view authenticator codes.
|
||||
export const getAuthURL = () => {
|
||||
const authURL = process.env.NEXT_PUBLIC_ENTE_AUTH_ENDPOINT;
|
||||
if (isDevDeployment() && authURL) {
|
||||
return authURL;
|
||||
}
|
||||
return `https://auth.ente.io`;
|
||||
};
|
||||
|
||||
export const getSentryTunnelURL = () => {
|
||||
return `https://sentry-reporter.ente.io`;
|
||||
};
|
||||
|
||||
/*
|
||||
It's a dev deployment (and should use the environment override for endpoints ) in three cases:
|
||||
1. when the URL opened is that of the staging web app, or
|
||||
2. when the URL opened is that of the staging album app, or
|
||||
3. if the app is running locally (hence node_env is development)
|
||||
4. if the app is running in test mode
|
||||
*/
|
||||
export const isDevDeployment = () => {
|
||||
if (globalThis?.location) {
|
||||
return (
|
||||
process.env.NEXT_PUBLIC_ENTE_WEB_ENDPOINT ===
|
||||
globalThis.location.origin ||
|
||||
process.env.NEXT_PUBLIC_ENTE_ALBUM_ENDPOINT ===
|
||||
globalThis.location.origin ||
|
||||
process.env.NEXT_PUBLIC_IS_TEST_APP === 'true' ||
|
||||
process.env.NODE_ENV === 'development'
|
||||
);
|
||||
}
|
||||
};
|
149
apps/cast/src/utils/common/index.ts
Normal file
149
apps/cast/src/utils/common/index.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
import { CustomError } from 'utils/error';
|
||||
import isElectron from 'is-electron';
|
||||
import { APP_DOWNLOAD_URL } from 'constants/urls';
|
||||
|
||||
export function checkConnectivity() {
|
||||
if (navigator.onLine) {
|
||||
return true;
|
||||
}
|
||||
throw new Error(CustomError.NO_INTERNET_CONNECTION);
|
||||
}
|
||||
|
||||
export function runningInBrowser() {
|
||||
return typeof window !== 'undefined';
|
||||
}
|
||||
|
||||
export function runningInWorker() {
|
||||
return typeof importScripts === 'function';
|
||||
}
|
||||
|
||||
export function runningInElectron() {
|
||||
return isElectron();
|
||||
}
|
||||
|
||||
export function runningInChrome(includeMobile: boolean) {
|
||||
try {
|
||||
const userAgentData = navigator['userAgentData'];
|
||||
const chromeBrand = userAgentData?.brands?.filter(
|
||||
(b) => b.brand === 'Google Chrome' || b.brand === 'Chromium'
|
||||
)?.[0];
|
||||
return chromeBrand && (includeMobile || userAgentData.mobile === false);
|
||||
} catch (error) {
|
||||
console.error('Error in runningInChrome: ', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function offscreenCanvasSupported() {
|
||||
return !(typeof OffscreenCanvas === 'undefined');
|
||||
}
|
||||
|
||||
export function webglSupported() {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const gl = canvas.getContext('webgl');
|
||||
return gl && gl instanceof WebGLRenderingContext;
|
||||
} catch (error) {
|
||||
console.error('Error in webglSupported: ', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function sleep(time: number) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(null), time);
|
||||
});
|
||||
}
|
||||
|
||||
export function downloadApp() {
|
||||
openLink(APP_DOWNLOAD_URL, true);
|
||||
}
|
||||
|
||||
export function reverseString(title: string) {
|
||||
return title
|
||||
?.split(' ')
|
||||
.reduce((reversedString, currWord) => `${currWord} ${reversedString}`);
|
||||
}
|
||||
|
||||
export function initiateEmail(email: string) {
|
||||
const a = document.createElement('a');
|
||||
a.href = 'mailto:' + email;
|
||||
a.rel = 'noreferrer noopener';
|
||||
a.click();
|
||||
}
|
||||
export const promiseWithTimeout = async <T>(
|
||||
request: Promise<T>,
|
||||
timeout: number
|
||||
): Promise<T> => {
|
||||
const timeoutRef = { current: null };
|
||||
const rejectOnTimeout = new Promise<null>((_, reject) => {
|
||||
timeoutRef.current = setTimeout(
|
||||
() => reject(Error(CustomError.WAIT_TIME_EXCEEDED)),
|
||||
timeout
|
||||
);
|
||||
});
|
||||
const requestWithTimeOutCancellation = async () => {
|
||||
const resp = await request;
|
||||
clearTimeout(timeoutRef.current);
|
||||
return resp;
|
||||
};
|
||||
return await Promise.race([
|
||||
requestWithTimeOutCancellation(),
|
||||
rejectOnTimeout,
|
||||
]);
|
||||
};
|
||||
|
||||
export const preloadImage = (imgBasePath: string) => {
|
||||
const srcSet = [];
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
srcSet.push(`${imgBasePath}/${i}x.png ${i}x`);
|
||||
}
|
||||
new Image().srcset = srcSet.join(',');
|
||||
};
|
||||
export function openLink(href: string, newTab?: boolean) {
|
||||
const a = document.createElement('a');
|
||||
a.href = href;
|
||||
if (newTab) {
|
||||
a.target = '_blank';
|
||||
}
|
||||
a.rel = 'noreferrer noopener';
|
||||
a.click();
|
||||
}
|
||||
|
||||
export async function waitAndRun(
|
||||
waitPromise: Promise<void>,
|
||||
task: () => Promise<void>
|
||||
) {
|
||||
if (waitPromise && isPromise(waitPromise)) {
|
||||
await waitPromise;
|
||||
}
|
||||
await task();
|
||||
}
|
||||
|
||||
function isPromise(p: any) {
|
||||
if (typeof p === 'object' && typeof p.then === 'function') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isClipboardItemPresent() {
|
||||
return typeof ClipboardItem !== 'undefined';
|
||||
}
|
||||
|
||||
export function batch<T>(arr: T[], batchSize: number): T[][] {
|
||||
const batches: T[][] = [];
|
||||
for (let i = 0; i < arr.length; i += batchSize) {
|
||||
batches.push(arr.slice(i, i + batchSize));
|
||||
}
|
||||
return batches;
|
||||
}
|
||||
|
||||
export const mergeMaps = <K, V>(map1: Map<K, V>, map2: Map<K, V>) => {
|
||||
const mergedMap = new Map<K, V>(map1);
|
||||
map2.forEach((value, key) => {
|
||||
mergedMap.set(key, value);
|
||||
});
|
||||
return mergedMap;
|
||||
};
|
26
apps/cast/src/utils/common/key.ts
Normal file
26
apps/cast/src/utils/common/key.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { B64EncryptionResult } from 'types/crypto';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||
import { CustomError } from '../error';
|
||||
|
||||
export const getActualKey = async () => {
|
||||
try {
|
||||
const encryptionKeyAttributes: B64EncryptionResult = getKey(
|
||||
SESSION_KEYS.ENCRYPTION_KEY
|
||||
);
|
||||
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const key = await cryptoWorker.decryptB64(
|
||||
encryptionKeyAttributes.encryptedData,
|
||||
encryptionKeyAttributes.nonce,
|
||||
encryptionKeyAttributes.key
|
||||
);
|
||||
return key;
|
||||
} catch (e) {
|
||||
throw new Error(CustomError.KEY_MISSING);
|
||||
}
|
||||
};
|
||||
|
||||
export const getToken = () => getData(LS_KEYS.USER)?.token;
|
||||
export const getUserID = () => getData(LS_KEYS.USER)?.id;
|
365
apps/cast/src/utils/crypto/index.ts
Normal file
365
apps/cast/src/utils/crypto/index.ts
Normal file
|
@ -0,0 +1,365 @@
|
|||
import { KeyAttributes, SRPSetupAttributes } from 'types/user';
|
||||
import { SESSION_KEYS, setKey } from 'utils/storage/sessionStorage';
|
||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||
import { getActualKey, getToken } from 'utils/common/key';
|
||||
import { setRecoveryKey } from 'services/userService';
|
||||
import { logError } from 'utils/sentry';
|
||||
import isElectron from 'is-electron';
|
||||
// import safeStorageService from 'services/electron/safeStorage';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
import { PasswordStrength } from 'constants/crypto';
|
||||
import zxcvbn from 'zxcvbn';
|
||||
import { SRP, SrpClient } from 'fast-srp-hap';
|
||||
import { convertBase64ToBuffer, convertBufferToBase64 } from 'utils/user';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { addLocalLog } from 'utils/logging';
|
||||
|
||||
const SRP_PARAMS = SRP.params['4096'];
|
||||
|
||||
const LOGIN_SUB_KEY_LENGTH = 32;
|
||||
const LOGIN_SUB_KEY_ID = 1;
|
||||
const LOGIN_SUB_KEY_CONTEXT = 'loginctx';
|
||||
const LOGIN_SUB_KEY_BYTE_LENGTH = 16;
|
||||
|
||||
export async function generateKeyAndSRPAttributes(passphrase: string): Promise<{
|
||||
keyAttributes: KeyAttributes;
|
||||
masterKey: string;
|
||||
srpSetupAttributes: SRPSetupAttributes;
|
||||
}> {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const masterKey = await cryptoWorker.generateEncryptionKey();
|
||||
const recoveryKey = await cryptoWorker.generateEncryptionKey();
|
||||
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
|
||||
const kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
|
||||
|
||||
const masterKeyEncryptedWithKek = await cryptoWorker.encryptToB64(
|
||||
masterKey,
|
||||
kek.key
|
||||
);
|
||||
const masterKeyEncryptedWithRecoveryKey = await cryptoWorker.encryptToB64(
|
||||
masterKey,
|
||||
recoveryKey
|
||||
);
|
||||
const recoveryKeyEncryptedWithMasterKey = await cryptoWorker.encryptToB64(
|
||||
recoveryKey,
|
||||
masterKey
|
||||
);
|
||||
|
||||
const keyPair = await cryptoWorker.generateKeyPair();
|
||||
const encryptedKeyPairAttributes = await cryptoWorker.encryptToB64(
|
||||
keyPair.privateKey,
|
||||
masterKey
|
||||
);
|
||||
|
||||
const loginSubKey = await generateLoginSubKey(kek.key);
|
||||
|
||||
const srpSetupAttributes = await generateSRPSetupAttributes(loginSubKey);
|
||||
|
||||
const keyAttributes: KeyAttributes = {
|
||||
kekSalt,
|
||||
encryptedKey: masterKeyEncryptedWithKek.encryptedData,
|
||||
keyDecryptionNonce: masterKeyEncryptedWithKek.nonce,
|
||||
publicKey: keyPair.publicKey,
|
||||
encryptedSecretKey: encryptedKeyPairAttributes.encryptedData,
|
||||
secretKeyDecryptionNonce: encryptedKeyPairAttributes.nonce,
|
||||
opsLimit: kek.opsLimit,
|
||||
memLimit: kek.memLimit,
|
||||
masterKeyEncryptedWithRecoveryKey:
|
||||
masterKeyEncryptedWithRecoveryKey.encryptedData,
|
||||
masterKeyDecryptionNonce: masterKeyEncryptedWithRecoveryKey.nonce,
|
||||
recoveryKeyEncryptedWithMasterKey:
|
||||
recoveryKeyEncryptedWithMasterKey.encryptedData,
|
||||
recoveryKeyDecryptionNonce: recoveryKeyEncryptedWithMasterKey.nonce,
|
||||
};
|
||||
|
||||
return {
|
||||
keyAttributes,
|
||||
masterKey,
|
||||
srpSetupAttributes,
|
||||
};
|
||||
}
|
||||
|
||||
// We encrypt the masterKey, with an intermediate key derived from the
|
||||
// passphrase (with Interactive mem and ops limits) to avoid saving it to local
|
||||
// storage in plain text. This means that on the web user will always have to
|
||||
// enter their passphrase to access their masterKey.
|
||||
export async function generateAndSaveIntermediateKeyAttributes(
|
||||
passphrase: string,
|
||||
existingKeyAttributes: KeyAttributes,
|
||||
key: string
|
||||
): Promise<KeyAttributes> {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const intermediateKekSalt = await cryptoWorker.generateSaltToDeriveKey();
|
||||
const intermediateKek = await cryptoWorker.deriveInteractiveKey(
|
||||
passphrase,
|
||||
intermediateKekSalt
|
||||
);
|
||||
const encryptedKeyAttributes = await cryptoWorker.encryptToB64(
|
||||
key,
|
||||
intermediateKek.key
|
||||
);
|
||||
|
||||
const intermediateKeyAttributes = Object.assign(existingKeyAttributes, {
|
||||
kekSalt: intermediateKekSalt,
|
||||
encryptedKey: encryptedKeyAttributes.encryptedData,
|
||||
keyDecryptionNonce: encryptedKeyAttributes.nonce,
|
||||
opsLimit: intermediateKek.opsLimit,
|
||||
memLimit: intermediateKek.memLimit,
|
||||
});
|
||||
setData(LS_KEYS.KEY_ATTRIBUTES, intermediateKeyAttributes);
|
||||
return intermediateKeyAttributes;
|
||||
}
|
||||
|
||||
export const saveKeyInSessionStore = async (
|
||||
keyType: SESSION_KEYS,
|
||||
key: string,
|
||||
fromDesktop?: boolean
|
||||
) => {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const sessionKeyAttributes = await cryptoWorker.generateKeyAndEncryptToB64(
|
||||
key
|
||||
);
|
||||
setKey(keyType, sessionKeyAttributes);
|
||||
if (
|
||||
isElectron() &&
|
||||
!fromDesktop &&
|
||||
keyType === SESSION_KEYS.ENCRYPTION_KEY
|
||||
) {
|
||||
// safeStorageService.setEncryptionKey(key);
|
||||
}
|
||||
};
|
||||
|
||||
export const getRecoveryKey = async () => {
|
||||
let recoveryKey: string = null;
|
||||
try {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
|
||||
const keyAttributes: KeyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
||||
const {
|
||||
recoveryKeyEncryptedWithMasterKey,
|
||||
recoveryKeyDecryptionNonce,
|
||||
} = keyAttributes;
|
||||
const masterKey = await getActualKey();
|
||||
if (recoveryKeyEncryptedWithMasterKey) {
|
||||
recoveryKey = await cryptoWorker.decryptB64(
|
||||
recoveryKeyEncryptedWithMasterKey,
|
||||
recoveryKeyDecryptionNonce,
|
||||
masterKey
|
||||
);
|
||||
} else {
|
||||
recoveryKey = await createNewRecoveryKey();
|
||||
}
|
||||
recoveryKey = await cryptoWorker.toHex(recoveryKey);
|
||||
return recoveryKey;
|
||||
} catch (e) {
|
||||
logError(e, 'getRecoveryKey failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
// Used only for legacy users for whom we did not generate recovery keys during
|
||||
// sign up
|
||||
async function createNewRecoveryKey() {
|
||||
const masterKey = await getActualKey();
|
||||
const existingAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
||||
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
|
||||
const recoveryKey = await cryptoWorker.generateEncryptionKey();
|
||||
const encryptedMasterKey = await cryptoWorker.encryptToB64(
|
||||
masterKey,
|
||||
recoveryKey
|
||||
);
|
||||
const encryptedRecoveryKey = await cryptoWorker.encryptToB64(
|
||||
recoveryKey,
|
||||
masterKey
|
||||
);
|
||||
const recoveryKeyAttributes = {
|
||||
masterKeyEncryptedWithRecoveryKey: encryptedMasterKey.encryptedData,
|
||||
masterKeyDecryptionNonce: encryptedMasterKey.nonce,
|
||||
recoveryKeyEncryptedWithMasterKey: encryptedRecoveryKey.encryptedData,
|
||||
recoveryKeyDecryptionNonce: encryptedRecoveryKey.nonce,
|
||||
};
|
||||
await setRecoveryKey(getToken(), recoveryKeyAttributes);
|
||||
|
||||
const updatedKeyAttributes = Object.assign(
|
||||
existingAttributes,
|
||||
recoveryKeyAttributes
|
||||
);
|
||||
setData(LS_KEYS.KEY_ATTRIBUTES, updatedKeyAttributes);
|
||||
|
||||
return recoveryKey;
|
||||
}
|
||||
|
||||
export async function decryptAndStoreToken(
|
||||
keyAttributes: KeyAttributes,
|
||||
masterKey: string
|
||||
) {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const user = getData(LS_KEYS.USER);
|
||||
let decryptedToken = null;
|
||||
const { encryptedToken } = user;
|
||||
if (encryptedToken && encryptedToken.length > 0) {
|
||||
const secretKey = await cryptoWorker.decryptB64(
|
||||
keyAttributes.encryptedSecretKey,
|
||||
keyAttributes.secretKeyDecryptionNonce,
|
||||
masterKey
|
||||
);
|
||||
const urlUnsafeB64DecryptedToken = await cryptoWorker.boxSealOpen(
|
||||
encryptedToken,
|
||||
keyAttributes.publicKey,
|
||||
secretKey
|
||||
);
|
||||
const decryptedTokenBytes = await cryptoWorker.fromB64(
|
||||
urlUnsafeB64DecryptedToken
|
||||
);
|
||||
decryptedToken = await cryptoWorker.toURLSafeB64(decryptedTokenBytes);
|
||||
setData(LS_KEYS.USER, {
|
||||
...user,
|
||||
token: decryptedToken,
|
||||
encryptedToken: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function encryptWithRecoveryKey(key: string) {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const hexRecoveryKey = await getRecoveryKey();
|
||||
const recoveryKey = await cryptoWorker.fromHex(hexRecoveryKey);
|
||||
const encryptedKey = await cryptoWorker.encryptToB64(key, recoveryKey);
|
||||
return encryptedKey;
|
||||
}
|
||||
|
||||
export async function decryptDeleteAccountChallenge(
|
||||
encryptedChallenge: string
|
||||
) {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const masterKey = await getActualKey();
|
||||
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
||||
const secretKey = await cryptoWorker.decryptB64(
|
||||
keyAttributes.encryptedSecretKey,
|
||||
keyAttributes.secretKeyDecryptionNonce,
|
||||
masterKey
|
||||
);
|
||||
const b64DecryptedChallenge = await cryptoWorker.boxSealOpen(
|
||||
encryptedChallenge,
|
||||
keyAttributes.publicKey,
|
||||
secretKey
|
||||
);
|
||||
const utf8DecryptedChallenge = atob(b64DecryptedChallenge);
|
||||
return utf8DecryptedChallenge;
|
||||
}
|
||||
|
||||
export function estimatePasswordStrength(password: string): PasswordStrength {
|
||||
if (!password) {
|
||||
return PasswordStrength.WEAK;
|
||||
}
|
||||
|
||||
const zxcvbnResult = zxcvbn(password);
|
||||
if (zxcvbnResult.score < 2) {
|
||||
return PasswordStrength.WEAK;
|
||||
} else if (zxcvbnResult.score < 3) {
|
||||
return PasswordStrength.MODERATE;
|
||||
} else {
|
||||
return PasswordStrength.STRONG;
|
||||
}
|
||||
}
|
||||
|
||||
export const isWeakPassword = (password: string) => {
|
||||
return estimatePasswordStrength(password) === PasswordStrength.WEAK;
|
||||
};
|
||||
|
||||
export const generateLoginSubKey = async (kek: string) => {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const kekSubKeyString = await cryptoWorker.generateSubKey(
|
||||
kek,
|
||||
LOGIN_SUB_KEY_LENGTH,
|
||||
LOGIN_SUB_KEY_ID,
|
||||
LOGIN_SUB_KEY_CONTEXT
|
||||
);
|
||||
const kekSubKey = await cryptoWorker.fromB64(kekSubKeyString);
|
||||
|
||||
// use first 16 bytes of generated kekSubKey as loginSubKey
|
||||
const loginSubKey = await cryptoWorker.toB64(
|
||||
kekSubKey.slice(0, LOGIN_SUB_KEY_BYTE_LENGTH)
|
||||
);
|
||||
|
||||
return loginSubKey;
|
||||
};
|
||||
|
||||
export const generateSRPSetupAttributes = async (
|
||||
loginSubKey: string
|
||||
): Promise<SRPSetupAttributes> => {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
|
||||
const srpSalt = await cryptoWorker.generateSaltToDeriveKey();
|
||||
|
||||
const srpUserID = uuidv4();
|
||||
|
||||
const srpVerifierBuffer = SRP.computeVerifier(
|
||||
SRP_PARAMS,
|
||||
convertBase64ToBuffer(srpSalt),
|
||||
Buffer.from(srpUserID),
|
||||
convertBase64ToBuffer(loginSubKey)
|
||||
);
|
||||
|
||||
const srpVerifier = convertBufferToBase64(srpVerifierBuffer);
|
||||
|
||||
addLocalLog(
|
||||
() => `SRP setup attributes generated',
|
||||
${JSON.stringify({
|
||||
srpSalt,
|
||||
srpUserID,
|
||||
srpVerifier,
|
||||
loginSubKey,
|
||||
})}`
|
||||
);
|
||||
|
||||
return {
|
||||
srpUserID,
|
||||
srpSalt,
|
||||
srpVerifier,
|
||||
loginSubKey,
|
||||
};
|
||||
};
|
||||
|
||||
export const computeVerifierHelper = (
|
||||
srpSalt: string,
|
||||
srpUserID: string,
|
||||
loginSubKey: string
|
||||
) => {
|
||||
const srpVerifierBuffer = SRP.computeVerifier(
|
||||
SRP_PARAMS,
|
||||
convertBase64ToBuffer(srpSalt),
|
||||
Buffer.from(srpUserID),
|
||||
convertBase64ToBuffer(loginSubKey)
|
||||
);
|
||||
return convertBufferToBase64(srpVerifierBuffer);
|
||||
};
|
||||
export const generateSRPClient = async (
|
||||
srpSalt: string,
|
||||
srpUserID: string,
|
||||
loginSubKey: string
|
||||
) => {
|
||||
return new Promise<SrpClient>((resolve, reject) => {
|
||||
SRP.genKey(function (err, secret1) {
|
||||
try {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
const srpClient = new SrpClient(
|
||||
SRP_PARAMS,
|
||||
convertBase64ToBuffer(srpSalt),
|
||||
Buffer.from(srpUserID),
|
||||
convertBase64ToBuffer(loginSubKey),
|
||||
secret1,
|
||||
false
|
||||
);
|
||||
|
||||
resolve(srpClient);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
422
apps/cast/src/utils/crypto/libsodium.ts
Normal file
422
apps/cast/src/utils/crypto/libsodium.ts
Normal file
|
@ -0,0 +1,422 @@
|
|||
import sodium, { StateAddress } from 'libsodium-wrappers';
|
||||
import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto';
|
||||
import { B64EncryptionResult } from 'types/crypto';
|
||||
import { CustomError } from 'utils/error';
|
||||
|
||||
export async function decryptChaChaOneShot(
|
||||
data: Uint8Array,
|
||||
header: Uint8Array,
|
||||
key: string
|
||||
) {
|
||||
await sodium.ready;
|
||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
|
||||
header,
|
||||
await fromB64(key)
|
||||
);
|
||||
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||
pullState,
|
||||
data,
|
||||
null
|
||||
);
|
||||
return pullResult.message;
|
||||
}
|
||||
|
||||
export async function decryptChaCha(
|
||||
data: Uint8Array,
|
||||
header: Uint8Array,
|
||||
key: string
|
||||
) {
|
||||
await sodium.ready;
|
||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
|
||||
header,
|
||||
await fromB64(key)
|
||||
);
|
||||
const decryptionChunkSize =
|
||||
ENCRYPTION_CHUNK_SIZE +
|
||||
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
||||
let bytesRead = 0;
|
||||
const decryptedData = [];
|
||||
let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
while (tag !== sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
|
||||
let chunkSize = decryptionChunkSize;
|
||||
if (bytesRead + chunkSize > data.length) {
|
||||
chunkSize = data.length - bytesRead;
|
||||
}
|
||||
const buffer = data.slice(bytesRead, bytesRead + chunkSize);
|
||||
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||
pullState,
|
||||
buffer
|
||||
);
|
||||
if (!pullResult.message) {
|
||||
throw new Error(CustomError.PROCESSING_FAILED);
|
||||
}
|
||||
for (let index = 0; index < pullResult.message.length; index++) {
|
||||
decryptedData.push(pullResult.message[index]);
|
||||
}
|
||||
tag = pullResult.tag;
|
||||
bytesRead += chunkSize;
|
||||
}
|
||||
return Uint8Array.from(decryptedData);
|
||||
}
|
||||
|
||||
export async function initChunkDecryption(header: Uint8Array, key: Uint8Array) {
|
||||
await sodium.ready;
|
||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
|
||||
header,
|
||||
key
|
||||
);
|
||||
const decryptionChunkSize =
|
||||
ENCRYPTION_CHUNK_SIZE +
|
||||
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
||||
const tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
return { pullState, decryptionChunkSize, tag };
|
||||
}
|
||||
|
||||
export async function decryptFileChunk(
|
||||
data: Uint8Array,
|
||||
pullState: StateAddress
|
||||
) {
|
||||
await sodium.ready;
|
||||
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||
pullState,
|
||||
data
|
||||
);
|
||||
if (!pullResult.message) {
|
||||
throw new Error(CustomError.PROCESSING_FAILED);
|
||||
}
|
||||
const newTag = pullResult.tag;
|
||||
return { decryptedData: pullResult.message, newTag };
|
||||
}
|
||||
|
||||
export async function encryptChaChaOneShot(data: Uint8Array, key: string) {
|
||||
await sodium.ready;
|
||||
|
||||
const uintkey: Uint8Array = await fromB64(key);
|
||||
const initPushResult =
|
||||
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
|
||||
const [pushState, header] = [initPushResult.state, initPushResult.header];
|
||||
|
||||
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
|
||||
pushState,
|
||||
data,
|
||||
null,
|
||||
sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
|
||||
);
|
||||
return {
|
||||
key: await toB64(uintkey),
|
||||
file: {
|
||||
encryptedData: pushResult,
|
||||
decryptionHeader: await toB64(header),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptChaCha(data: Uint8Array) {
|
||||
await sodium.ready;
|
||||
|
||||
const uintkey: Uint8Array =
|
||||
sodium.crypto_secretstream_xchacha20poly1305_keygen();
|
||||
|
||||
const initPushResult =
|
||||
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
|
||||
const [pushState, header] = [initPushResult.state, initPushResult.header];
|
||||
let bytesRead = 0;
|
||||
let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
|
||||
const encryptedData = [];
|
||||
|
||||
while (tag !== sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
|
||||
let chunkSize = ENCRYPTION_CHUNK_SIZE;
|
||||
if (bytesRead + chunkSize >= data.length) {
|
||||
chunkSize = data.length - bytesRead;
|
||||
tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;
|
||||
}
|
||||
|
||||
const buffer = data.slice(bytesRead, bytesRead + chunkSize);
|
||||
bytesRead += chunkSize;
|
||||
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
|
||||
pushState,
|
||||
buffer,
|
||||
null,
|
||||
tag
|
||||
);
|
||||
for (let index = 0; index < pushResult.length; index++) {
|
||||
encryptedData.push(pushResult[index]);
|
||||
}
|
||||
}
|
||||
return {
|
||||
key: await toB64(uintkey),
|
||||
file: {
|
||||
encryptedData: new Uint8Array(encryptedData),
|
||||
decryptionHeader: await toB64(header),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function initChunkEncryption() {
|
||||
await sodium.ready;
|
||||
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
|
||||
const initPushResult =
|
||||
sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
|
||||
const [pushState, header] = [initPushResult.state, initPushResult.header];
|
||||
return {
|
||||
key: await toB64(key),
|
||||
decryptionHeader: await toB64(header),
|
||||
pushState,
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptFileChunk(
|
||||
data: Uint8Array,
|
||||
pushState: sodium.StateAddress,
|
||||
isFinalChunk: boolean
|
||||
) {
|
||||
await sodium.ready;
|
||||
const tag = isFinalChunk
|
||||
? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
|
||||
: sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
|
||||
pushState,
|
||||
data,
|
||||
null,
|
||||
tag
|
||||
);
|
||||
|
||||
return pushResult;
|
||||
}
|
||||
export async function encryptToB64(data: string, key: string) {
|
||||
await sodium.ready;
|
||||
const encrypted = await encrypt(await fromB64(data), await fromB64(key));
|
||||
|
||||
return {
|
||||
encryptedData: await toB64(encrypted.encryptedData),
|
||||
key: await toB64(encrypted.key),
|
||||
nonce: await toB64(encrypted.nonce),
|
||||
} as B64EncryptionResult;
|
||||
}
|
||||
|
||||
export async function generateKeyAndEncryptToB64(data: string) {
|
||||
await sodium.ready;
|
||||
const key = sodium.crypto_secretbox_keygen();
|
||||
return await encryptToB64(data, await toB64(key));
|
||||
}
|
||||
|
||||
export async function encryptUTF8(data: string, key: string) {
|
||||
const b64Data = await toB64(await fromUTF8(data));
|
||||
return await encryptToB64(b64Data, key);
|
||||
}
|
||||
|
||||
export async function decryptB64(data: string, nonce: string, key: string) {
|
||||
await sodium.ready;
|
||||
const decrypted = await decrypt(
|
||||
await fromB64(data),
|
||||
await fromB64(nonce),
|
||||
await fromB64(key)
|
||||
);
|
||||
|
||||
return await toB64(decrypted);
|
||||
}
|
||||
|
||||
export async function decryptToUTF8(data: string, nonce: string, key: string) {
|
||||
await sodium.ready;
|
||||
const decrypted = await decrypt(
|
||||
await fromB64(data),
|
||||
await fromB64(nonce),
|
||||
await fromB64(key)
|
||||
);
|
||||
|
||||
return sodium.to_string(decrypted);
|
||||
}
|
||||
|
||||
async function encrypt(data: Uint8Array, key: Uint8Array) {
|
||||
await sodium.ready;
|
||||
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const encryptedData = sodium.crypto_secretbox_easy(data, nonce, key);
|
||||
return {
|
||||
encryptedData,
|
||||
key,
|
||||
nonce,
|
||||
};
|
||||
}
|
||||
|
||||
async function decrypt(data: Uint8Array, nonce: Uint8Array, key: Uint8Array) {
|
||||
await sodium.ready;
|
||||
return sodium.crypto_secretbox_open_easy(data, nonce, key);
|
||||
}
|
||||
|
||||
export async function initChunkHashing() {
|
||||
await sodium.ready;
|
||||
const hashState = sodium.crypto_generichash_init(
|
||||
null,
|
||||
sodium.crypto_generichash_BYTES_MAX
|
||||
);
|
||||
return hashState;
|
||||
}
|
||||
|
||||
export async function hashFileChunk(
|
||||
hashState: sodium.StateAddress,
|
||||
chunk: Uint8Array
|
||||
) {
|
||||
await sodium.ready;
|
||||
sodium.crypto_generichash_update(hashState, chunk);
|
||||
}
|
||||
|
||||
export async function completeChunkHashing(hashState: sodium.StateAddress) {
|
||||
await sodium.ready;
|
||||
const hash = sodium.crypto_generichash_final(
|
||||
hashState,
|
||||
sodium.crypto_generichash_BYTES_MAX
|
||||
);
|
||||
const hashString = toB64(hash);
|
||||
return hashString;
|
||||
}
|
||||
|
||||
export async function deriveKey(
|
||||
passphrase: string,
|
||||
salt: string,
|
||||
opsLimit: number,
|
||||
memLimit: number
|
||||
) {
|
||||
await sodium.ready;
|
||||
return await toB64(
|
||||
sodium.crypto_pwhash(
|
||||
sodium.crypto_secretbox_KEYBYTES,
|
||||
await fromUTF8(passphrase),
|
||||
await fromB64(salt),
|
||||
opsLimit,
|
||||
memLimit,
|
||||
sodium.crypto_pwhash_ALG_ARGON2ID13
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function deriveSensitiveKey(passphrase: string, salt: string) {
|
||||
await sodium.ready;
|
||||
const minMemLimit = sodium.crypto_pwhash_MEMLIMIT_MIN;
|
||||
let opsLimit = sodium.crypto_pwhash_OPSLIMIT_SENSITIVE;
|
||||
let memLimit = sodium.crypto_pwhash_MEMLIMIT_SENSITIVE;
|
||||
while (memLimit > minMemLimit) {
|
||||
try {
|
||||
const key = await deriveKey(passphrase, salt, opsLimit, memLimit);
|
||||
return {
|
||||
key,
|
||||
opsLimit,
|
||||
memLimit,
|
||||
};
|
||||
} catch (e) {
|
||||
opsLimit *= 2;
|
||||
memLimit /= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deriveInteractiveKey(passphrase: string, salt: string) {
|
||||
await sodium.ready;
|
||||
const key = await toB64(
|
||||
sodium.crypto_pwhash(
|
||||
sodium.crypto_secretbox_KEYBYTES,
|
||||
await fromUTF8(passphrase),
|
||||
await fromB64(salt),
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_ARGON2ID13
|
||||
)
|
||||
);
|
||||
return {
|
||||
key,
|
||||
opsLimit: sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
memLimit: sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateEncryptionKey() {
|
||||
await sodium.ready;
|
||||
return await toB64(sodium.crypto_kdf_keygen());
|
||||
}
|
||||
|
||||
export async function generateSaltToDeriveKey() {
|
||||
await sodium.ready;
|
||||
return await toB64(sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES));
|
||||
}
|
||||
|
||||
export async function generateKeyPair() {
|
||||
await sodium.ready;
|
||||
const keyPair: sodium.KeyPair = sodium.crypto_box_keypair();
|
||||
return {
|
||||
privateKey: await toB64(keyPair.privateKey),
|
||||
publicKey: await toB64(keyPair.publicKey),
|
||||
};
|
||||
}
|
||||
|
||||
export async function boxSealOpen(
|
||||
input: string,
|
||||
publicKey: string,
|
||||
secretKey: string
|
||||
) {
|
||||
await sodium.ready;
|
||||
return await toB64(
|
||||
sodium.crypto_box_seal_open(
|
||||
await fromB64(input),
|
||||
await fromB64(publicKey),
|
||||
await fromB64(secretKey)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function boxSeal(input: string, publicKey: string) {
|
||||
await sodium.ready;
|
||||
return await toB64(
|
||||
sodium.crypto_box_seal(await fromB64(input), await fromB64(publicKey))
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateSubKey(
|
||||
key: string,
|
||||
subKeyLength: number,
|
||||
subKeyID: number,
|
||||
context: string
|
||||
) {
|
||||
await sodium.ready;
|
||||
return await toB64(
|
||||
sodium.crypto_kdf_derive_from_key(
|
||||
subKeyLength,
|
||||
subKeyID,
|
||||
context,
|
||||
await fromB64(key)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function fromB64(input: string) {
|
||||
await sodium.ready;
|
||||
return sodium.from_base64(input, sodium.base64_variants.ORIGINAL);
|
||||
}
|
||||
|
||||
export async function toB64(input: Uint8Array) {
|
||||
await sodium.ready;
|
||||
return sodium.to_base64(input, sodium.base64_variants.ORIGINAL);
|
||||
}
|
||||
|
||||
export async function toURLSafeB64(input: Uint8Array) {
|
||||
await sodium.ready;
|
||||
return sodium.to_base64(input, sodium.base64_variants.URLSAFE);
|
||||
}
|
||||
|
||||
export async function fromUTF8(input: string) {
|
||||
await sodium.ready;
|
||||
return sodium.from_string(input);
|
||||
}
|
||||
|
||||
export async function toUTF8(input: string) {
|
||||
await sodium.ready;
|
||||
return sodium.to_string(await fromB64(input));
|
||||
}
|
||||
export async function toHex(input: string) {
|
||||
await sodium.ready;
|
||||
return sodium.to_hex(await fromB64(input));
|
||||
}
|
||||
|
||||
export async function fromHex(input: string) {
|
||||
await sodium.ready;
|
||||
return await toB64(sodium.from_hex(input));
|
||||
}
|
163
apps/cast/src/utils/error/index.ts
Normal file
163
apps/cast/src/utils/error/index.ts
Normal file
|
@ -0,0 +1,163 @@
|
|||
import { HttpStatusCode } from 'axios';
|
||||
|
||||
export class ApiErrorResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
export class ApiError extends Error {
|
||||
httpStatusCode: number;
|
||||
errCode: string;
|
||||
|
||||
constructor(message: string, errCode: string, httpStatus: number) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.errCode = errCode;
|
||||
this.httpStatusCode = httpStatus;
|
||||
}
|
||||
}
|
||||
|
||||
export function isApiErrorResponse(object: any): object is ApiErrorResponse {
|
||||
return object && 'code' in object && 'message' in object;
|
||||
}
|
||||
|
||||
export const CustomError = {
|
||||
THUMBNAIL_GENERATION_FAILED: 'thumbnail generation failed',
|
||||
VIDEO_PLAYBACK_FAILED: 'video playback failed',
|
||||
ETAG_MISSING: 'no header/etag present in response body',
|
||||
KEY_MISSING: 'encrypted key missing from localStorage',
|
||||
FAILED_TO_LOAD_WEB_WORKER: 'failed to load web worker',
|
||||
CHUNK_MORE_THAN_EXPECTED: 'chunks more than expected',
|
||||
CHUNK_LESS_THAN_EXPECTED: 'chunks less than expected',
|
||||
UNSUPPORTED_FILE_FORMAT: 'unsupported file format',
|
||||
FILE_TOO_LARGE: 'file too large',
|
||||
SUBSCRIPTION_EXPIRED: 'subscription expired',
|
||||
STORAGE_QUOTA_EXCEEDED: 'storage quota exceeded',
|
||||
SESSION_EXPIRED: 'session expired',
|
||||
INVALID_MIME_TYPE: (type: string) => `invalid mime type -${type}`,
|
||||
SIGNUP_FAILED: 'signup failed',
|
||||
FAV_COLLECTION_MISSING: 'favorite collection missing',
|
||||
INVALID_COLLECTION_OPERATION: 'invalid collection operation',
|
||||
TO_MOVE_FILES_FROM_MULTIPLE_COLLECTIONS:
|
||||
'to move files from multiple collections',
|
||||
WAIT_TIME_EXCEEDED: 'operation wait time exceeded',
|
||||
REQUEST_CANCELLED: 'request canceled',
|
||||
REQUEST_FAILED: 'request failed',
|
||||
TOKEN_EXPIRED: 'token expired',
|
||||
TOKEN_MISSING: 'token missing',
|
||||
TOO_MANY_REQUESTS: 'too many requests',
|
||||
BAD_REQUEST: 'bad request',
|
||||
SUBSCRIPTION_NEEDED: 'subscription not present',
|
||||
NOT_FOUND: 'not found ',
|
||||
NO_METADATA: 'no metadata',
|
||||
TOO_LARGE_LIVE_PHOTO_ASSETS: 'too large live photo assets',
|
||||
NOT_A_DATE: 'not a date',
|
||||
NOT_A_LOCATION: 'not a location',
|
||||
FILE_ID_NOT_FOUND: 'file with id not found',
|
||||
WEAK_DEVICE: 'password decryption failed on the device',
|
||||
INCORRECT_PASSWORD: 'incorrect password',
|
||||
UPLOAD_CANCELLED: 'upload cancelled',
|
||||
REQUEST_TIMEOUT: 'request taking too long',
|
||||
HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED:
|
||||
'hidden collection sync file attempted',
|
||||
UNKNOWN_ERROR: 'Something went wrong, please try again',
|
||||
TYPE_DETECTION_FAILED: (fileFormat: string) =>
|
||||
`type detection failed ${fileFormat}`,
|
||||
WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED:
|
||||
'Windows native image processing is not supported',
|
||||
NETWORK_ERROR: 'Network Error',
|
||||
NOT_FILE_OWNER: 'not file owner',
|
||||
UPDATE_EXPORTED_RECORD_FAILED: 'update file exported record failed',
|
||||
EXPORT_STOPPED: 'export stopped',
|
||||
NO_EXPORT_FOLDER_SELECTED: 'no export folder selected',
|
||||
EXPORT_FOLDER_DOES_NOT_EXIST: 'export folder does not exist',
|
||||
NO_INTERNET_CONNECTION: 'no internet connection',
|
||||
AUTH_KEY_NOT_FOUND: 'auth key not found',
|
||||
EXIF_DATA_NOT_FOUND: 'exif data not found',
|
||||
SELECT_FOLDER_ABORTED: 'select folder aborted',
|
||||
NON_MEDIA_FILE: 'non media file',
|
||||
NOT_AVAILABLE_ON_WEB: 'not available on web',
|
||||
UNSUPPORTED_RAW_FORMAT: 'unsupported raw format',
|
||||
NON_PREVIEWABLE_FILE: 'non previewable file',
|
||||
PROCESSING_FAILED: 'processing failed',
|
||||
EXPORT_RECORD_JSON_PARSING_FAILED: 'export record json parsing failed',
|
||||
TWO_FACTOR_ENABLED: 'two factor enabled',
|
||||
CLIENT_ERROR: 'client error',
|
||||
ServerError: 'server error',
|
||||
};
|
||||
|
||||
export function handleUploadError(error): Error {
|
||||
const parsedError = parseUploadErrorCodes(error);
|
||||
|
||||
// breaking errors
|
||||
switch (parsedError.message) {
|
||||
case CustomError.SUBSCRIPTION_EXPIRED:
|
||||
case CustomError.STORAGE_QUOTA_EXCEEDED:
|
||||
case CustomError.SESSION_EXPIRED:
|
||||
case CustomError.UPLOAD_CANCELLED:
|
||||
throw parsedError;
|
||||
}
|
||||
return parsedError;
|
||||
}
|
||||
|
||||
export function errorWithContext(originalError: Error, context: string) {
|
||||
const errorWithContext = new Error(context);
|
||||
errorWithContext.stack =
|
||||
errorWithContext.stack.split('\n').slice(2, 4).join('\n') +
|
||||
'\n' +
|
||||
originalError.stack;
|
||||
return errorWithContext;
|
||||
}
|
||||
|
||||
export function parseUploadErrorCodes(error) {
|
||||
let parsedMessage = null;
|
||||
if (error instanceof ApiError) {
|
||||
switch (error.httpStatusCode) {
|
||||
case HttpStatusCode.PaymentRequired:
|
||||
parsedMessage = CustomError.SUBSCRIPTION_EXPIRED;
|
||||
break;
|
||||
case HttpStatusCode.UpgradeRequired:
|
||||
parsedMessage = CustomError.STORAGE_QUOTA_EXCEEDED;
|
||||
break;
|
||||
case HttpStatusCode.Unauthorized:
|
||||
parsedMessage = CustomError.SESSION_EXPIRED;
|
||||
break;
|
||||
case HttpStatusCode.PayloadTooLarge:
|
||||
parsedMessage = CustomError.FILE_TOO_LARGE;
|
||||
break;
|
||||
default:
|
||||
parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${error.httpStatusCode}`;
|
||||
}
|
||||
} else {
|
||||
parsedMessage = error.message;
|
||||
}
|
||||
return new Error(parsedMessage);
|
||||
}
|
||||
|
||||
export const parseSharingErrorCodes = (error) => {
|
||||
let parsedMessage = null;
|
||||
if (error instanceof ApiError) {
|
||||
switch (error.httpStatusCode) {
|
||||
case HttpStatusCode.BadRequest:
|
||||
parsedMessage = CustomError.BAD_REQUEST;
|
||||
break;
|
||||
case HttpStatusCode.PaymentRequired:
|
||||
parsedMessage = CustomError.SUBSCRIPTION_NEEDED;
|
||||
break;
|
||||
case HttpStatusCode.NotFound:
|
||||
parsedMessage = CustomError.NOT_FOUND;
|
||||
break;
|
||||
case HttpStatusCode.Unauthorized:
|
||||
case HttpStatusCode.Gone:
|
||||
parsedMessage = CustomError.TOKEN_EXPIRED;
|
||||
break;
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
parsedMessage = CustomError.TOO_MANY_REQUESTS;
|
||||
break;
|
||||
default:
|
||||
parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${error.httpStatusCode}`;
|
||||
}
|
||||
} else {
|
||||
parsedMessage = error.message;
|
||||
}
|
||||
return new Error(parsedMessage);
|
||||
};
|
15
apps/cast/src/utils/file/blob.ts
Normal file
15
apps/cast/src/utils/file/blob.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export const readAsDataURL = (blob) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = () => resolve(fileReader.result as string);
|
||||
fileReader.onerror = () => reject(fileReader.error);
|
||||
fileReader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
export const readAsText = (blob) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = () => resolve(fileReader.result as string);
|
||||
fileReader.onerror = () => reject(fileReader.error);
|
||||
fileReader.readAsText(blob);
|
||||
});
|
967
apps/cast/src/utils/file/index.ts
Normal file
967
apps/cast/src/utils/file/index.ts
Normal file
|
@ -0,0 +1,967 @@
|
|||
import { SelectedState } from 'types/gallery';
|
||||
import {
|
||||
EnteFile,
|
||||
EncryptedEnteFile,
|
||||
FileWithUpdatedMagicMetadata,
|
||||
FileMagicMetadata,
|
||||
FileMagicMetadataProps,
|
||||
FilePublicMagicMetadata,
|
||||
FilePublicMagicMetadataProps,
|
||||
} from 'types/file';
|
||||
import { decodeLivePhoto } from 'services/livePhotoService';
|
||||
import { getFileType } from 'services/typeDetectionService';
|
||||
import DownloadManager from 'services/downloadManager';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { User } from 'types/user';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { updateFileCreationDateInEXIF } from 'services/upload/exifService';
|
||||
import {
|
||||
TYPE_JPEG,
|
||||
TYPE_JPG,
|
||||
TYPE_HEIC,
|
||||
TYPE_HEIF,
|
||||
FILE_TYPE,
|
||||
SUPPORTED_RAW_FORMATS,
|
||||
RAW_FORMATS,
|
||||
} from 'constants/file';
|
||||
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
|
||||
import heicConversionService from 'services/heicConversionService';
|
||||
import * as ffmpegService from 'services/ffmpeg/ffmpegService';
|
||||
import { VISIBILITY_STATE } from 'types/magicMetadata';
|
||||
import { isArchivedFile, updateMagicMetadata } from 'utils/magicMetadata';
|
||||
|
||||
import { addLocalLog, addLogLine } from 'utils/logging';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { convertBytesToHumanReadable } from './size';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
import {
|
||||
deleteFromTrash,
|
||||
trashFiles,
|
||||
updateFileMagicMetadata,
|
||||
updateFilePublicMagicMetadata,
|
||||
} from 'services/fileService';
|
||||
import isElectron from 'is-electron';
|
||||
import imageProcessor from 'services/electron/imageProcessor';
|
||||
import { isPlaybackPossible } from 'utils/photoFrame';
|
||||
import { FileTypeInfo } from 'types/upload';
|
||||
import { moveToHiddenCollection } from 'services/collectionService';
|
||||
|
||||
import ElectronFSService from 'services/electron/fs';
|
||||
import { getFileExportPath, getUniqueFileExportName } from 'utils/export';
|
||||
|
||||
const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000;
|
||||
|
||||
export enum FILE_OPS_TYPE {
|
||||
DOWNLOAD,
|
||||
FIX_TIME,
|
||||
ARCHIVE,
|
||||
UNARCHIVE,
|
||||
HIDE,
|
||||
TRASH,
|
||||
DELETE_PERMANENTLY,
|
||||
}
|
||||
|
||||
export function downloadAsFile(filename: string, content: string) {
|
||||
const file = new Blob([content], {
|
||||
type: 'text/plain',
|
||||
});
|
||||
const fileURL = URL.createObjectURL(file);
|
||||
downloadUsingAnchor(fileURL, filename);
|
||||
}
|
||||
|
||||
export async function getUpdatedEXIFFileForDownload(
|
||||
fileReader: FileReader,
|
||||
file: EnteFile,
|
||||
fileStream: ReadableStream<Uint8Array>
|
||||
): Promise<ReadableStream<Uint8Array>> {
|
||||
const extension = getFileExtension(file.metadata.title);
|
||||
if (
|
||||
file.metadata.fileType === FILE_TYPE.IMAGE &&
|
||||
file.pubMagicMetadata?.data.editedTime &&
|
||||
(extension === TYPE_JPEG || extension === TYPE_JPG)
|
||||
) {
|
||||
const fileBlob = await new Response(fileStream).blob();
|
||||
const updatedFileBlob = await updateFileCreationDateInEXIF(
|
||||
fileReader,
|
||||
fileBlob,
|
||||
new Date(file.pubMagicMetadata.data.editedTime / 1000)
|
||||
);
|
||||
return updatedFileBlob.stream();
|
||||
} else {
|
||||
return fileStream;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFile(
|
||||
file: EnteFile,
|
||||
accessedThroughSharedURL: boolean,
|
||||
token?: string,
|
||||
passwordToken?: string
|
||||
) {
|
||||
try {
|
||||
let fileBlob: Blob;
|
||||
const fileReader = new FileReader();
|
||||
if (accessedThroughSharedURL) {
|
||||
const fileURL =
|
||||
await PublicCollectionDownloadManager.getCachedOriginalFile(
|
||||
file
|
||||
)[0];
|
||||
if (!fileURL) {
|
||||
fileBlob = await new Response(
|
||||
await PublicCollectionDownloadManager.downloadFile(
|
||||
token,
|
||||
passwordToken,
|
||||
file
|
||||
)
|
||||
).blob();
|
||||
} else {
|
||||
fileBlob = await (await fetch(fileURL)).blob();
|
||||
}
|
||||
} else {
|
||||
const fileURL = await DownloadManager.getCachedOriginalFile(
|
||||
file
|
||||
)[0];
|
||||
if (!fileURL) {
|
||||
fileBlob = await new Response(
|
||||
await DownloadManager.downloadFile(file)
|
||||
).blob();
|
||||
} else {
|
||||
fileBlob = await (await fetch(fileURL)).blob();
|
||||
}
|
||||
}
|
||||
|
||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
const image = new File([livePhoto.image], livePhoto.imageNameTitle);
|
||||
const imageType = await getFileType(image);
|
||||
const tempImageURL = URL.createObjectURL(
|
||||
new Blob([livePhoto.image], { type: imageType.mimeType })
|
||||
);
|
||||
const video = new File([livePhoto.video], livePhoto.videoNameTitle);
|
||||
const videoType = await getFileType(video);
|
||||
const tempVideoURL = URL.createObjectURL(
|
||||
new Blob([livePhoto.video], { type: videoType.mimeType })
|
||||
);
|
||||
downloadUsingAnchor(tempImageURL, livePhoto.imageNameTitle);
|
||||
downloadUsingAnchor(tempVideoURL, livePhoto.videoNameTitle);
|
||||
} else {
|
||||
const fileType = await getFileType(
|
||||
new File([fileBlob], file.metadata.title)
|
||||
);
|
||||
fileBlob = await new Response(
|
||||
await getUpdatedEXIFFileForDownload(
|
||||
fileReader,
|
||||
file,
|
||||
fileBlob.stream()
|
||||
)
|
||||
).blob();
|
||||
fileBlob = new Blob([fileBlob], { type: fileType.mimeType });
|
||||
const tempURL = URL.createObjectURL(fileBlob);
|
||||
downloadUsingAnchor(tempURL, file.metadata.title);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'failed to download file');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadUsingAnchor(link: string, name: string) {
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = link;
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(link);
|
||||
a.remove();
|
||||
}
|
||||
|
||||
export function groupFilesBasedOnCollectionID(files: EnteFile[]) {
|
||||
const collectionWiseFiles = new Map<number, EnteFile[]>();
|
||||
for (const file of files) {
|
||||
if (!collectionWiseFiles.has(file.collectionID)) {
|
||||
collectionWiseFiles.set(file.collectionID, []);
|
||||
}
|
||||
collectionWiseFiles.get(file.collectionID).push(file);
|
||||
}
|
||||
return collectionWiseFiles;
|
||||
}
|
||||
|
||||
function getSelectedFileIds(selectedFiles: SelectedState) {
|
||||
const filesIDs: number[] = [];
|
||||
for (const [key, val] of Object.entries(selectedFiles)) {
|
||||
if (typeof val === 'boolean' && val) {
|
||||
filesIDs.push(Number(key));
|
||||
}
|
||||
}
|
||||
return new Set(filesIDs);
|
||||
}
|
||||
export function getSelectedFiles(
|
||||
selected: SelectedState,
|
||||
files: EnteFile[]
|
||||
): EnteFile[] {
|
||||
const selectedFilesIDs = getSelectedFileIds(selected);
|
||||
return files.filter((file) => selectedFilesIDs.has(file.id));
|
||||
}
|
||||
|
||||
export function sortFiles(files: EnteFile[], sortAsc = false) {
|
||||
// sort based on the time of creation time of the file,
|
||||
// for files with same creation time, sort based on the time of last modification
|
||||
const factor = sortAsc ? -1 : 1;
|
||||
return files.sort((a, b) => {
|
||||
if (a.metadata.creationTime === b.metadata.creationTime) {
|
||||
return (
|
||||
factor *
|
||||
(b.metadata.modificationTime - a.metadata.modificationTime)
|
||||
);
|
||||
}
|
||||
return factor * (b.metadata.creationTime - a.metadata.creationTime);
|
||||
});
|
||||
}
|
||||
|
||||
export function sortTrashFiles(files: EnteFile[]) {
|
||||
return files.sort((a, b) => {
|
||||
if (a.deleteBy === b.deleteBy) {
|
||||
if (a.metadata.creationTime === b.metadata.creationTime) {
|
||||
return (
|
||||
b.metadata.modificationTime - a.metadata.modificationTime
|
||||
);
|
||||
}
|
||||
return b.metadata.creationTime - a.metadata.creationTime;
|
||||
}
|
||||
return a.deleteBy - b.deleteBy;
|
||||
});
|
||||
}
|
||||
|
||||
export async function decryptFile(
|
||||
file: EncryptedEnteFile,
|
||||
collectionKey: string
|
||||
): Promise<EnteFile> {
|
||||
try {
|
||||
const worker = await ComlinkCryptoWorker.getInstance();
|
||||
const {
|
||||
encryptedKey,
|
||||
keyDecryptionNonce,
|
||||
metadata,
|
||||
magicMetadata,
|
||||
pubMagicMetadata,
|
||||
...restFileProps
|
||||
} = file;
|
||||
const fileKey = await worker.decryptB64(
|
||||
encryptedKey,
|
||||
keyDecryptionNonce,
|
||||
collectionKey
|
||||
);
|
||||
const fileMetadata = await worker.decryptMetadata(
|
||||
metadata.encryptedData,
|
||||
metadata.decryptionHeader,
|
||||
fileKey
|
||||
);
|
||||
let fileMagicMetadata: FileMagicMetadata;
|
||||
let filePubMagicMetadata: FilePublicMagicMetadata;
|
||||
if (magicMetadata?.data) {
|
||||
fileMagicMetadata = {
|
||||
...file.magicMetadata,
|
||||
data: await worker.decryptMetadata(
|
||||
magicMetadata.data,
|
||||
magicMetadata.header,
|
||||
fileKey
|
||||
),
|
||||
};
|
||||
}
|
||||
if (pubMagicMetadata?.data) {
|
||||
filePubMagicMetadata = {
|
||||
...pubMagicMetadata,
|
||||
data: await worker.decryptMetadata(
|
||||
pubMagicMetadata.data,
|
||||
pubMagicMetadata.header,
|
||||
fileKey
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...restFileProps,
|
||||
key: fileKey,
|
||||
metadata: fileMetadata,
|
||||
magicMetadata: fileMagicMetadata,
|
||||
pubMagicMetadata: filePubMagicMetadata,
|
||||
};
|
||||
} catch (e) {
|
||||
logError(e, 'file decryption failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFileNameWithoutExtension(filename: string) {
|
||||
const lastDotPosition = filename.lastIndexOf('.');
|
||||
if (lastDotPosition === -1) return filename;
|
||||
else return filename.slice(0, lastDotPosition);
|
||||
}
|
||||
|
||||
export function getFileExtensionWithDot(filename: string) {
|
||||
const lastDotPosition = filename.lastIndexOf('.');
|
||||
if (lastDotPosition === -1) return '';
|
||||
else return filename.slice(lastDotPosition);
|
||||
}
|
||||
|
||||
export function splitFilenameAndExtension(filename: string): [string, string] {
|
||||
const lastDotPosition = filename.lastIndexOf('.');
|
||||
if (lastDotPosition === -1) return [filename, null];
|
||||
else
|
||||
return [
|
||||
filename.slice(0, lastDotPosition),
|
||||
filename.slice(lastDotPosition + 1),
|
||||
];
|
||||
}
|
||||
|
||||
export function getFileExtension(filename: string) {
|
||||
return splitFilenameAndExtension(filename)[1]?.toLocaleLowerCase();
|
||||
}
|
||||
|
||||
export function generateStreamFromArrayBuffer(data: Uint8Array) {
|
||||
return new ReadableStream({
|
||||
async start(controller: ReadableStreamDefaultController) {
|
||||
controller.enqueue(data);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRenderableFileURL(file: EnteFile, fileBlob: Blob) {
|
||||
switch (file.metadata.fileType) {
|
||||
case FILE_TYPE.IMAGE: {
|
||||
const convertedBlob = await getRenderableImage(
|
||||
file.metadata.title,
|
||||
fileBlob
|
||||
);
|
||||
const { originalURL, convertedURL } = getFileObjectURLs(
|
||||
fileBlob,
|
||||
convertedBlob
|
||||
);
|
||||
return {
|
||||
converted: [convertedURL],
|
||||
original: [originalURL],
|
||||
};
|
||||
}
|
||||
case FILE_TYPE.LIVE_PHOTO: {
|
||||
return await getRenderableLivePhotoURL(file, fileBlob);
|
||||
}
|
||||
case FILE_TYPE.VIDEO: {
|
||||
const convertedBlob = await getPlayableVideo(
|
||||
file.metadata.title,
|
||||
fileBlob
|
||||
);
|
||||
const { originalURL, convertedURL } = getFileObjectURLs(
|
||||
fileBlob,
|
||||
convertedBlob
|
||||
);
|
||||
return {
|
||||
converted: [convertedURL],
|
||||
original: [originalURL],
|
||||
};
|
||||
}
|
||||
default: {
|
||||
const previewURL = await createTypedObjectURL(
|
||||
fileBlob,
|
||||
file.metadata.title
|
||||
);
|
||||
return {
|
||||
converted: [previewURL],
|
||||
original: [previewURL],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getRenderableLivePhotoURL(
|
||||
file: EnteFile,
|
||||
fileBlob: Blob
|
||||
): Promise<{ original: string[]; converted: string[] }> {
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
const imageBlob = new Blob([livePhoto.image]);
|
||||
const videoBlob = new Blob([livePhoto.video]);
|
||||
const convertedImageBlob = await getRenderableImage(
|
||||
livePhoto.imageNameTitle,
|
||||
imageBlob
|
||||
);
|
||||
const convertedVideoBlob = await getPlayableVideo(
|
||||
livePhoto.videoNameTitle,
|
||||
videoBlob,
|
||||
true
|
||||
);
|
||||
const { originalURL: originalImageURL, convertedURL: convertedImageURL } =
|
||||
getFileObjectURLs(imageBlob, convertedImageBlob);
|
||||
|
||||
const { originalURL: originalVideoURL, convertedURL: convertedVideoURL } =
|
||||
getFileObjectURLs(videoBlob, convertedVideoBlob);
|
||||
return {
|
||||
converted: [convertedImageURL, convertedVideoURL],
|
||||
original: [originalImageURL, originalVideoURL],
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPlayableVideo(
|
||||
videoNameTitle: string,
|
||||
videoBlob: Blob,
|
||||
forceConvert = false
|
||||
) {
|
||||
try {
|
||||
const isPlayable = await isPlaybackPossible(
|
||||
URL.createObjectURL(videoBlob)
|
||||
);
|
||||
if (isPlayable && !forceConvert) {
|
||||
return videoBlob;
|
||||
} else {
|
||||
if (!forceConvert && !isElectron()) {
|
||||
return null;
|
||||
}
|
||||
addLogLine(
|
||||
'video format not supported, converting it name:',
|
||||
videoNameTitle
|
||||
);
|
||||
const mp4ConvertedVideo = await ffmpegService.convertToMP4(
|
||||
new File([videoBlob], videoNameTitle)
|
||||
);
|
||||
addLogLine('video successfully converted', videoNameTitle);
|
||||
return new Blob([await mp4ConvertedVideo.arrayBuffer()]);
|
||||
}
|
||||
} catch (e) {
|
||||
addLogLine('video conversion failed', videoNameTitle);
|
||||
logError(e, 'video conversion failed');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRenderableImage(fileName: string, imageBlob: Blob) {
|
||||
let fileTypeInfo: FileTypeInfo;
|
||||
try {
|
||||
const tempFile = new File([imageBlob], fileName);
|
||||
fileTypeInfo = await getFileType(tempFile);
|
||||
addLocalLog(() => `file type info: ${JSON.stringify(fileTypeInfo)}`);
|
||||
const { exactType } = fileTypeInfo;
|
||||
let convertedImageBlob: Blob;
|
||||
if (isRawFile(exactType)) {
|
||||
try {
|
||||
if (!isSupportedRawFormat(exactType)) {
|
||||
throw Error(CustomError.UNSUPPORTED_RAW_FORMAT);
|
||||
}
|
||||
|
||||
if (!isElectron()) {
|
||||
throw Error(CustomError.NOT_AVAILABLE_ON_WEB);
|
||||
}
|
||||
addLogLine(
|
||||
`RawConverter called for ${fileName}-${convertBytesToHumanReadable(
|
||||
imageBlob.size
|
||||
)}`
|
||||
);
|
||||
convertedImageBlob = await imageProcessor.convertToJPEG(
|
||||
imageBlob,
|
||||
fileName
|
||||
);
|
||||
addLogLine(`${fileName} successfully converted`);
|
||||
} catch (e) {
|
||||
try {
|
||||
if (!isFileHEIC(exactType)) {
|
||||
throw e;
|
||||
}
|
||||
addLogLine(
|
||||
`HEICConverter called for ${fileName}-${convertBytesToHumanReadable(
|
||||
imageBlob.size
|
||||
)}`
|
||||
);
|
||||
convertedImageBlob = await heicConversionService.convert(
|
||||
imageBlob
|
||||
);
|
||||
addLogLine(`${fileName} successfully converted`);
|
||||
} catch (e) {
|
||||
throw Error(CustomError.NON_PREVIEWABLE_FILE);
|
||||
}
|
||||
}
|
||||
return convertedImageBlob;
|
||||
} else {
|
||||
return imageBlob;
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'get Renderable Image failed', { fileTypeInfo });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isFileHEIC(exactType: string) {
|
||||
return (
|
||||
exactType.toLowerCase().endsWith(TYPE_HEIC) ||
|
||||
exactType.toLowerCase().endsWith(TYPE_HEIF)
|
||||
);
|
||||
}
|
||||
|
||||
export function isRawFile(exactType: string) {
|
||||
return RAW_FORMATS.includes(exactType.toLowerCase());
|
||||
}
|
||||
|
||||
export function isRawFileFromFileName(fileName: string) {
|
||||
for (const rawFormat of RAW_FORMATS) {
|
||||
if (fileName.toLowerCase().endsWith(rawFormat)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isSupportedRawFormat(exactType: string) {
|
||||
return SUPPORTED_RAW_FORMATS.includes(exactType.toLowerCase());
|
||||
}
|
||||
|
||||
export async function changeFilesVisibility(
|
||||
files: EnteFile[],
|
||||
visibility: VISIBILITY_STATE
|
||||
): Promise<EnteFile[]> {
|
||||
const fileWithUpdatedMagicMetadataList: FileWithUpdatedMagicMetadata[] = [];
|
||||
for (const file of files) {
|
||||
const updatedMagicMetadataProps: FileMagicMetadataProps = {
|
||||
visibility,
|
||||
};
|
||||
|
||||
fileWithUpdatedMagicMetadataList.push({
|
||||
file,
|
||||
updatedMagicMetadata: await updateMagicMetadata(
|
||||
updatedMagicMetadataProps,
|
||||
file.magicMetadata,
|
||||
file.key
|
||||
),
|
||||
});
|
||||
}
|
||||
return await updateFileMagicMetadata(fileWithUpdatedMagicMetadataList);
|
||||
}
|
||||
|
||||
export async function changeFileCreationTime(
|
||||
file: EnteFile,
|
||||
editedTime: number
|
||||
): Promise<EnteFile> {
|
||||
const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = {
|
||||
editedTime,
|
||||
};
|
||||
const updatedPublicMagicMetadata: FilePublicMagicMetadata =
|
||||
await updateMagicMetadata(
|
||||
updatedPublicMagicMetadataProps,
|
||||
file.pubMagicMetadata,
|
||||
file.key
|
||||
);
|
||||
const updateResult = await updateFilePublicMagicMetadata([
|
||||
{ file, updatedPublicMagicMetadata },
|
||||
]);
|
||||
return updateResult[0];
|
||||
}
|
||||
|
||||
export async function changeFileName(
|
||||
file: EnteFile,
|
||||
editedName: string
|
||||
): Promise<EnteFile> {
|
||||
const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = {
|
||||
editedName,
|
||||
};
|
||||
|
||||
const updatedPublicMagicMetadata: FilePublicMagicMetadata =
|
||||
await updateMagicMetadata(
|
||||
updatedPublicMagicMetadataProps,
|
||||
file.pubMagicMetadata,
|
||||
file.key
|
||||
);
|
||||
const updateResult = await updateFilePublicMagicMetadata([
|
||||
{ file, updatedPublicMagicMetadata },
|
||||
]);
|
||||
return updateResult[0];
|
||||
}
|
||||
|
||||
export async function changeCaption(
|
||||
file: EnteFile,
|
||||
caption: string
|
||||
): Promise<EnteFile> {
|
||||
const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = {
|
||||
caption,
|
||||
};
|
||||
|
||||
const updatedPublicMagicMetadata: FilePublicMagicMetadata =
|
||||
await updateMagicMetadata(
|
||||
updatedPublicMagicMetadataProps,
|
||||
file.pubMagicMetadata,
|
||||
file.key
|
||||
);
|
||||
const updateResult = await updateFilePublicMagicMetadata([
|
||||
{ file, updatedPublicMagicMetadata },
|
||||
]);
|
||||
return updateResult[0];
|
||||
}
|
||||
|
||||
export function isSharedFile(user: User, file: EnteFile) {
|
||||
if (!user?.id || !file?.ownerID) {
|
||||
return false;
|
||||
}
|
||||
return file.ownerID !== user.id;
|
||||
}
|
||||
|
||||
export function mergeMetadata(files: EnteFile[]): EnteFile[] {
|
||||
return files.map((file) => {
|
||||
if (file.pubMagicMetadata?.data.editedTime) {
|
||||
file.metadata.creationTime = file.pubMagicMetadata.data.editedTime;
|
||||
}
|
||||
if (file.pubMagicMetadata?.data.editedName) {
|
||||
file.metadata.title = file.pubMagicMetadata.data.editedName;
|
||||
}
|
||||
|
||||
return file;
|
||||
});
|
||||
}
|
||||
|
||||
export function updateExistingFilePubMetadata(
|
||||
existingFile: EnteFile,
|
||||
updatedFile: EnteFile
|
||||
) {
|
||||
existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata;
|
||||
existingFile.metadata = mergeMetadata([existingFile])[0].metadata;
|
||||
}
|
||||
|
||||
export async function getFileFromURL(fileURL: string) {
|
||||
const fileBlob = await (await fetch(fileURL)).blob();
|
||||
const fileFile = new File([fileBlob], 'temp');
|
||||
return fileFile;
|
||||
}
|
||||
|
||||
export function getUniqueFiles(files: EnteFile[]) {
|
||||
const idSet = new Set<number>();
|
||||
const uniqueFiles = files.filter((file) => {
|
||||
if (!idSet.has(file.id)) {
|
||||
idSet.add(file.id);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return uniqueFiles;
|
||||
}
|
||||
|
||||
export async function downloadFiles(
|
||||
files: EnteFile[],
|
||||
progressBarUpdater?: {
|
||||
increaseSuccess: () => void;
|
||||
increaseFailed: () => void;
|
||||
isCancelled: () => boolean;
|
||||
}
|
||||
) {
|
||||
for (const file of files) {
|
||||
try {
|
||||
if (progressBarUpdater?.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
await downloadFile(file, false);
|
||||
progressBarUpdater?.increaseSuccess();
|
||||
} catch (e) {
|
||||
logError(e, 'download fail for file');
|
||||
progressBarUpdater?.increaseFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFilesDesktop(
|
||||
files: EnteFile[],
|
||||
progressBarUpdater: {
|
||||
increaseSuccess: () => void;
|
||||
increaseFailed: () => void;
|
||||
isCancelled: () => boolean;
|
||||
},
|
||||
downloadPath: string
|
||||
) {
|
||||
const fileReader = new FileReader();
|
||||
for (const file of files) {
|
||||
try {
|
||||
if (progressBarUpdater?.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
await downloadFileDesktop(fileReader, file, downloadPath);
|
||||
progressBarUpdater?.increaseSuccess();
|
||||
} catch (e) {
|
||||
logError(e, 'download fail for file');
|
||||
progressBarUpdater?.increaseFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFileDesktop(
|
||||
fileReader: FileReader,
|
||||
file: EnteFile,
|
||||
downloadPath: string
|
||||
) {
|
||||
let fileStream: ReadableStream<Uint8Array>;
|
||||
const fileURL = await DownloadManager.getCachedOriginalFile(file)[0];
|
||||
if (!fileURL) {
|
||||
fileStream = await DownloadManager.downloadFile(file);
|
||||
} else {
|
||||
fileStream = await fetch(fileURL).then((res) => res.body);
|
||||
}
|
||||
const updatedFileStream = await getUpdatedEXIFFileForDownload(
|
||||
fileReader,
|
||||
file,
|
||||
fileStream
|
||||
);
|
||||
|
||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
const fileBlob = await new Response(updatedFileStream).blob();
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
const imageExportName = getUniqueFileExportName(
|
||||
downloadPath,
|
||||
livePhoto.imageNameTitle
|
||||
);
|
||||
const imageStream = generateStreamFromArrayBuffer(livePhoto.image);
|
||||
await ElectronFSService.saveMediaFile(
|
||||
getFileExportPath(downloadPath, imageExportName),
|
||||
imageStream
|
||||
);
|
||||
try {
|
||||
const videoExportName = getUniqueFileExportName(
|
||||
downloadPath,
|
||||
livePhoto.videoNameTitle
|
||||
);
|
||||
const videoStream = generateStreamFromArrayBuffer(livePhoto.video);
|
||||
await ElectronFSService.saveMediaFile(
|
||||
getFileExportPath(downloadPath, videoExportName),
|
||||
videoStream
|
||||
);
|
||||
} catch (e) {
|
||||
ElectronFSService.deleteFile(
|
||||
getFileExportPath(downloadPath, imageExportName)
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
const fileExportName = getUniqueFileExportName(
|
||||
downloadPath,
|
||||
file.metadata.title
|
||||
);
|
||||
await ElectronFSService.saveMediaFile(
|
||||
getFileExportPath(downloadPath, fileExportName),
|
||||
updatedFileStream
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const isImageOrVideo = (fileType: FILE_TYPE) =>
|
||||
[FILE_TYPE.IMAGE, FILE_TYPE.VIDEO].includes(fileType);
|
||||
|
||||
export const getArchivedFiles = (files: EnteFile[]) => {
|
||||
return files.filter(isArchivedFile).map((file) => file.id);
|
||||
};
|
||||
|
||||
export const createTypedObjectURL = async (blob: Blob, fileName: string) => {
|
||||
const type = await getFileType(new File([blob], fileName));
|
||||
return URL.createObjectURL(new Blob([blob], { type: type.mimeType }));
|
||||
};
|
||||
|
||||
export const getUserOwnedFiles = (files: EnteFile[]) => {
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
if (!user?.id) {
|
||||
throw Error('user missing');
|
||||
}
|
||||
return files.filter((file) => file.ownerID === user.id);
|
||||
};
|
||||
|
||||
// doesn't work on firefox
|
||||
export const copyFileToClipboard = async (fileUrl: string) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvasCTX = canvas.getContext('2d');
|
||||
const image = new Image();
|
||||
|
||||
const blobPromise = new Promise<Blob>((resolve, reject) => {
|
||||
let timeout: NodeJS.Timeout = null;
|
||||
try {
|
||||
image.setAttribute('src', fileUrl);
|
||||
image.onload = () => {
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
canvasCTX.drawImage(image, 0, 0, image.width, image.height);
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
resolve(blob);
|
||||
},
|
||||
'image/png',
|
||||
1
|
||||
);
|
||||
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
} catch (e) {
|
||||
void logError(e, 'failed to copy to clipboard');
|
||||
reject(e);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(
|
||||
() => reject(Error(CustomError.WAIT_TIME_EXCEEDED)),
|
||||
WAIT_TIME_IMAGE_CONVERSION
|
||||
);
|
||||
});
|
||||
|
||||
const { ClipboardItem } = window;
|
||||
|
||||
await navigator.clipboard
|
||||
.write([new ClipboardItem({ 'image/png': blobPromise })])
|
||||
.catch((e) => logError(e, 'failed to copy to clipboard'));
|
||||
};
|
||||
|
||||
export function getLatestVersionFiles(files: EnteFile[]) {
|
||||
const latestVersionFiles = new Map<string, EnteFile>();
|
||||
files.forEach((file) => {
|
||||
const uid = `${file.collectionID}-${file.id}`;
|
||||
if (
|
||||
!latestVersionFiles.has(uid) ||
|
||||
latestVersionFiles.get(uid).updationTime < file.updationTime
|
||||
) {
|
||||
latestVersionFiles.set(uid, file);
|
||||
}
|
||||
});
|
||||
return Array.from(latestVersionFiles.values()).filter(
|
||||
(file) => !file.isDeleted
|
||||
);
|
||||
}
|
||||
|
||||
export function getPersonalFiles(files: EnteFile[], user: User) {
|
||||
if (!user?.id) {
|
||||
throw Error('user missing');
|
||||
}
|
||||
return files.filter((file) => file.ownerID === user.id);
|
||||
}
|
||||
|
||||
export function getIDBasedSortedFiles(files: EnteFile[]) {
|
||||
return files.sort((a, b) => a.id - b.id);
|
||||
}
|
||||
|
||||
export function constructFileToCollectionMap(files: EnteFile[]) {
|
||||
const fileToCollectionsMap = new Map<number, number[]>();
|
||||
(files ?? []).forEach((file) => {
|
||||
if (!fileToCollectionsMap.get(file.id)) {
|
||||
fileToCollectionsMap.set(file.id, []);
|
||||
}
|
||||
fileToCollectionsMap.get(file.id).push(file.collectionID);
|
||||
});
|
||||
return fileToCollectionsMap;
|
||||
}
|
||||
|
||||
export const shouldShowAvatar = (file: EnteFile, user: User) => {
|
||||
if (!file || !user) {
|
||||
return false;
|
||||
}
|
||||
// is Shared file
|
||||
else if (file.ownerID !== user.id) {
|
||||
return true;
|
||||
}
|
||||
// is public collected file
|
||||
else if (
|
||||
file.ownerID === user.id &&
|
||||
file.pubMagicMetadata?.data?.uploaderName
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const handleFileOps = async (
|
||||
ops: FILE_OPS_TYPE,
|
||||
files: EnteFile[],
|
||||
setDeletedFileIds: (
|
||||
deletedFileIds: Set<number> | ((prev: Set<number>) => Set<number>)
|
||||
) => void,
|
||||
setHiddenFileIds: (
|
||||
hiddenFileIds: Set<number> | ((prev: Set<number>) => Set<number>)
|
||||
) => void,
|
||||
setFixCreationTimeAttributes: (
|
||||
fixCreationTimeAttributes:
|
||||
| {
|
||||
files: EnteFile[];
|
||||
}
|
||||
| ((prev: { files: EnteFile[] }) => { files: EnteFile[] })
|
||||
) => void
|
||||
) => {
|
||||
switch (ops) {
|
||||
case FILE_OPS_TYPE.TRASH:
|
||||
await deleteFileHelper(files, false, setDeletedFileIds);
|
||||
break;
|
||||
case FILE_OPS_TYPE.DELETE_PERMANENTLY:
|
||||
await deleteFileHelper(files, true, setDeletedFileIds);
|
||||
break;
|
||||
case FILE_OPS_TYPE.HIDE:
|
||||
await hideFilesHelper(files, setHiddenFileIds);
|
||||
break;
|
||||
case FILE_OPS_TYPE.DOWNLOAD:
|
||||
await downloadFiles(files);
|
||||
break;
|
||||
case FILE_OPS_TYPE.FIX_TIME:
|
||||
fixTimeHelper(files, setFixCreationTimeAttributes);
|
||||
break;
|
||||
case FILE_OPS_TYPE.ARCHIVE:
|
||||
await changeFilesVisibility(files, VISIBILITY_STATE.ARCHIVED);
|
||||
break;
|
||||
case FILE_OPS_TYPE.UNARCHIVE:
|
||||
await changeFilesVisibility(files, VISIBILITY_STATE.VISIBLE);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFileHelper = async (
|
||||
selectedFiles: EnteFile[],
|
||||
permanent: boolean,
|
||||
setDeletedFileIds: (
|
||||
deletedFileIds: Set<number> | ((prev: Set<number>) => Set<number>)
|
||||
) => void
|
||||
) => {
|
||||
try {
|
||||
setDeletedFileIds((deletedFileIds) => {
|
||||
selectedFiles.forEach((file) => deletedFileIds.add(file.id));
|
||||
return new Set(deletedFileIds);
|
||||
});
|
||||
if (permanent) {
|
||||
await deleteFromTrash(selectedFiles.map((file) => file.id));
|
||||
} else {
|
||||
await trashFiles(selectedFiles);
|
||||
}
|
||||
} catch (e) {
|
||||
setDeletedFileIds(new Set());
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const hideFilesHelper = async (
|
||||
selectedFiles: EnteFile[],
|
||||
setHiddenFileIds: (
|
||||
hiddenFileIds: Set<number> | ((prev: Set<number>) => Set<number>)
|
||||
) => void
|
||||
) => {
|
||||
try {
|
||||
setHiddenFileIds((hiddenFileIds) => {
|
||||
selectedFiles.forEach((file) => hiddenFileIds.add(file.id));
|
||||
return new Set(hiddenFileIds);
|
||||
});
|
||||
await moveToHiddenCollection(selectedFiles);
|
||||
} catch (e) {
|
||||
setHiddenFileIds(new Set());
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const fixTimeHelper = async (
|
||||
selectedFiles: EnteFile[],
|
||||
setFixCreationTimeAttributes: (fixCreationTimeAttributes: {
|
||||
files: EnteFile[];
|
||||
}) => void
|
||||
) => {
|
||||
setFixCreationTimeAttributes({ files: selectedFiles });
|
||||
};
|
||||
|
||||
const getFileObjectURLs = (originalBlob: Blob, convertedBlob: Blob) => {
|
||||
const originalURL = URL.createObjectURL(originalBlob);
|
||||
const convertedURL = convertedBlob
|
||||
? convertedBlob === originalBlob
|
||||
? originalURL
|
||||
: URL.createObjectURL(convertedBlob)
|
||||
: null;
|
||||
return { originalURL, convertedURL };
|
||||
};
|
42
apps/cast/src/utils/file/livePhoto.ts
Normal file
42
apps/cast/src/utils/file/livePhoto.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { FILE_TYPE } from 'constants/file';
|
||||
import { getFileExtension } from 'utils/file';
|
||||
|
||||
const IMAGE_EXTENSIONS = [
|
||||
'heic',
|
||||
'heif',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'png',
|
||||
'gif',
|
||||
'bmp',
|
||||
'tiff',
|
||||
'webp',
|
||||
];
|
||||
|
||||
const VIDEO_EXTENSIONS = [
|
||||
'mov',
|
||||
'mp4',
|
||||
'm4v',
|
||||
'avi',
|
||||
'wmv',
|
||||
'flv',
|
||||
'mkv',
|
||||
'webm',
|
||||
'3gp',
|
||||
'3g2',
|
||||
'avi',
|
||||
'ogv',
|
||||
'mpg',
|
||||
'mp',
|
||||
];
|
||||
|
||||
export function getFileTypeFromExtensionForLivePhotoClustering(
|
||||
filename: string
|
||||
) {
|
||||
const extension = getFileExtension(filename)?.toLowerCase();
|
||||
if (IMAGE_EXTENSIONS.includes(extension)) {
|
||||
return FILE_TYPE.IMAGE;
|
||||
} else if (VIDEO_EXTENSIONS.includes(extension)) {
|
||||
return FILE_TYPE.VIDEO;
|
||||
}
|
||||
}
|
12
apps/cast/src/utils/file/size.ts
Normal file
12
apps/cast/src/utils/file/size.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export function convertBytesToHumanReadable(
|
||||
bytes: number,
|
||||
precision = 2
|
||||
): string {
|
||||
if (bytes === 0 || isNaN(bytes)) {
|
||||
return '0 MB';
|
||||
}
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
return (bytes / Math.pow(1024, i)).toFixed(precision) + ' ' + sizes[i];
|
||||
}
|
116
apps/cast/src/utils/logging/index.ts
Normal file
116
apps/cast/src/utils/logging/index.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
import { ElectronFile } from 'types/upload';
|
||||
import { convertBytesToHumanReadable } from 'utils/file/size';
|
||||
import { formatDateTimeShort } from 'utils/time/format';
|
||||
import { isDEVSentryENV } from 'constants/sentry';
|
||||
import isElectron from 'is-electron';
|
||||
// import ElectronService from 'services/electron/common';
|
||||
import { logError } from 'utils/sentry';
|
||||
import {
|
||||
getData,
|
||||
LS_KEYS,
|
||||
removeData,
|
||||
setData,
|
||||
} from 'utils/storage/localStorage';
|
||||
|
||||
export const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
export const MAX_LOG_LINES = 1000;
|
||||
|
||||
export interface Log {
|
||||
timestamp: number;
|
||||
logLine: string;
|
||||
}
|
||||
|
||||
export function addLogLine(
|
||||
log: string | number | boolean,
|
||||
...optionalParams: (string | number | boolean)[]
|
||||
) {
|
||||
try {
|
||||
const completeLog = [log, ...optionalParams].join(' ');
|
||||
if (isDEVSentryENV()) {
|
||||
console.log(completeLog);
|
||||
}
|
||||
|
||||
if (isElectron()) {
|
||||
// ElectronService.logToDisk(completeLog);
|
||||
} else {
|
||||
saveLogLine({
|
||||
timestamp: Date.now(),
|
||||
logLine: completeLog,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name === 'QuotaExceededError') {
|
||||
deleteLogs();
|
||||
addLogLine('logs cleared');
|
||||
}
|
||||
logError(e, 'failed to addLogLine', undefined, true);
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export const addLocalLog = (getLog: () => string) => {
|
||||
if (isDEVSentryENV()) {
|
||||
console.log(getLog());
|
||||
}
|
||||
};
|
||||
|
||||
export function getDebugLogs() {
|
||||
return combineLogLines(getLogs());
|
||||
}
|
||||
|
||||
export function getFileNameSize(file: File | ElectronFile) {
|
||||
return `${file.name}_${convertBytesToHumanReadable(file.size)}`;
|
||||
}
|
||||
|
||||
export const clearLogsIfLocalStorageLimitExceeded = () => {
|
||||
try {
|
||||
const logs = getDebugLogs();
|
||||
const logSize = getStringSize(logs);
|
||||
if (logSize > MAX_LOG_SIZE) {
|
||||
deleteLogs();
|
||||
addLogLine('Logs cleared due to size limit exceeded');
|
||||
} else {
|
||||
try {
|
||||
addLogLine(`app started`);
|
||||
} catch (e) {
|
||||
deleteLogs();
|
||||
}
|
||||
}
|
||||
addLogLine(`logs size: ${convertBytesToHumanReadable(logSize)}`);
|
||||
} catch (e) {
|
||||
logError(e, 'failed to clearLogsIfLocalStorageLimitExceeded');
|
||||
}
|
||||
};
|
||||
|
||||
function saveLogLine(log: Log) {
|
||||
const logs = getLogs();
|
||||
if (logs.length > MAX_LOG_LINES) {
|
||||
logs.slice(logs.length - MAX_LOG_LINES);
|
||||
}
|
||||
logs.push(log);
|
||||
setLogs(logs);
|
||||
}
|
||||
|
||||
function getLogs(): Log[] {
|
||||
return getData(LS_KEYS.LOGS)?.logs ?? [];
|
||||
}
|
||||
|
||||
function setLogs(logs: Log[]) {
|
||||
setData(LS_KEYS.LOGS, { logs });
|
||||
}
|
||||
|
||||
function deleteLogs() {
|
||||
removeData(LS_KEYS.LOGS);
|
||||
}
|
||||
|
||||
function getStringSize(str: string) {
|
||||
return new Blob([str]).size;
|
||||
}
|
||||
|
||||
function formatLog(log: Log) {
|
||||
return `[${formatDateTimeShort(log.timestamp)}] ${log.logLine}`;
|
||||
}
|
||||
|
||||
function combineLogLines(logs: Log[]) {
|
||||
return logs.map(formatLog).join('\n');
|
||||
}
|
167
apps/cast/src/utils/logging/upload/index.ts
Normal file
167
apps/cast/src/utils/logging/upload/index.ts
Normal file
|
@ -0,0 +1,167 @@
|
|||
import { FILE_TYPE } from 'constants/file';
|
||||
import { Collection } from 'types/collection';
|
||||
import { B64EncryptionResult, LocalFileAttributes } from 'types/crypto';
|
||||
import {
|
||||
MetadataFileAttributes,
|
||||
S3FileAttributes,
|
||||
FilePublicMagicMetadata,
|
||||
FilePublicMagicMetadataProps,
|
||||
} from 'types/file';
|
||||
import { EncryptedMagicMetadata } from 'types/magicMetadata';
|
||||
|
||||
export interface DataStream {
|
||||
stream: ReadableStream<Uint8Array>;
|
||||
chunkCount: number;
|
||||
}
|
||||
|
||||
export function isDataStream(object: any): object is DataStream {
|
||||
return 'stream' in object;
|
||||
}
|
||||
|
||||
export type Logger = (message: string) => void;
|
||||
|
||||
export interface Metadata {
|
||||
title: string;
|
||||
creationTime: number;
|
||||
modificationTime: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
fileType: FILE_TYPE;
|
||||
hasStaticThumbnail?: boolean;
|
||||
hash?: string;
|
||||
imageHash?: string;
|
||||
videoHash?: string;
|
||||
localID?: number;
|
||||
version?: number;
|
||||
deviceFolder?: string;
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface ParsedMetadataJSON {
|
||||
creationTime: number;
|
||||
modificationTime: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface MultipartUploadURLs {
|
||||
objectKey: string;
|
||||
partURLs: string[];
|
||||
completeURL: string;
|
||||
}
|
||||
|
||||
export interface FileTypeInfo {
|
||||
fileType: FILE_TYPE;
|
||||
exactType: string;
|
||||
mimeType?: string;
|
||||
imageType?: string;
|
||||
videoType?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* ElectronFile is a custom interface that is used to represent
|
||||
* any file on disk as a File-like object in the Electron desktop app.
|
||||
*
|
||||
* This was added to support the auto-resuming of failed uploads
|
||||
* which needed absolute paths to the files which the
|
||||
* normal File interface does not provide.
|
||||
*/
|
||||
export interface ElectronFile {
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
lastModified: number;
|
||||
stream: () => Promise<ReadableStream<Uint8Array>>;
|
||||
blob: () => Promise<Blob>;
|
||||
arrayBuffer: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
export interface UploadAsset {
|
||||
isLivePhoto?: boolean;
|
||||
file?: File | ElectronFile;
|
||||
livePhotoAssets?: LivePhotoAssets;
|
||||
isElectron?: boolean;
|
||||
}
|
||||
export interface LivePhotoAssets {
|
||||
image: globalThis.File | ElectronFile;
|
||||
video: globalThis.File | ElectronFile;
|
||||
}
|
||||
|
||||
export interface FileWithCollection extends UploadAsset {
|
||||
localID: number;
|
||||
collection?: Collection;
|
||||
collectionID?: number;
|
||||
}
|
||||
|
||||
export type ParsedMetadataJSONMap = Map<string, ParsedMetadataJSON>;
|
||||
|
||||
export interface UploadURL {
|
||||
url: string;
|
||||
objectKey: string;
|
||||
}
|
||||
|
||||
export interface FileInMemory {
|
||||
filedata: Uint8Array | DataStream;
|
||||
thumbnail: Uint8Array;
|
||||
hasStaticThumbnail: boolean;
|
||||
}
|
||||
|
||||
export interface FileWithMetadata
|
||||
extends Omit<FileInMemory, 'hasStaticThumbnail'> {
|
||||
metadata: Metadata;
|
||||
localID: number;
|
||||
pubMagicMetadata: FilePublicMagicMetadata;
|
||||
}
|
||||
|
||||
export interface EncryptedFile {
|
||||
file: ProcessedFile;
|
||||
fileKey: B64EncryptionResult;
|
||||
}
|
||||
export interface ProcessedFile {
|
||||
file: LocalFileAttributes<Uint8Array | DataStream>;
|
||||
thumbnail: LocalFileAttributes<Uint8Array>;
|
||||
metadata: LocalFileAttributes<string>;
|
||||
pubMagicMetadata: EncryptedMagicMetadata;
|
||||
localID: number;
|
||||
}
|
||||
export interface BackupedFile {
|
||||
file: S3FileAttributes;
|
||||
thumbnail: S3FileAttributes;
|
||||
metadata: MetadataFileAttributes;
|
||||
pubMagicMetadata: EncryptedMagicMetadata;
|
||||
}
|
||||
|
||||
export interface UploadFile extends BackupedFile {
|
||||
collectionID: number;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
}
|
||||
|
||||
export interface ParsedExtractedMetadata {
|
||||
location: Location;
|
||||
creationTime: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
// This is used to prompt the user the make upload strategy choice
|
||||
export interface ImportSuggestion {
|
||||
rootFolderName: string;
|
||||
hasNestedFolders: boolean;
|
||||
hasRootLevelFileWithFolder: boolean;
|
||||
}
|
||||
|
||||
export interface PublicUploadProps {
|
||||
token: string;
|
||||
passwordToken: string;
|
||||
accessedThroughSharedURL: boolean;
|
||||
}
|
||||
|
||||
export interface ExtractMetadataResult {
|
||||
metadata: Metadata;
|
||||
publicMagicMetadata: FilePublicMagicMetadataProps;
|
||||
}
|
43
apps/cast/src/utils/logging/upload/ui.ts
Normal file
43
apps/cast/src/utils/logging/upload/ui.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { UPLOAD_RESULT, UPLOAD_STAGES } from 'constants/upload';
|
||||
|
||||
export type FileID = number;
|
||||
export type FileName = string;
|
||||
|
||||
export type PercentageUploaded = number;
|
||||
export type UploadFileNames = Map<FileID, FileName>;
|
||||
|
||||
export interface UploadCounter {
|
||||
finished: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface InProgressUpload {
|
||||
localFileID: FileID;
|
||||
progress: PercentageUploaded;
|
||||
}
|
||||
|
||||
export interface FinishedUpload {
|
||||
localFileID: FileID;
|
||||
result: UPLOAD_RESULT;
|
||||
}
|
||||
|
||||
export type InProgressUploads = Map<FileID, PercentageUploaded>;
|
||||
|
||||
export type FinishedUploads = Map<FileID, UPLOAD_RESULT>;
|
||||
|
||||
export type SegregatedFinishedUploads = Map<UPLOAD_RESULT, FileID[]>;
|
||||
|
||||
export interface ProgressUpdater {
|
||||
setPercentComplete: React.Dispatch<React.SetStateAction<number>>;
|
||||
setUploadCounter: React.Dispatch<React.SetStateAction<UploadCounter>>;
|
||||
setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>;
|
||||
setInProgressUploads: React.Dispatch<
|
||||
React.SetStateAction<InProgressUpload[]>
|
||||
>;
|
||||
setFinishedUploads: React.Dispatch<
|
||||
React.SetStateAction<SegregatedFinishedUploads>
|
||||
>;
|
||||
setUploadFilenames: React.Dispatch<React.SetStateAction<UploadFileNames>>;
|
||||
setHasLivePhotos: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setUploadProgressView: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
97
apps/cast/src/utils/magicMetadata/index.ts
Normal file
97
apps/cast/src/utils/magicMetadata/index.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
import { Collection } from 'types/collection';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { MagicMetadataCore, VISIBILITY_STATE } from 'types/magicMetadata';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
|
||||
export function isArchivedFile(item: EnteFile): boolean {
|
||||
if (!item || !item.magicMetadata || !item.magicMetadata.data) {
|
||||
return false;
|
||||
}
|
||||
return item.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED;
|
||||
}
|
||||
|
||||
export function isArchivedCollection(item: Collection): boolean {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.magicMetadata && item.magicMetadata.data) {
|
||||
return item.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED;
|
||||
}
|
||||
|
||||
if (item.sharedMagicMetadata && item.sharedMagicMetadata.data) {
|
||||
return (
|
||||
item.sharedMagicMetadata.data.visibility ===
|
||||
VISIBILITY_STATE.ARCHIVED
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isPinnedCollection(item: Collection) {
|
||||
if (
|
||||
!item ||
|
||||
!item.magicMetadata ||
|
||||
!item.magicMetadata.data ||
|
||||
typeof item.magicMetadata.data === 'string' ||
|
||||
typeof item.magicMetadata.data.order === 'undefined'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return item.magicMetadata.data.order !== 0;
|
||||
}
|
||||
|
||||
export async function updateMagicMetadata<T>(
|
||||
magicMetadataUpdates: T,
|
||||
originalMagicMetadata?: MagicMetadataCore<T>,
|
||||
decryptionKey?: string
|
||||
): Promise<MagicMetadataCore<T>> {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
|
||||
if (!originalMagicMetadata) {
|
||||
originalMagicMetadata = getNewMagicMetadata<T>();
|
||||
}
|
||||
|
||||
if (typeof originalMagicMetadata?.data === 'string') {
|
||||
originalMagicMetadata.data = await cryptoWorker.decryptMetadata(
|
||||
originalMagicMetadata.data,
|
||||
originalMagicMetadata.header,
|
||||
decryptionKey
|
||||
);
|
||||
}
|
||||
// copies the existing magic metadata properties of the files and updates the visibility value
|
||||
// The expected behavior while updating magic metadata is to let the existing property as it is and update/add the property you want
|
||||
const magicMetadataProps: T = {
|
||||
...originalMagicMetadata.data,
|
||||
...magicMetadataUpdates,
|
||||
};
|
||||
|
||||
const nonEmptyMagicMetadataProps =
|
||||
getNonEmptyMagicMetadataProps(magicMetadataProps);
|
||||
|
||||
const magicMetadata = {
|
||||
...originalMagicMetadata,
|
||||
data: nonEmptyMagicMetadataProps,
|
||||
count: Object.keys(nonEmptyMagicMetadataProps).length,
|
||||
};
|
||||
|
||||
return magicMetadata;
|
||||
}
|
||||
|
||||
export const getNewMagicMetadata = <T>(): MagicMetadataCore<T> => {
|
||||
return {
|
||||
version: 1,
|
||||
data: null,
|
||||
header: null,
|
||||
count: 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const getNonEmptyMagicMetadataProps = <T>(magicMetadataProps: T): T => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(magicMetadataProps).filter(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
([_, v]) => v !== null && v !== undefined
|
||||
)
|
||||
) as T;
|
||||
};
|
75
apps/cast/src/utils/sentry/index.ts
Normal file
75
apps/cast/src/utils/sentry/index.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import * as Sentry from '@sentry/nextjs';
|
||||
import { addLocalLog, addLogLine } from 'utils/logging';
|
||||
import { getSentryUserID } from 'utils/user';
|
||||
import InMemoryStore, { MS_KEYS } from 'services/InMemoryStore';
|
||||
import { getHasOptedOutOfCrashReports } from 'utils/storage';
|
||||
import { ApiError } from 'utils/error';
|
||||
|
||||
export const logError = async (
|
||||
error: any,
|
||||
msg: string,
|
||||
info?: Record<string, unknown>,
|
||||
skipAddLogLine = false
|
||||
) => {
|
||||
const err = errorWithContext(error, msg);
|
||||
if (!skipAddLogLine) {
|
||||
if (error instanceof ApiError) {
|
||||
addLogLine(`error: ${error?.name} ${error?.message}
|
||||
msg: ${msg} errorCode: ${JSON.stringify(error?.errCode)}
|
||||
httpStatusCode: ${JSON.stringify(error?.httpStatusCode)} ${
|
||||
info ? `info: ${JSON.stringify(info)}` : ''
|
||||
}
|
||||
${error?.stack}`);
|
||||
} else {
|
||||
addLogLine(
|
||||
`error: ${error?.name} ${error?.message}
|
||||
msg: ${msg} ${info ? `info: ${JSON.stringify(info)}` : ''}
|
||||
${error?.stack}`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!InMemoryStore.has(MS_KEYS.OPT_OUT_OF_CRASH_REPORTS)) {
|
||||
const optedOutOfCrashReports = getHasOptedOutOfCrashReports();
|
||||
InMemoryStore.set(
|
||||
MS_KEYS.OPT_OUT_OF_CRASH_REPORTS,
|
||||
optedOutOfCrashReports
|
||||
);
|
||||
}
|
||||
if (InMemoryStore.get(MS_KEYS.OPT_OUT_OF_CRASH_REPORTS)) {
|
||||
addLocalLog(() => `skipping sentry error: ${error?.name}`);
|
||||
return;
|
||||
}
|
||||
if (isErrorUnnecessaryForSentry(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Sentry.captureException(err, {
|
||||
level: 'info',
|
||||
user: { id: await getSentryUserID() },
|
||||
contexts: {
|
||||
...(info && {
|
||||
info: info,
|
||||
}),
|
||||
rootCause: { message: error?.message, completeError: error },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// copy of errorWithContext to prevent importing error util
|
||||
function errorWithContext(originalError: Error, context: string) {
|
||||
const errorWithContext = new Error(context);
|
||||
errorWithContext.stack =
|
||||
errorWithContext.stack.split('\n').slice(2, 4).join('\n') +
|
||||
'\n' +
|
||||
originalError.stack;
|
||||
return errorWithContext;
|
||||
}
|
||||
|
||||
function isErrorUnnecessaryForSentry(error: any) {
|
||||
if (error?.message?.includes('Network Error')) {
|
||||
return true;
|
||||
} else if (error?.status === 401) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
40
apps/cast/src/utils/storage/index.ts
Normal file
40
apps/cast/src/utils/storage/index.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Language } from 'constants/locale';
|
||||
import { getData, LS_KEYS, setData } from './localStorage';
|
||||
|
||||
export const isFirstLogin = () =>
|
||||
getData(LS_KEYS.IS_FIRST_LOGIN)?.status ?? false;
|
||||
|
||||
export function setIsFirstLogin(status) {
|
||||
setData(LS_KEYS.IS_FIRST_LOGIN, { status });
|
||||
}
|
||||
|
||||
export const justSignedUp = () =>
|
||||
getData(LS_KEYS.JUST_SIGNED_UP)?.status ?? false;
|
||||
|
||||
export function setJustSignedUp(status) {
|
||||
setData(LS_KEYS.JUST_SIGNED_UP, { status });
|
||||
}
|
||||
|
||||
export function getLivePhotoInfoShownCount() {
|
||||
return getData(LS_KEYS.LIVE_PHOTO_INFO_SHOWN_COUNT)?.count ?? 0;
|
||||
}
|
||||
|
||||
export function setLivePhotoInfoShownCount(count) {
|
||||
setData(LS_KEYS.LIVE_PHOTO_INFO_SHOWN_COUNT, { count });
|
||||
}
|
||||
|
||||
export function getUserLocale(): Language {
|
||||
return getData(LS_KEYS.LOCALE)?.value;
|
||||
}
|
||||
|
||||
export function getLocalMapEnabled(): boolean {
|
||||
return getData(LS_KEYS.MAP_ENABLED)?.value ?? false;
|
||||
}
|
||||
|
||||
export function setLocalMapEnabled(value: boolean) {
|
||||
setData(LS_KEYS.MAP_ENABLED, { value });
|
||||
}
|
||||
|
||||
export function getHasOptedOutOfCrashReports(): boolean {
|
||||
return getData(LS_KEYS.OPT_OUT_OF_CRASH_REPORTS)?.value ?? false;
|
||||
}
|
12
apps/cast/src/utils/storage/localForage.ts
Normal file
12
apps/cast/src/utils/storage/localForage.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { runningInBrowser } from 'utils/common';
|
||||
|
||||
import localForage from 'localforage';
|
||||
|
||||
if (runningInBrowser()) {
|
||||
localForage.config({
|
||||
name: 'ente-files',
|
||||
version: 1.0,
|
||||
storeName: 'files',
|
||||
});
|
||||
}
|
||||
export default localForage;
|
68
apps/cast/src/utils/storage/localStorage.ts
Normal file
68
apps/cast/src/utils/storage/localStorage.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { logError } from 'utils/sentry';
|
||||
|
||||
export enum LS_KEYS {
|
||||
USER = 'user',
|
||||
SESSION = 'session',
|
||||
KEY_ATTRIBUTES = 'keyAttributes',
|
||||
ORIGINAL_KEY_ATTRIBUTES = 'originalKeyAttributes',
|
||||
SUBSCRIPTION = 'subscription',
|
||||
FAMILY_DATA = 'familyData',
|
||||
PLANS = 'plans',
|
||||
IS_FIRST_LOGIN = 'isFirstLogin',
|
||||
JUST_SIGNED_UP = 'justSignedUp',
|
||||
SHOW_BACK_BUTTON = 'showBackButton',
|
||||
EXPORT = 'export',
|
||||
AnonymizedUserID = 'anonymizedUserID',
|
||||
THUMBNAIL_FIX_STATE = 'thumbnailFixState',
|
||||
LIVE_PHOTO_INFO_SHOWN_COUNT = 'livePhotoInfoShownCount',
|
||||
LOGS = 'logs',
|
||||
USER_DETAILS = 'userDetails',
|
||||
COLLECTION_SORT_BY = 'collectionSortBy',
|
||||
THEME = 'theme',
|
||||
WAIT_TIME = 'waitTime',
|
||||
API_ENDPOINT = 'apiEndpoint',
|
||||
LOCALE = 'locale',
|
||||
MAP_ENABLED = 'mapEnabled',
|
||||
SRP_SETUP_ATTRIBUTES = 'srpSetupAttributes',
|
||||
SRP_ATTRIBUTES = 'srpAttributes',
|
||||
OPT_OUT_OF_CRASH_REPORTS = 'optOutOfCrashReports',
|
||||
CF_PROXY_DISABLED = 'cfProxyDisabled',
|
||||
}
|
||||
|
||||
export const setData = (key: LS_KEYS, value: object) => {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
};
|
||||
|
||||
export const removeData = (key: LS_KEYS) => {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
localStorage.removeItem(key);
|
||||
};
|
||||
|
||||
export const getData = (key: LS_KEYS) => {
|
||||
try {
|
||||
if (
|
||||
typeof localStorage === 'undefined' ||
|
||||
typeof key === 'undefined' ||
|
||||
typeof localStorage.getItem(key) === 'undefined' ||
|
||||
localStorage.getItem(key) === 'undefined'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const data = localStorage.getItem(key);
|
||||
return data && JSON.parse(data);
|
||||
} catch (e) {
|
||||
logError(e, 'Failed to Parse JSON for key ' + key);
|
||||
}
|
||||
};
|
||||
|
||||
export const clearData = () => {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
localStorage.clear();
|
||||
};
|
32
apps/cast/src/utils/storage/sessionStorage.ts
Normal file
32
apps/cast/src/utils/storage/sessionStorage.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
export enum SESSION_KEYS {
|
||||
ENCRYPTION_KEY = 'encryptionKey',
|
||||
KEY_ENCRYPTION_KEY = 'keyEncryptionKey',
|
||||
}
|
||||
|
||||
export const setKey = (key: SESSION_KEYS, value: object) => {
|
||||
if (typeof sessionStorage === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
sessionStorage.setItem(key, JSON.stringify(value));
|
||||
};
|
||||
|
||||
export const getKey = (key: SESSION_KEYS) => {
|
||||
if (typeof sessionStorage === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(sessionStorage.getItem(key));
|
||||
};
|
||||
|
||||
export const removeKey = (key: SESSION_KEYS) => {
|
||||
if (typeof sessionStorage === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
sessionStorage.removeItem(key);
|
||||
};
|
||||
|
||||
export const clearKeys = () => {
|
||||
if (typeof sessionStorage === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
sessionStorage.clear();
|
||||
};
|
78
apps/cast/src/utils/time/format.ts
Normal file
78
apps/cast/src/utils/time/format.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import i18n, { t } from 'i18next';
|
||||
|
||||
const dateTimeFullFormatter1 = new Intl.DateTimeFormat(i18n.language, {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
const dateTimeFullFormatter2 = new Intl.DateTimeFormat(i18n.language, {
|
||||
year: 'numeric',
|
||||
});
|
||||
const dateTimeShortFormatter = new Intl.DateTimeFormat(i18n.language, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
|
||||
const timeFormatter = new Intl.DateTimeFormat(i18n.language, {
|
||||
timeStyle: 'short',
|
||||
});
|
||||
|
||||
export function formatDateFull(date: number | Date) {
|
||||
return [dateTimeFullFormatter1, dateTimeFullFormatter2]
|
||||
.map((f) => f.format(date))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
export function formatDate(date: number | Date) {
|
||||
const withinYear =
|
||||
new Date().getFullYear() === new Date(date).getFullYear();
|
||||
const dateTimeFormat2 = !withinYear ? dateTimeFullFormatter2 : null;
|
||||
return [dateTimeFullFormatter1, dateTimeFormat2]
|
||||
.filter((f) => !!f)
|
||||
.map((f) => f.format(date))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
export function formatDateTimeShort(date: number | Date) {
|
||||
return dateTimeShortFormatter.format(date);
|
||||
}
|
||||
|
||||
export function formatTime(date: number | Date) {
|
||||
return timeFormatter.format(date).toUpperCase();
|
||||
}
|
||||
|
||||
export function formatDateTimeFull(dateTime: number | Date): string {
|
||||
return [formatDateFull(dateTime), t('at'), formatTime(dateTime)].join(' ');
|
||||
}
|
||||
|
||||
export function formatDateTime(dateTime: number | Date): string {
|
||||
return [formatDate(dateTime), t('at'), formatTime(dateTime)].join(' ');
|
||||
}
|
||||
|
||||
export function formatDateRelative(date: number) {
|
||||
const units = {
|
||||
year: 24 * 60 * 60 * 1000 * 365,
|
||||
month: (24 * 60 * 60 * 1000 * 365) / 12,
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
hour: 60 * 60 * 1000,
|
||||
minute: 60 * 1000,
|
||||
second: 1000,
|
||||
};
|
||||
const relativeDateFormat = new Intl.RelativeTimeFormat(i18n.language, {
|
||||
localeMatcher: 'best fit',
|
||||
numeric: 'always',
|
||||
style: 'long',
|
||||
});
|
||||
const elapsed = date - Date.now(); // "Math.abs" accounts for both "past" & "future" scenarios
|
||||
|
||||
for (const u in units)
|
||||
if (Math.abs(elapsed) > units[u] || u === 'second')
|
||||
return relativeDateFormat.format(
|
||||
Math.round(elapsed / units[u]),
|
||||
u as Intl.RelativeTimeFormatUnit
|
||||
);
|
||||
}
|
177
apps/cast/src/utils/time/index.ts
Normal file
177
apps/cast/src/utils/time/index.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
export interface TimeDelta {
|
||||
hours?: number;
|
||||
days?: number;
|
||||
months?: number;
|
||||
years?: number;
|
||||
}
|
||||
|
||||
interface DateComponent<T = number> {
|
||||
year: T;
|
||||
month: T;
|
||||
day: T;
|
||||
hour: T;
|
||||
minute: T;
|
||||
second: T;
|
||||
}
|
||||
|
||||
export function getUnixTimeInMicroSecondsWithDelta(delta: TimeDelta): number {
|
||||
let currentDate = new Date();
|
||||
if (delta?.hours) {
|
||||
currentDate = _addHours(currentDate, delta.hours);
|
||||
}
|
||||
if (delta?.days) {
|
||||
currentDate = _addDays(currentDate, delta.days);
|
||||
}
|
||||
if (delta?.months) {
|
||||
currentDate = _addMonth(currentDate, delta.months);
|
||||
}
|
||||
if (delta?.years) {
|
||||
currentDate = _addYears(currentDate, delta.years);
|
||||
}
|
||||
return currentDate.getTime() * 1000;
|
||||
}
|
||||
|
||||
export function validateAndGetCreationUnixTimeInMicroSeconds(dateTime: Date) {
|
||||
if (!dateTime || isNaN(dateTime.getTime())) {
|
||||
return null;
|
||||
}
|
||||
const unixTime = dateTime.getTime() * 1000;
|
||||
//ignoring dateTimeString = "0000:00:00 00:00:00"
|
||||
if (unixTime === Date.UTC(0, 0, 0, 0, 0, 0, 0) || unixTime === 0) {
|
||||
return null;
|
||||
} else if (unixTime > Date.now() * 1000) {
|
||||
return null;
|
||||
} else {
|
||||
return unixTime;
|
||||
}
|
||||
}
|
||||
|
||||
function _addDays(date: Date, days: number): Date {
|
||||
const result = new Date(date);
|
||||
result.setDate(date.getDate() + days);
|
||||
return result;
|
||||
}
|
||||
|
||||
function _addHours(date: Date, hours: number): Date {
|
||||
const result = new Date(date);
|
||||
result.setHours(date.getHours() + hours);
|
||||
return result;
|
||||
}
|
||||
|
||||
function _addMonth(date: Date, months: number) {
|
||||
const result = new Date(date);
|
||||
result.setMonth(date.getMonth() + months);
|
||||
return result;
|
||||
}
|
||||
|
||||
function _addYears(date: Date, years: number) {
|
||||
const result = new Date(date);
|
||||
result.setFullYear(date.getFullYear() + years);
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
generates data component for date in format YYYYMMDD-HHMMSS
|
||||
*/
|
||||
export function parseDateFromFusedDateString(dateTime: string) {
|
||||
const dateComponent: DateComponent<number> = convertDateComponentToNumber({
|
||||
year: dateTime.slice(0, 4),
|
||||
month: dateTime.slice(4, 6),
|
||||
day: dateTime.slice(6, 8),
|
||||
hour: dateTime.slice(9, 11),
|
||||
minute: dateTime.slice(11, 13),
|
||||
second: dateTime.slice(13, 15),
|
||||
});
|
||||
return validateAndGetDateFromComponents(dateComponent);
|
||||
}
|
||||
|
||||
/* sample date format = 2018-08-19 12:34:45
|
||||
the date has six symbol separated number values
|
||||
which we would extract and use to form the date
|
||||
*/
|
||||
export function tryToParseDateTime(dateTime: string): Date {
|
||||
const dateComponent = getDateComponentsFromSymbolJoinedString(dateTime);
|
||||
if (dateComponent.year?.length === 8 && dateComponent.month?.length === 6) {
|
||||
// the filename has size 8 consecutive and then 6 consecutive digits
|
||||
// high possibility that the it is a date in format YYYYMMDD-HHMMSS
|
||||
const possibleDateTime = dateComponent.year + '-' + dateComponent.month;
|
||||
return parseDateFromFusedDateString(possibleDateTime);
|
||||
}
|
||||
return validateAndGetDateFromComponents(
|
||||
convertDateComponentToNumber(dateComponent)
|
||||
);
|
||||
}
|
||||
|
||||
function getDateComponentsFromSymbolJoinedString(
|
||||
dateTime: string
|
||||
): DateComponent<string> {
|
||||
const [year, month, day, hour, minute, second] =
|
||||
dateTime.match(/\d+/g) ?? [];
|
||||
|
||||
return { year, month, day, hour, minute, second };
|
||||
}
|
||||
|
||||
function validateAndGetDateFromComponents(
|
||||
dateComponent: DateComponent<number>
|
||||
) {
|
||||
let date = getDateFromComponents(dateComponent);
|
||||
if (hasTimeValues(dateComponent) && !isTimePartValid(date, dateComponent)) {
|
||||
// if the date has time values but they are not valid
|
||||
// then we remove the time values and try to validate the date
|
||||
date = getDateFromComponents(removeTimeValues(dateComponent));
|
||||
}
|
||||
if (!isDatePartValid(date, dateComponent)) {
|
||||
return null;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
function isTimePartValid(date: Date, dateComponent: DateComponent<number>) {
|
||||
return (
|
||||
date.getHours() === dateComponent.hour &&
|
||||
date.getMinutes() === dateComponent.minute &&
|
||||
date.getSeconds() === dateComponent.second
|
||||
);
|
||||
}
|
||||
|
||||
function isDatePartValid(date: Date, dateComponent: DateComponent<number>) {
|
||||
return (
|
||||
date.getFullYear() === dateComponent.year &&
|
||||
date.getMonth() === dateComponent.month &&
|
||||
date.getDate() === dateComponent.day
|
||||
);
|
||||
}
|
||||
|
||||
function convertDateComponentToNumber(
|
||||
dateComponent: DateComponent<string>
|
||||
): DateComponent<number> {
|
||||
return {
|
||||
year: Number(dateComponent.year),
|
||||
// https://stackoverflow.com/questions/2552483/why-does-the-month-argument-range-from-0-to-11-in-javascripts-date-constructor
|
||||
month: Number(dateComponent.month) - 1,
|
||||
day: Number(dateComponent.day),
|
||||
hour: Number(dateComponent.hour),
|
||||
minute: Number(dateComponent.minute),
|
||||
second: Number(dateComponent.second),
|
||||
};
|
||||
}
|
||||
|
||||
function getDateFromComponents(dateComponent: DateComponent<number>) {
|
||||
const { year, month, day, hour, minute, second } = dateComponent;
|
||||
if (hasTimeValues(dateComponent)) {
|
||||
return new Date(year, month, day, hour, minute, second);
|
||||
} else {
|
||||
return new Date(year, month, day);
|
||||
}
|
||||
}
|
||||
|
||||
function hasTimeValues(dateComponent: DateComponent<number>) {
|
||||
const { hour, minute, second } = dateComponent;
|
||||
return !isNaN(hour) && !isNaN(minute) && !isNaN(second);
|
||||
}
|
||||
|
||||
function removeTimeValues(
|
||||
dateComponent: DateComponent<number>
|
||||
): DateComponent<number> {
|
||||
return { ...dateComponent, hour: 0, minute: 0, second: 0 };
|
||||
}
|
45
apps/cast/src/utils/user/family.ts
Normal file
45
apps/cast/src/utils/user/family.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { FamilyData, FamilyMember, User } from 'types/user';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
|
||||
export function getLocalFamilyData(): FamilyData {
|
||||
return getData(LS_KEYS.FAMILY_DATA);
|
||||
}
|
||||
|
||||
// isPartOfFamily return true if the current user is part of some family plan
|
||||
export function isPartOfFamily(familyData: FamilyData): boolean {
|
||||
return Boolean(
|
||||
familyData && familyData.members && familyData.members.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
// hasNonAdminFamilyMembers return true if the admin user has members in his family
|
||||
export function hasNonAdminFamilyMembers(familyData: FamilyData): boolean {
|
||||
return Boolean(isPartOfFamily(familyData) && familyData.members.length > 1);
|
||||
}
|
||||
|
||||
export function isFamilyAdmin(familyData: FamilyData): boolean {
|
||||
const familyAdmin: FamilyMember = getFamilyPlanAdmin(familyData);
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
return familyAdmin.email === user.email;
|
||||
}
|
||||
|
||||
export function getFamilyPlanAdmin(familyData: FamilyData): FamilyMember {
|
||||
if (isPartOfFamily(familyData)) {
|
||||
return familyData.members.find((x) => x.isAdmin);
|
||||
} else {
|
||||
logError(
|
||||
Error(
|
||||
'verify user is part of family plan before calling this method'
|
||||
),
|
||||
'invalid getFamilyPlanAdmin call'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getTotalFamilyUsage(familyData: FamilyData): number {
|
||||
return familyData.members.reduce(
|
||||
(sum, currentMember) => sum + currentMember.usage,
|
||||
0
|
||||
);
|
||||
}
|
52
apps/cast/src/utils/user/index.ts
Normal file
52
apps/cast/src/utils/user/index.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import isElectron from 'is-electron';
|
||||
import { UserDetails } from 'types/user';
|
||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||
// import ElectronService from 'services/electron/common';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
export function makeID(length) {
|
||||
let result = '';
|
||||
const characters =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const charactersLength = characters.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(
|
||||
Math.floor(Math.random() * charactersLength)
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getSentryUserID() {
|
||||
if (isElectron()) {
|
||||
// return await ElectronService.getSentryUserID();
|
||||
} else {
|
||||
let anonymizeUserID = getData(LS_KEYS.AnonymizedUserID)?.id;
|
||||
if (!anonymizeUserID) {
|
||||
anonymizeUserID = makeID(6);
|
||||
setData(LS_KEYS.AnonymizedUserID, { id: anonymizeUserID });
|
||||
}
|
||||
return anonymizeUserID;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocalUserDetails(): UserDetails {
|
||||
return getData(LS_KEYS.USER_DETAILS)?.value;
|
||||
}
|
||||
|
||||
export const isInternalUser = () => {
|
||||
const userEmail = getData(LS_KEYS.USER)?.email;
|
||||
if (!userEmail) return false;
|
||||
|
||||
return (
|
||||
userEmail.endsWith('@ente.io') || userEmail === 'kr.anand619@gmail.com'
|
||||
);
|
||||
};
|
||||
|
||||
export const convertBufferToBase64 = (buffer: Buffer) => {
|
||||
return buffer.toString('base64');
|
||||
};
|
||||
|
||||
export const convertBase64ToBuffer = (base64: string) => {
|
||||
return Buffer.from(base64, 'base64');
|
||||
};
|
215
apps/cast/src/worker/crypto.worker.ts
Normal file
215
apps/cast/src/worker/crypto.worker.ts
Normal file
|
@ -0,0 +1,215 @@
|
|||
import * as Comlink from 'comlink';
|
||||
import { StateAddress } from 'libsodium-wrappers';
|
||||
import * as libsodium from 'utils/crypto/libsodium';
|
||||
|
||||
const textDecoder = new TextDecoder();
|
||||
const textEncoder = new TextEncoder();
|
||||
export class DedicatedCryptoWorker {
|
||||
async decryptMetadata(
|
||||
encryptedMetadata: string,
|
||||
header: string,
|
||||
key: string
|
||||
) {
|
||||
const encodedMetadata = await libsodium.decryptChaChaOneShot(
|
||||
await libsodium.fromB64(encryptedMetadata),
|
||||
await libsodium.fromB64(header),
|
||||
key
|
||||
);
|
||||
return JSON.parse(textDecoder.decode(encodedMetadata));
|
||||
}
|
||||
|
||||
async decryptThumbnail(
|
||||
fileData: Uint8Array,
|
||||
header: Uint8Array,
|
||||
key: string
|
||||
) {
|
||||
return libsodium.decryptChaChaOneShot(fileData, header, key);
|
||||
}
|
||||
|
||||
async decryptEmbedding(
|
||||
encryptedEmbedding: string,
|
||||
header: string,
|
||||
key: string
|
||||
) {
|
||||
const encodedEmbedding = await libsodium.decryptChaChaOneShot(
|
||||
await libsodium.fromB64(encryptedEmbedding),
|
||||
await libsodium.fromB64(header),
|
||||
key
|
||||
);
|
||||
return Float32Array.from(
|
||||
JSON.parse(textDecoder.decode(encodedEmbedding))
|
||||
);
|
||||
}
|
||||
|
||||
async decryptFile(fileData: Uint8Array, header: Uint8Array, key: string) {
|
||||
return libsodium.decryptChaCha(fileData, header, key);
|
||||
}
|
||||
|
||||
async encryptMetadata(metadata: Object, key: string) {
|
||||
const encodedMetadata = textEncoder.encode(JSON.stringify(metadata));
|
||||
|
||||
const { file: encryptedMetadata } =
|
||||
await libsodium.encryptChaChaOneShot(encodedMetadata, key);
|
||||
const { encryptedData, ...other } = encryptedMetadata;
|
||||
return {
|
||||
file: {
|
||||
encryptedData: await libsodium.toB64(encryptedData),
|
||||
...other,
|
||||
},
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
async encryptThumbnail(fileData: Uint8Array, key: string) {
|
||||
return libsodium.encryptChaChaOneShot(fileData, key);
|
||||
}
|
||||
|
||||
async encryptEmbedding(embedding: Float32Array, key: string) {
|
||||
const encodedEmbedding = textEncoder.encode(
|
||||
JSON.stringify(Array.from(embedding))
|
||||
);
|
||||
const { file: encryptEmbedding } = await libsodium.encryptChaChaOneShot(
|
||||
encodedEmbedding,
|
||||
key
|
||||
);
|
||||
const { encryptedData, ...other } = encryptEmbedding;
|
||||
return {
|
||||
file: {
|
||||
encryptedData: await libsodium.toB64(encryptedData),
|
||||
...other,
|
||||
},
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
async encryptFile(fileData: Uint8Array) {
|
||||
return libsodium.encryptChaCha(fileData);
|
||||
}
|
||||
|
||||
async encryptFileChunk(
|
||||
data: Uint8Array,
|
||||
pushState: StateAddress,
|
||||
isFinalChunk: boolean
|
||||
) {
|
||||
return libsodium.encryptFileChunk(data, pushState, isFinalChunk);
|
||||
}
|
||||
|
||||
async initChunkEncryption() {
|
||||
return libsodium.initChunkEncryption();
|
||||
}
|
||||
|
||||
async initChunkDecryption(header: Uint8Array, key: Uint8Array) {
|
||||
return libsodium.initChunkDecryption(header, key);
|
||||
}
|
||||
|
||||
async decryptFileChunk(fileData: Uint8Array, pullState: StateAddress) {
|
||||
return libsodium.decryptFileChunk(fileData, pullState);
|
||||
}
|
||||
|
||||
async initChunkHashing() {
|
||||
return libsodium.initChunkHashing();
|
||||
}
|
||||
|
||||
async hashFileChunk(hashState: StateAddress, chunk: Uint8Array) {
|
||||
return libsodium.hashFileChunk(hashState, chunk);
|
||||
}
|
||||
|
||||
async completeChunkHashing(hashState: StateAddress) {
|
||||
return libsodium.completeChunkHashing(hashState);
|
||||
}
|
||||
|
||||
async deriveKey(
|
||||
passphrase: string,
|
||||
salt: string,
|
||||
opsLimit: number,
|
||||
memLimit: number
|
||||
) {
|
||||
return libsodium.deriveKey(passphrase, salt, opsLimit, memLimit);
|
||||
}
|
||||
|
||||
async deriveSensitiveKey(passphrase: string, salt: string) {
|
||||
return libsodium.deriveSensitiveKey(passphrase, salt);
|
||||
}
|
||||
|
||||
async deriveInteractiveKey(passphrase: string, salt: string) {
|
||||
return libsodium.deriveInteractiveKey(passphrase, salt);
|
||||
}
|
||||
|
||||
async decryptB64(data: string, nonce: string, key: string) {
|
||||
return libsodium.decryptB64(data, nonce, key);
|
||||
}
|
||||
|
||||
async decryptToUTF8(data: string, nonce: string, key: string) {
|
||||
return libsodium.decryptToUTF8(data, nonce, key);
|
||||
}
|
||||
|
||||
async encryptToB64(data: string, key: string) {
|
||||
return libsodium.encryptToB64(data, key);
|
||||
}
|
||||
|
||||
async generateKeyAndEncryptToB64(data: string) {
|
||||
return libsodium.generateKeyAndEncryptToB64(data);
|
||||
}
|
||||
|
||||
async encryptUTF8(data: string, key: string) {
|
||||
return libsodium.encryptUTF8(data, key);
|
||||
}
|
||||
|
||||
async generateEncryptionKey() {
|
||||
return libsodium.generateEncryptionKey();
|
||||
}
|
||||
|
||||
async generateSaltToDeriveKey() {
|
||||
return libsodium.generateSaltToDeriveKey();
|
||||
}
|
||||
|
||||
async generateKeyPair() {
|
||||
return libsodium.generateKeyPair();
|
||||
}
|
||||
|
||||
async boxSealOpen(input: string, publicKey: string, secretKey: string) {
|
||||
return libsodium.boxSealOpen(input, publicKey, secretKey);
|
||||
}
|
||||
|
||||
async boxSeal(input: string, publicKey: string) {
|
||||
return libsodium.boxSeal(input, publicKey);
|
||||
}
|
||||
|
||||
async generateSubKey(
|
||||
key: string,
|
||||
subKeyLength: number,
|
||||
subKeyID: number,
|
||||
context: string
|
||||
) {
|
||||
return libsodium.generateSubKey(key, subKeyLength, subKeyID, context);
|
||||
}
|
||||
|
||||
async fromUTF8(string: string) {
|
||||
return libsodium.fromUTF8(string);
|
||||
}
|
||||
async toUTF8(data: string) {
|
||||
return libsodium.toUTF8(data);
|
||||
}
|
||||
|
||||
async toB64(data: Uint8Array) {
|
||||
return libsodium.toB64(data);
|
||||
}
|
||||
|
||||
async toURLSafeB64(data: Uint8Array) {
|
||||
return libsodium.toURLSafeB64(data);
|
||||
}
|
||||
|
||||
async fromB64(string: string) {
|
||||
return libsodium.fromB64(string);
|
||||
}
|
||||
|
||||
async toHex(string: string) {
|
||||
return libsodium.toHex(string);
|
||||
}
|
||||
|
||||
async fromHex(string: string) {
|
||||
return libsodium.fromHex(string);
|
||||
}
|
||||
}
|
||||
|
||||
Comlink.expose(DedicatedCryptoWorker, self);
|
18
apps/cast/tsconfig.json
Normal file
18
apps/cast/tsconfig.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./src",
|
||||
"downlevelIteration": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "@emotion/react",
|
||||
"lib": ["dom", "dom.iterable", "esnext", "webworker"],
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"strictNullChecks": false,
|
||||
"target": "es5",
|
||||
"useUnknownInCatchVariables": false
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
|
||||
"exclude": ["node_modules", "out", ".next", "thirdparty"]
|
||||
}
|
Loading…
Reference in a new issue