Accounts-passkeys (#1523)
This commit is contained in:
commit
1c18fb8392
77 changed files with 3183 additions and 83 deletions
13
apps/accounts/.eslintrc.js
Normal file
13
apps/accounts/.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/accounts/.gitignore
vendored
Normal file
36
apps/accounts/.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/accounts/README.md
Normal file
40
apps/accounts/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.
|
11
apps/accounts/next.config.js
Normal file
11
apps/accounts/next.config.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const nextConfigBase = require('@ente/shared/next/next.config.base.js');
|
||||
|
||||
module.exports = {
|
||||
...nextConfigBase,
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
experimental: {
|
||||
externalDir: true,
|
||||
},
|
||||
};
|
18
apps/accounts/package.json
Normal file
18
apps/accounts/package.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "accounts",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"export": "next export"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.6.4",
|
||||
"@types/react": "^16.9.49"
|
||||
},
|
||||
"types": "src/index.ts"
|
||||
}
|
BIN
apps/accounts/public/favicon.ico
Normal file
BIN
apps/accounts/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
93
apps/accounts/public/fonts/OFL.txt
Normal file
93
apps/accounts/public/fonts/OFL.txt
Normal file
|
@ -0,0 +1,93 @@
|
|||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
apps/accounts/public/fonts/inter-v11-latin-500.woff
Normal file
BIN
apps/accounts/public/fonts/inter-v11-latin-500.woff
Normal file
Binary file not shown.
BIN
apps/accounts/public/fonts/inter-v11-latin-500.woff2
Normal file
BIN
apps/accounts/public/fonts/inter-v11-latin-500.woff2
Normal file
Binary file not shown.
BIN
apps/accounts/public/fonts/inter-v11-latin-600.woff
Normal file
BIN
apps/accounts/public/fonts/inter-v11-latin-600.woff
Normal file
Binary file not shown.
BIN
apps/accounts/public/fonts/inter-v11-latin-600.woff2
Normal file
BIN
apps/accounts/public/fonts/inter-v11-latin-600.woff2
Normal file
Binary file not shown.
BIN
apps/accounts/public/fonts/inter-v11-latin-800.woff
Normal file
BIN
apps/accounts/public/fonts/inter-v11-latin-800.woff
Normal file
Binary file not shown.
BIN
apps/accounts/public/fonts/inter-v11-latin-800.woff2
Normal file
BIN
apps/accounts/public/fonts/inter-v11-latin-800.woff2
Normal file
Binary file not shown.
BIN
apps/accounts/public/images/ente-circular.png
Normal file
BIN
apps/accounts/public/images/ente-circular.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
3
apps/accounts/public/images/ente.svg
Normal file
3
apps/accounts/public/images/ente.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="43" height="13" viewBox="0 0 43 13" fill="#fff" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.102 12.144C4.998 12.144 4.026 11.928 3.186 11.496C2.358 11.064 1.716 10.476 1.26 9.732C0.804 8.976 0.576 8.118 0.576 7.158C0.576 6.186 0.798 5.328 1.242 4.584C1.698 3.828 2.316 3.24 3.096 2.82C3.876 2.388 4.758 2.172 5.742 2.172C6.69 2.172 7.542 2.376 8.298 2.784C9.066 3.18 9.672 3.756 10.116 4.512C10.56 5.256 10.782 6.15 10.782 7.194C10.782 7.302 10.776 7.428 10.764 7.572C10.752 7.704 10.74 7.83 10.728 7.95H2.862V6.312H9.252L8.172 6.798C8.172 6.294 8.07 5.856 7.866 5.484C7.662 5.112 7.38 4.824 7.02 4.62C6.66 4.404 6.24 4.296 5.76 4.296C5.28 4.296 4.854 4.404 4.482 4.62C4.122 4.824 3.84 5.118 3.636 5.502C3.432 5.874 3.33 6.318 3.33 6.834V7.266C3.33 7.794 3.444 8.262 3.672 8.67C3.912 9.066 4.242 9.372 4.662 9.588C5.094 9.792 5.598 9.894 6.174 9.894C6.69 9.894 7.14 9.816 7.524 9.66C7.92 9.504 8.28 9.27 8.604 8.958L10.098 10.578C9.654 11.082 9.096 11.472 8.424 11.748C7.752 12.012 6.978 12.144 6.102 12.144ZM18.5375 2.172C19.3055 2.172 19.9895 2.328 20.5895 2.64C21.2015 2.94 21.6815 3.408 22.0295 4.044C22.3775 4.668 22.5515 5.472 22.5515 6.456V12H19.7435V6.888C19.7435 6.108 19.5695 5.532 19.2215 5.16C18.8855 4.788 18.4055 4.602 17.7815 4.602C17.3375 4.602 16.9355 4.698 16.5755 4.89C16.2275 5.07 15.9515 5.352 15.7475 5.736C15.5555 6.12 15.4595 6.612 15.4595 7.212V12H12.6515V2.316H15.3335V4.998L14.8295 4.188C15.1775 3.54 15.6755 3.042 16.3235 2.694C16.9715 2.346 17.7095 2.172 18.5375 2.172ZM29.0568 12.144C27.9168 12.144 27.0288 11.856 26.3928 11.28C25.7568 10.692 25.4388 9.822 25.4388 8.67V0.174H28.2468V8.634C28.2468 9.042 28.3548 9.36 28.5708 9.588C28.7868 9.804 29.0808 9.912 29.4528 9.912C29.8968 9.912 30.2748 9.792 30.5868 9.552L31.3428 11.532C31.0548 11.736 30.7068 11.892 30.2988 12C29.9028 12.096 29.4888 12.144 29.0568 12.144ZM23.9448 4.692V2.532H30.6588V4.692H23.9448ZM37.4262 12.144C36.3222 12.144 35.3502 11.928 34.5102 11.496C33.6822 11.064 33.0402 10.476 32.5842 9.732C32.1282 8.976 31.9002 8.118 31.9002 7.158C31.9002 6.186 32.1222 5.328 32.5662 4.584C33.0222 3.828 33.6402 3.24 34.4202 2.82C35.2002 2.388 36.0822 2.172 37.0662 2.172C38.0142 2.172 38.8662 2.376 39.6222 2.784C40.3902 3.18 40.9962 3.756 41.4402 4.512C41.8842 5.256 42.1062 6.15 42.1062 7.194C42.1062 7.302 42.1002 7.428 42.0882 7.572C42.0762 7.704 42.0642 7.83 42.0522 7.95H34.1862V6.312H40.5762L39.4962 6.798C39.4962 6.294 39.3942 5.856 39.1902 5.484C38.9862 5.112 38.7042 4.824 38.3442 4.62C37.9842 4.404 37.5642 4.296 37.0842 4.296C36.6042 4.296 36.1782 4.404 35.8062 4.62C35.4462 4.824 35.1642 5.118 34.9602 5.502C34.7562 5.874 34.6542 6.318 34.6542 6.834V7.266C34.6542 7.794 34.7682 8.262 34.9962 8.67C35.2362 9.066 35.5662 9.372 35.9862 9.588C36.4182 9.792 36.9222 9.894 37.4982 9.894C38.0142 9.894 38.4642 9.816 38.8482 9.66C39.2442 9.504 39.6042 9.27 39.9282 8.958L41.4222 10.578C40.9782 11.082 40.4202 11.472 39.7482 11.748C39.0762 12.012 38.3022 12.144 37.4262 12.144Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
BIN
apps/accounts/public/images/favicon.png
Normal file
BIN
apps/accounts/public/images/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 422 B |
642
apps/accounts/public/locales/en/translation.json
Normal file
642
apps/accounts/public/locales/en/translation.json
Normal file
|
@ -0,0 +1,642 @@
|
|||
{
|
||||
"HERO_SLIDE_1_TITLE": "<div>Private backups</div><div>for your memories</div>",
|
||||
"HERO_SLIDE_1": "End-to-end encrypted by default",
|
||||
"HERO_SLIDE_2_TITLE": "<div>Safely stored</div><div>at a fallout shelter</div>",
|
||||
"HERO_SLIDE_2": "Designed to outlive",
|
||||
"HERO_SLIDE_3_TITLE": "<div>Available</div><div> everywhere</div>",
|
||||
"HERO_SLIDE_3": "Android, iOS, Web, Desktop",
|
||||
"LOGIN": "Login",
|
||||
"SIGN_UP": "Signup",
|
||||
"NEW_USER": "New to ente",
|
||||
"EXISTING_USER": "Existing user",
|
||||
"ENTER_NAME": "Enter name",
|
||||
"PUBLIC_UPLOADER_NAME_MESSAGE": "Add a name so that your friends know who to thank for these great photos!",
|
||||
"ENTER_EMAIL": "Enter email address",
|
||||
"EMAIL_ERROR": "Enter a valid email",
|
||||
"REQUIRED": "Required",
|
||||
"EMAIL_SENT": "Verification code sent to <a>{{email}}</a>",
|
||||
"CHECK_INBOX": "Please check your inbox (and spam) to complete verification",
|
||||
"ENTER_OTT": "Verification code",
|
||||
"RESEND_MAIL": "Resend code",
|
||||
"VERIFY": "Verify",
|
||||
"UNKNOWN_ERROR": "Something went wrong, please try again",
|
||||
"INVALID_CODE": "Invalid verification code",
|
||||
"EXPIRED_CODE": "Your verification code has expired",
|
||||
"SENDING": "Sending...",
|
||||
"SENT": "Sent!",
|
||||
"PASSWORD": "Password",
|
||||
"LINK_PASSWORD": "Enter password to unlock the album",
|
||||
"RETURN_PASSPHRASE_HINT": "Password",
|
||||
"SET_PASSPHRASE": "Set password",
|
||||
"VERIFY_PASSPHRASE": "Sign in",
|
||||
"INCORRECT_PASSPHRASE": "Incorrect password",
|
||||
"ENTER_ENC_PASSPHRASE": "Please enter a password that we can use to encrypt your data",
|
||||
"PASSPHRASE_DISCLAIMER": "We don't store your password, so if you forget it, <strong>we will not be able to help you </strong>recover your data without a recovery key.",
|
||||
"WELCOME_TO_ENTE_HEADING": "Welcome to <a/>",
|
||||
"WELCOME_TO_ENTE_SUBHEADING": "End to end encrypted photo storage and sharing",
|
||||
"WHERE_YOUR_BEST_PHOTOS_LIVE": "Where your best photos live",
|
||||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generating encryption keys...",
|
||||
"PASSPHRASE_HINT": "Password",
|
||||
"CONFIRM_PASSPHRASE": "Confirm password",
|
||||
"REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)",
|
||||
"REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!",
|
||||
"PASSPHRASE_MATCH_ERROR": "Passwords don't match",
|
||||
"CONSOLE_WARNING_STOP": "STOP!",
|
||||
"CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.",
|
||||
"CREATE_COLLECTION": "New album",
|
||||
"ENTER_ALBUM_NAME": "Album name",
|
||||
"CLOSE_OPTION": "Close (Esc)",
|
||||
"ENTER_FILE_NAME": "File name",
|
||||
"CLOSE": "Close",
|
||||
"NO": "No",
|
||||
"NOTHING_HERE": "Nothing to see here yet 👀",
|
||||
"UPLOAD": "Upload",
|
||||
"IMPORT": "Import",
|
||||
"ADD_PHOTOS": "Add photos",
|
||||
"ADD_MORE_PHOTOS": "Add more photos",
|
||||
"add_photos_one": "Add 1 item",
|
||||
"add_photos_other": "Add {{count, number}} items",
|
||||
"SELECT_PHOTOS": "Select photos",
|
||||
"FILE_UPLOAD": "File Upload",
|
||||
"UPLOAD_STAGE_MESSAGE": {
|
||||
"0": "Preparing to upload",
|
||||
"1": "Reading google metadata files",
|
||||
"2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files metadata extracted",
|
||||
"3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} files processed",
|
||||
"4": "Cancelling remaining uploads",
|
||||
"5": "Backup complete"
|
||||
},
|
||||
"FILE_NOT_UPLOADED_LIST": "The following files were not uploaded",
|
||||
"SUBSCRIPTION_EXPIRED": "Subscription expired",
|
||||
"SUBSCRIPTION_EXPIRED_MESSAGE": "Your subscription has expired, please <a>renew</a>",
|
||||
"STORAGE_QUOTA_EXCEEDED": "Storage limit exceeded",
|
||||
"INITIAL_LOAD_DELAY_WARNING": "First load may take some time",
|
||||
"USER_DOES_NOT_EXIST": "Sorry, could not find a user with that email",
|
||||
"NO_ACCOUNT": "Don't have an account",
|
||||
"ACCOUNT_EXISTS": "Already have an account",
|
||||
"CREATE": "Create",
|
||||
"DOWNLOAD": "Download",
|
||||
"DOWNLOAD_OPTION": "Download (D)",
|
||||
"DOWNLOAD_FAVORITES": "Download favorites",
|
||||
"DOWNLOAD_UNCATEGORIZED": "Download uncategorized",
|
||||
"DOWNLOAD_HIDDEN_ITEMS": "Download hidden items",
|
||||
"COPY_OPTION": "Copy as PNG (Ctrl/Cmd - C)",
|
||||
"TOGGLE_FULLSCREEN": "Toggle fullscreen (F)",
|
||||
"ZOOM_IN_OUT": "Zoom in/out",
|
||||
"PREVIOUS": "Previous (←)",
|
||||
"NEXT": "Next (→)",
|
||||
"TITLE_PHOTOS": "Ente Photos",
|
||||
"TITLE_ALBUMS": "Ente Photos",
|
||||
"TITLE_AUTH": "Ente Auth",
|
||||
"UPLOAD_FIRST_PHOTO": "Upload your first photo",
|
||||
"IMPORT_YOUR_FOLDERS": "Import your folders",
|
||||
"UPLOAD_DROPZONE_MESSAGE": "Drop to backup your files",
|
||||
"WATCH_FOLDER_DROPZONE_MESSAGE": "Drop to add watched folder",
|
||||
"TRASH_FILES_TITLE": "Delete files?",
|
||||
"TRASH_FILE_TITLE": "Delete file?",
|
||||
"DELETE_FILES_TITLE": "Delete immediately?",
|
||||
"DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your ente account.",
|
||||
"DELETE": "Delete",
|
||||
"DELETE_OPTION": "Delete (DEL)",
|
||||
"FAVORITE_OPTION": "Favorite (L)",
|
||||
"UNFAVORITE_OPTION": "Unfavorite (L)",
|
||||
"MULTI_FOLDER_UPLOAD": "Multiple folders detected",
|
||||
"UPLOAD_STRATEGY_CHOICE": "Would you like to upload them into",
|
||||
"UPLOAD_STRATEGY_SINGLE_COLLECTION": "A single album",
|
||||
"OR": "or",
|
||||
"UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Separate albums",
|
||||
"SESSION_EXPIRED_MESSAGE": "Your session has expired, please login again to continue",
|
||||
"SESSION_EXPIRED": "Session expired",
|
||||
"PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets ente's encryption standards, please try using the mobile app or another browser",
|
||||
"CHANGE_PASSWORD": "Change password",
|
||||
"GO_BACK": "Go back",
|
||||
"RECOVERY_KEY": "Recovery key",
|
||||
"SAVE_LATER": "Do this later",
|
||||
"SAVE": "Save Key",
|
||||
"RECOVERY_KEY_DESCRIPTION": "If you forget your password, the only way you can recover your data is with this key.",
|
||||
"RECOVER_KEY_GENERATION_FAILED": "Recovery code could not be generated, please try again",
|
||||
"KEY_NOT_STORED_DISCLAIMER": "We don't store this key, so please save this in a safe place",
|
||||
"FORGOT_PASSWORD": "Forgot password",
|
||||
"RECOVER_ACCOUNT": "Recover account",
|
||||
"RECOVERY_KEY_HINT": "Recovery key",
|
||||
"RECOVER": "Recover",
|
||||
"NO_RECOVERY_KEY": "No recovery key?",
|
||||
"INCORRECT_RECOVERY_KEY": "Incorrect recovery key",
|
||||
"SORRY": "Sorry",
|
||||
"NO_RECOVERY_KEY_MESSAGE": "Due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key",
|
||||
"NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Please drop an email to <a>{{emailID}}</a> from your registered email address",
|
||||
"CONTACT_SUPPORT": "Contact support",
|
||||
"REQUEST_FEATURE": "Request Feature",
|
||||
"SUPPORT": "Support",
|
||||
"CONFIRM": "Confirm",
|
||||
"CANCEL": "Cancel",
|
||||
"LOGOUT": "Logout",
|
||||
"DELETE_ACCOUNT": "Delete account",
|
||||
"DELETE_ACCOUNT_MESSAGE": "<p>Please send an email to <a>{{emailID}}</a> from your registered email address.</p><p>Your request will be processed within 72 hours.</p>",
|
||||
"LOGOUT_MESSAGE": "Are you sure you want to logout?",
|
||||
"CHANGE_EMAIL": "Change email",
|
||||
"OK": "OK",
|
||||
"SUCCESS": "Success",
|
||||
"ERROR": "Error",
|
||||
"MESSAGE": "Message",
|
||||
"INSTALL_MOBILE_APP": "Install our <a>Android</a> or <b>iOS</b> app to automatically backup all your photos",
|
||||
"DOWNLOAD_APP_MESSAGE": "Sorry, this operation is currently only supported on our desktop app",
|
||||
"DOWNLOAD_APP": "Download desktop app",
|
||||
"EXPORT": "Export Data",
|
||||
"SUBSCRIPTION": "Subscription",
|
||||
"SUBSCRIBE": "Subscribe",
|
||||
"MANAGEMENT_PORTAL": "Manage payment method",
|
||||
"MANAGE_FAMILY_PORTAL": "Manage family",
|
||||
"LEAVE_FAMILY_PLAN": "Leave family plan",
|
||||
"LEAVE": "Leave",
|
||||
"LEAVE_FAMILY_CONFIRM": "Are you sure that you want to leave family plan?",
|
||||
"CHOOSE_PLAN": "Choose your plan",
|
||||
"MANAGE_PLAN": "Manage your subscription",
|
||||
"ACTIVE": "Active",
|
||||
"OFFLINE_MSG": "You are offline, cached memories are being shown",
|
||||
"FREE_SUBSCRIPTION_INFO": "You are on the <strong>free</strong> plan that expires on {{date, dateTime}}",
|
||||
"FAMILY_SUBSCRIPTION_INFO": "You are on a family plan managed by",
|
||||
"RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Renews on {{date, dateTime}}",
|
||||
"RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Ends on {{date, dateTime}}",
|
||||
"RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Your subscription will be cancelled on {{date, dateTime}}",
|
||||
"ADD_ON_AVAILABLE_TILL": "Your {{storage, string}} add-on is valid till {{date, dateTime}}",
|
||||
"STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "You have exceeded your storage quota, please <a>upgrade</a>",
|
||||
"SUBSCRIPTION_PURCHASE_SUCCESS": "<p>We've received your payment</p><p>Your subscription is valid till <strong>{{date, dateTime}}</strong></p>",
|
||||
"SUBSCRIPTION_PURCHASE_CANCELLED": "Your purchase was canceled, please try again if you want to subscribe",
|
||||
"SUBSCRIPTION_PURCHASE_FAILED": "Subscription purchase failed , please try again",
|
||||
"SUBSCRIPTION_UPDATE_FAILED": "Subscription updated failed , please try again",
|
||||
"UPDATE_PAYMENT_METHOD_MESSAGE": "We are sorry, payment failed when we tried to charge your card, please update your payment method and try again",
|
||||
"STRIPE_AUTHENTICATION_FAILED": "We are unable to authenticate your payment method. please choose a different payment method and try again",
|
||||
"UPDATE_PAYMENT_METHOD": "Update payment method",
|
||||
"MONTHLY": "Monthly",
|
||||
"YEARLY": "Yearly",
|
||||
"UPDATE_SUBSCRIPTION_MESSAGE": "Are you sure you want to change your plan?",
|
||||
"UPDATE_SUBSCRIPTION": "Change plan",
|
||||
"CANCEL_SUBSCRIPTION": "Cancel subscription",
|
||||
"CANCEL_SUBSCRIPTION_MESSAGE": "<p>All of your data will be deleted from our servers at the end of this billing period.</p><p>Are you sure that you want to cancel your subscription?</p>",
|
||||
"CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "<p>Are you sure you want to cancel your subscription?</p>",
|
||||
"SUBSCRIPTION_CANCEL_FAILED": "Failed to cancel subscription",
|
||||
"SUBSCRIPTION_CANCEL_SUCCESS": "Subscription canceled successfully",
|
||||
"REACTIVATE_SUBSCRIPTION": "Reactivate subscription",
|
||||
"REACTIVATE_SUBSCRIPTION_MESSAGE": "Once reactivated, you will be billed on {{date, dateTime}}",
|
||||
"SUBSCRIPTION_ACTIVATE_SUCCESS": "Subscription activated successfully ",
|
||||
"SUBSCRIPTION_ACTIVATE_FAILED": "Failed to reactivate subscription renewals",
|
||||
"SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Thank you",
|
||||
"CANCEL_SUBSCRIPTION_ON_MOBILE": "Cancel mobile subscription",
|
||||
"CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Please cancel your subscription from the mobile app to activate a subscription here",
|
||||
"MAIL_TO_MANAGE_SUBSCRIPTION": "Please contact us at <a>{{emailID}}</a> to manage your subscription",
|
||||
"RENAME": "Rename",
|
||||
"RENAME_FILE": "Rename file",
|
||||
"RENAME_COLLECTION": "Rename album",
|
||||
"DELETE_COLLECTION_TITLE": "Delete album?",
|
||||
"DELETE_COLLECTION": "Delete album",
|
||||
"DELETE_COLLECTION_MESSAGE": "Also delete the photos (and videos) present in this album from <a>all</a> other albums they are part of?",
|
||||
"DELETE_PHOTOS": "Delete photos",
|
||||
"KEEP_PHOTOS": "Keep photos",
|
||||
"SHARE": "Share",
|
||||
"SHARE_COLLECTION": "Share album",
|
||||
"SHAREES": "Shared with",
|
||||
"SHARE_WITH_SELF": "Oops, you cannot share with yourself",
|
||||
"ALREADY_SHARED": "Oops, you're already sharing this with {{email}}",
|
||||
"SHARING_BAD_REQUEST_ERROR": "Sharing album not allowed",
|
||||
"SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Sharing is disabled for free accounts",
|
||||
"DOWNLOAD_COLLECTION": "Download album",
|
||||
"DOWNLOAD_COLLECTION_MESSAGE": "<p>Are you sure you want to download the complete album?</p><p>All files will be queued for download sequentially</p>",
|
||||
"CREATE_ALBUM_FAILED": "Failed to create album , please try again",
|
||||
"SEARCH": "Search",
|
||||
"SEARCH_RESULTS": "Search results",
|
||||
"NO_RESULTS": "No results found",
|
||||
"SEARCH_HINT": "Search for albums, dates, descriptions, ...",
|
||||
"SEARCH_TYPE": {
|
||||
"COLLECTION": "Album",
|
||||
"LOCATION": "Location",
|
||||
"DATE": "Date",
|
||||
"FILE_NAME": "File name",
|
||||
"THING": "Content",
|
||||
"FILE_CAPTION": "Description",
|
||||
"FILE_TYPE": "File type",
|
||||
"CLIP": "Magic"
|
||||
},
|
||||
"photos_count_zero": "No memories",
|
||||
"photos_count_one": "1 memory",
|
||||
"photos_count_other": "{{count, number}} memories",
|
||||
"TERMS_AND_CONDITIONS": "I agree to the <a>terms</a> and <b>privacy policy</b>",
|
||||
"ADD_TO_COLLECTION": "Add to album",
|
||||
"SELECTED": "selected",
|
||||
"VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "This video cannot be played on your browser",
|
||||
"PEOPLE": "People",
|
||||
"INDEXING_SCHEDULED": "Indexing is scheduled...",
|
||||
"ANALYZING_PHOTOS": "Indexing photos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})",
|
||||
"INDEXING_PEOPLE": "Indexing people in {{indexStatus.nSyncedFiles,number}} photos...",
|
||||
"INDEXING_DONE": "Indexed {{indexStatus.nSyncedFiles,number}} photos",
|
||||
"UNIDENTIFIED_FACES": "unidentified faces",
|
||||
"OBJECTS": "objects",
|
||||
"TEXT": "text",
|
||||
"INFO": "Info ",
|
||||
"INFO_OPTION": "Info (I)",
|
||||
"FILE_NAME": "File name",
|
||||
"CAPTION_PLACEHOLDER": "Add a description",
|
||||
"LOCATION": "Location",
|
||||
"SHOW_ON_MAP": "View on OpenStreetMap",
|
||||
"MAP": "Map",
|
||||
"MAP_SETTINGS": "Map Settings",
|
||||
"ENABLE_MAPS": "Enable Maps?",
|
||||
"ENABLE_MAP": "Enable map",
|
||||
"DISABLE_MAPS": "Disable Maps?",
|
||||
"ENABLE_MAP_DESCRIPTION": "<p>This will show your photos on a world map.</p> <p>The map is hosted by <a>OpenStreetMap</a>, and the exact locations of your photos are never shared.</p> <p>You can disable this feature anytime from Settings.</p>",
|
||||
"DISABLE_MAP_DESCRIPTION": "<p>This will disable the display of your photos on a world map.</p> <p>You can enable this feature anytime from Settings.</p>",
|
||||
"DISABLE_MAP": "Disable map",
|
||||
"DETAILS": "Details",
|
||||
"VIEW_EXIF": "View all EXIF data",
|
||||
"NO_EXIF": "No EXIF data",
|
||||
"EXIF": "EXIF",
|
||||
"ISO": "ISO",
|
||||
"TWO_FACTOR": "Two-factor",
|
||||
"TWO_FACTOR_AUTHENTICATION": "Two-factor authentication",
|
||||
"TWO_FACTOR_QR_INSTRUCTION": "Scan the QR code below with your favorite authenticator app",
|
||||
"ENTER_CODE_MANUALLY": "Enter the code manually",
|
||||
"TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Please enter this code in your favorite authenticator app",
|
||||
"SCAN_QR_CODE": "Scan QR code instead",
|
||||
"ENABLE_TWO_FACTOR": "Enable two-factor",
|
||||
"ENABLE": "Enable",
|
||||
"LOST_DEVICE": "Lost two-factor device",
|
||||
"INCORRECT_CODE": "Incorrect code",
|
||||
"TWO_FACTOR_INFO": "Add an additional layer of security by requiring more than your email and password to log in to your account",
|
||||
"DISABLE_TWO_FACTOR_LABEL": "Disable two-factor authentication",
|
||||
"UPDATE_TWO_FACTOR_LABEL": "Update your authenticator device",
|
||||
"DISABLE": "Disable",
|
||||
"RECONFIGURE": "Reconfigure",
|
||||
"UPDATE_TWO_FACTOR": "Update two-factor",
|
||||
"UPDATE_TWO_FACTOR_MESSAGE": "Continuing forward will void any previously configured authenticators",
|
||||
"UPDATE": "Update",
|
||||
"DISABLE_TWO_FACTOR": "Disable two-factor",
|
||||
"DISABLE_TWO_FACTOR_MESSAGE": "Are you sure you want to disable your two-factor authentication",
|
||||
"TWO_FACTOR_DISABLE_FAILED": "Failed to disable two factor, please try again",
|
||||
"EXPORT_DATA": "Export data",
|
||||
"SELECT_FOLDER": "Select folder",
|
||||
"DESTINATION": "Destination",
|
||||
"START": "Start",
|
||||
"LAST_EXPORT_TIME": "Last export time",
|
||||
"EXPORT_AGAIN": "Resync",
|
||||
"LOCAL_STORAGE_NOT_ACCESSIBLE": "Local storage not accessible",
|
||||
"LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking ente from saving data into local storage. please try loading this page after switching your browsing mode.",
|
||||
"SEND_OTT": "Send OTP",
|
||||
"EMAIl_ALREADY_OWNED": "Email already taken",
|
||||
"ETAGS_BLOCKED": "<p>We were unable to upload the following files because of your browser configuration.</p><p>Please disable any addons that might be preventing ente from using <code>eTags</code> to upload large files, or use our <a>desktop app</a> for a more reliable import experience.</p>",
|
||||
"SKIPPED_VIDEOS_INFO": "<p>Presently we do not support adding videos via public links.</p><p>To share videos, please <a>signup</a> for ente and share with the intended recipients using their email.</p>",
|
||||
"LIVE_PHOTOS_DETECTED": "The photo and video files from your Live Photos have been merged into a single file",
|
||||
"RETRY_FAILED": "Retry failed uploads",
|
||||
"FAILED_UPLOADS": "Failed uploads ",
|
||||
"SKIPPED_FILES": "Ignored uploads",
|
||||
"THUMBNAIL_GENERATION_FAILED_UPLOADS": "Thumbnail generation failed",
|
||||
"UNSUPPORTED_FILES": "Unsupported files",
|
||||
"SUCCESSFUL_UPLOADS": "Successful uploads",
|
||||
"SKIPPED_INFO": "Skipped these as there are files with matching names in the same album",
|
||||
"UNSUPPORTED_INFO": "ente does not support these file formats yet",
|
||||
"BLOCKED_UPLOADS": "Blocked uploads",
|
||||
"SKIPPED_VIDEOS": "Skipped videos",
|
||||
"INPROGRESS_METADATA_EXTRACTION": "In progress",
|
||||
"INPROGRESS_UPLOADS": "Uploads in progress",
|
||||
"TOO_LARGE_UPLOADS": "Large files",
|
||||
"LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Insufficient storage",
|
||||
"LARGER_THAN_AVAILABLE_STORAGE_INFO": "These files were not uploaded as they exceed the maximum size limit for your storage plan",
|
||||
"TOO_LARGE_INFO": "These files were not uploaded as they exceed our maximum file size limit",
|
||||
"THUMBNAIL_GENERATION_FAILED_INFO": "These files were uploaded, but unfortunately we could not generate the thumbnails for them.",
|
||||
"UPLOAD_TO_COLLECTION": "Upload to album",
|
||||
"UNCATEGORIZED": "Uncategorized",
|
||||
"ARCHIVE": "Archive",
|
||||
"FAVORITES": "Favorites",
|
||||
"ARCHIVE_COLLECTION": "Archive album",
|
||||
"ARCHIVE_SECTION_NAME": "Archive",
|
||||
"ALL_SECTION_NAME": "All",
|
||||
"MOVE_TO_COLLECTION": "Move to album",
|
||||
"UNARCHIVE": "Unarchive",
|
||||
"UNARCHIVE_COLLECTION": "Unarchive album",
|
||||
"HIDE_COLLECTION": "Hide album",
|
||||
"UNHIDE_COLLECTION": "Unhide album",
|
||||
"MOVE": "Move",
|
||||
"ADD": "Add",
|
||||
"REMOVE": "Remove",
|
||||
"YES_REMOVE": "Yes, remove",
|
||||
"REMOVE_FROM_COLLECTION": "Remove from album",
|
||||
"TRASH": "Trash",
|
||||
"MOVE_TO_TRASH": "Move to trash",
|
||||
"TRASH_FILES_MESSAGE": "Selected files will be removed from all albums and moved to trash.",
|
||||
"TRASH_FILE_MESSAGE": "The file will be removed from all albums and moved to trash.",
|
||||
"DELETE_PERMANENTLY": "Delete permanently",
|
||||
"RESTORE": "Restore",
|
||||
"RESTORE_TO_COLLECTION": "Restore to album",
|
||||
"EMPTY_TRASH": "Empty trash",
|
||||
"EMPTY_TRASH_TITLE": "Empty trash?",
|
||||
"EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your ente account.",
|
||||
"LEAVE_SHARED_ALBUM": "Yes, leave",
|
||||
"LEAVE_ALBUM": "Leave album",
|
||||
"LEAVE_SHARED_ALBUM_TITLE": "Leave shared album?",
|
||||
"LEAVE_SHARED_ALBUM_MESSAGE": "You will leave the album, and it will stop being visible to you.",
|
||||
"NOT_FILE_OWNER": "You cannot delete files in a shared album",
|
||||
"CONFIRM_SELF_REMOVE_MESSAGE": "Selected items will be removed from this album. Items which are only in this album will be moved to Uncategorized.",
|
||||
"CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Some of the items you are removing were added by other people, and you will lose access to them.",
|
||||
"SORT_BY_CREATION_TIME_ASCENDING": "Oldest",
|
||||
"SORT_BY_UPDATION_TIME_DESCENDING": "Last updated",
|
||||
"SORT_BY_NAME": "Name",
|
||||
"COMPRESS_THUMBNAILS": "Compress thumbnails",
|
||||
"THUMBNAIL_REPLACED": "Thumbnails compressed",
|
||||
"FIX_THUMBNAIL": "Compress",
|
||||
"FIX_THUMBNAIL_LATER": "Compress later",
|
||||
"REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like ente to compress them?",
|
||||
"REPLACE_THUMBNAIL_COMPLETED": "Successfully compressed all thumbnails",
|
||||
"REPLACE_THUMBNAIL_NOOP": "You have no thumbnails that can be compressed further",
|
||||
"REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Could not compress some of your thumbnails, please retry",
|
||||
"FIX_CREATION_TIME": "Fix time",
|
||||
"FIX_CREATION_TIME_IN_PROGRESS": "Fixing time",
|
||||
"CREATION_TIME_UPDATED": "File time updated",
|
||||
"UPDATE_CREATION_TIME_NOT_STARTED": "Select the option you want to use",
|
||||
"UPDATE_CREATION_TIME_COMPLETED": "Successfully updated all files",
|
||||
"UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "File time updation failed for some files, please retry",
|
||||
"CAPTION_CHARACTER_LIMIT": "5000 characters max",
|
||||
"DATE_TIME_ORIGINAL": "EXIF:DateTimeOriginal",
|
||||
"DATE_TIME_DIGITIZED": "EXIF:DateTimeDigitized",
|
||||
"METADATA_DATE": "EXIF:MetadataDate",
|
||||
"CUSTOM_TIME": "Custom time",
|
||||
"REOPEN_PLAN_SELECTOR_MODAL": "Re-open plans",
|
||||
"OPEN_PLAN_SELECTOR_MODAL_FAILED": "Failed to open plans",
|
||||
"INSTALL": "Install",
|
||||
"SHARING_DETAILS": "Sharing details",
|
||||
"MODIFY_SHARING": "Modify sharing",
|
||||
"ADD_COLLABORATORS": "Add collaborators",
|
||||
"ADD_NEW_EMAIL": "Add a new email",
|
||||
"shared_with_people_zero": "Share with specific people",
|
||||
"shared_with_people_one": "Shared with 1 person",
|
||||
"shared_with_people_other": "Shared with {{count, number}} people",
|
||||
"participants_zero": "No participants",
|
||||
"participants_one": "1 participant",
|
||||
"participants_other": "{{count, number}} participants",
|
||||
"ADD_VIEWERS": "Add viewers",
|
||||
"PARTICIPANTS": "Participants",
|
||||
"CHANGE_PERMISSIONS_TO_VIEWER": "<p>{{selectedEmail}} will not be able to add more photos to the album</p> <p>They will still be able to remove photos added by them</p>",
|
||||
"CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} will be able to add photos to the album",
|
||||
"CONVERT_TO_VIEWER": "Yes, convert to viewer",
|
||||
"CONVERT_TO_COLLABORATOR": "Yes, convert to collaborator",
|
||||
"CHANGE_PERMISSION": "Change permission?",
|
||||
"REMOVE_PARTICIPANT": "Remove?",
|
||||
"CONFIRM_REMOVE": "Yes, remove",
|
||||
"MANAGE": "Manage",
|
||||
"ADDED_AS": "Added as",
|
||||
"COLLABORATOR_RIGHTS": "Collaborators can add photos and videos to the shared album",
|
||||
"REMOVE_PARTICIPANT_HEAD": "Remove participant",
|
||||
"OWNER": "Owner",
|
||||
"COLLABORATORS": "Collaborators",
|
||||
"ADD_MORE": "Add more",
|
||||
"VIEWERS": "Viewers",
|
||||
"OR_ADD_EXISTING": "Or pick an existing one",
|
||||
"REMOVE_PARTICIPANT_MESSAGE": "<p>{{selectedEmail}} will be removed from the album</p> <p>Any photos added by them will also be removed from the album</p>",
|
||||
"NOT_FOUND": "404 - not found",
|
||||
"LINK_EXPIRED": "Link expired",
|
||||
"LINK_EXPIRED_MESSAGE": "This link has either expired or been disabled!",
|
||||
"MANAGE_LINK": "Manage link",
|
||||
"LINK_TOO_MANY_REQUESTS": "Sorry, this album has been viewed on too many devices!",
|
||||
"FILE_DOWNLOAD": "Allow downloads",
|
||||
"LINK_PASSWORD_LOCK": "Password lock",
|
||||
"PUBLIC_COLLECT": "Allow adding photos",
|
||||
"LINK_DEVICE_LIMIT": "Device limit",
|
||||
"NO_DEVICE_LIMIT": "None",
|
||||
"LINK_EXPIRY": "Link expiry",
|
||||
"NEVER": "Never",
|
||||
"DISABLE_FILE_DOWNLOAD": "Disable download",
|
||||
"DISABLE_FILE_DOWNLOAD_MESSAGE": "<p>Are you sure that you want to disable the download button for files?</p><p>Viewers can still take screenshots or save a copy of your photos using external tools.</p>",
|
||||
"MALICIOUS_CONTENT": "Contains malicious content",
|
||||
"COPYRIGHT": "Infringes on the copyright of someone I am authorized to represent",
|
||||
"SHARED_USING": "Shared using ",
|
||||
"ENTE_IO": "ente.io",
|
||||
"SHARING_REFERRAL_CODE": "Use code <strong>{{referralCode}}</strong> to get 10 GB free",
|
||||
"LIVE": "LIVE",
|
||||
"DISABLE_PASSWORD": "Disable password lock",
|
||||
"DISABLE_PASSWORD_MESSAGE": "Are you sure that you want to disable the password lock?",
|
||||
"PASSWORD_LOCK": "Password lock",
|
||||
"LOCK": "Lock",
|
||||
"DOWNLOAD_UPLOAD_LOGS": "Debug logs",
|
||||
"UPLOAD_FILES": "File",
|
||||
"UPLOAD_DIRS": "Folder",
|
||||
"UPLOAD_GOOGLE_TAKEOUT": "Google takeout",
|
||||
"DEDUPLICATE_FILES": "Deduplicate files",
|
||||
"AUTHENTICATOR_SECTION": "Authenticator",
|
||||
"NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared",
|
||||
"CLUB_BY_CAPTURE_TIME": "Club by capture time",
|
||||
"FILES": "Files",
|
||||
"EACH": "Each",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?",
|
||||
"STOP_UPLOADS_HEADER": "Stop uploads?",
|
||||
"YES_STOP_UPLOADS": "Yes, stop uploads",
|
||||
"STOP_DOWNLOADS_HEADER": "Stop downloads?",
|
||||
"YES_STOP_DOWNLOADS": "Yes, stop downloads",
|
||||
"STOP_ALL_DOWNLOADS_MESSAGE": "Are you sure that you want to stop all the downloads in progress?",
|
||||
"albums_one": "1 Album",
|
||||
"albums_other": "{{count, number}} Albums",
|
||||
"ALL_ALBUMS": "All Albums",
|
||||
"ALBUMS": "Albums",
|
||||
"ALL_HIDDEN_ALBUMS": "All hidden albums",
|
||||
"HIDDEN_ALBUMS": "Hidden albums",
|
||||
"HIDDEN_ITEMS": "Hidden items",
|
||||
"HIDDEN_ITEMS_SECTION_NAME": "Hidden_items",
|
||||
"ENTER_TWO_FACTOR_OTP": "Enter the 6-digit code from your authenticator app.",
|
||||
"CREATE_ACCOUNT": "Create account",
|
||||
"COPIED": "Copied",
|
||||
"CANVAS_BLOCKED_TITLE": "Unable to generate thumbnail",
|
||||
"CANVAS_BLOCKED_MESSAGE": "<p>It looks like your browser has disabled access to canvas, which is necessary to generate thumbnails for your photos </p> <p> Please enable access to your browser's canvas, or check out our desktop app</p>",
|
||||
"WATCH_FOLDERS": "Watch folders",
|
||||
"UPGRADE_NOW": "Upgrade now",
|
||||
"RENEW_NOW": "Renew now",
|
||||
"STORAGE": "Storage",
|
||||
"USED": "used",
|
||||
"YOU": "You",
|
||||
"FAMILY": "Family",
|
||||
"FREE": "free",
|
||||
"OF": "of",
|
||||
"WATCHED_FOLDERS": "Watched folders",
|
||||
"NO_FOLDERS_ADDED": "No folders added yet!",
|
||||
"FOLDERS_AUTOMATICALLY_MONITORED": "The folders you add here will monitored to automatically",
|
||||
"UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to ente",
|
||||
"REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from ente",
|
||||
"ADD_FOLDER": "Add folder",
|
||||
"STOP_WATCHING": "Stop watching",
|
||||
"STOP_WATCHING_FOLDER": "Stop watching folder?",
|
||||
"STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but ente will stop automatically updating the linked ente album on changes in this folder.",
|
||||
"YES_STOP": "Yes, stop",
|
||||
"MONTH_SHORT": "mo",
|
||||
"YEAR": "year",
|
||||
"FAMILY_PLAN": "Family plan",
|
||||
"DOWNLOAD_LOGS": "Download logs",
|
||||
"DOWNLOAD_LOGS_MESSAGE": "<p>This will download debug logs, which you can email to us to help debug your issue.</p><p> Please note that file names will be included to help track issues with specific files. </p>",
|
||||
"CHANGE_FOLDER": "Change Folder",
|
||||
"TWO_MONTHS_FREE": "Get 2 months free on yearly plans",
|
||||
"GB": "GB",
|
||||
"POPULAR": "Popular",
|
||||
"FREE_PLAN_OPTION_LABEL": "Continue with free trial",
|
||||
"FREE_PLAN_DESCRIPTION": "1 GB for 1 year",
|
||||
"CURRENT_USAGE": "Current usage is <strong>{{usage}}</strong>",
|
||||
"WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to ente on your computer, or download the ente mobile/desktop app.",
|
||||
"DRAG_AND_DROP_HINT": "Or drag and drop into the ente window",
|
||||
"CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.<br/><br/>This action is not reversible.",
|
||||
"AUTHENTICATE": "Authenticate",
|
||||
"UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection",
|
||||
"UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections",
|
||||
"NEVERMIND": "Nevermind",
|
||||
"UPDATE_AVAILABLE": "Update available",
|
||||
"UPDATE_INSTALLABLE_MESSAGE": "A new version of ente is ready to be installed.",
|
||||
"INSTALL_NOW": "Install now",
|
||||
"INSTALL_ON_NEXT_LAUNCH": "Install on next launch",
|
||||
"UPDATE_AVAILABLE_MESSAGE": "A new version of ente has been released, but it cannot be automatically downloaded and installed.",
|
||||
"DOWNLOAD_AND_INSTALL": "Download and install",
|
||||
"IGNORE_THIS_VERSION": "Ignore this version",
|
||||
"TODAY": "Today",
|
||||
"YESTERDAY": "Yesterday",
|
||||
"NAME_PLACEHOLDER": "Name...",
|
||||
"ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Cannot create albums from file/folder mix",
|
||||
"ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "<p>You have dragged and dropped a mixture of files and folders.</p><p>Please provide either only files, or only folders when selecting option to create separate albums</p>",
|
||||
"CHOSE_THEME": "Choose theme",
|
||||
"ML_SEARCH": "ML search (beta)",
|
||||
"ENABLE_ML_SEARCH_DESCRIPTION": "<p>This will enable on-device machine learning and face search which will start analyzing your uploaded photos locally.</p><p>For the first run after login or enabling this feature, it will download all images on local device to analyze them. So please only enable this if you are ok with bandwidth and local processing of all images in your photo library.</p><p>If this is the first time you're enabling this, we'll also ask your permission to process face data.</p>",
|
||||
"ML_MORE_DETAILS": "More details",
|
||||
"ENABLE_FACE_SEARCH": "Enable face search",
|
||||
"ENABLE_FACE_SEARCH_TITLE": "Enable face search?",
|
||||
"ENABLE_FACE_SEARCH_DESCRIPTION": "<p>If you enable face search, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.<p/><p><a>Please click here for more details about this feature in our privacy policy</a></p>",
|
||||
"DISABLE_BETA": "Disable beta",
|
||||
"DISABLE_FACE_SEARCH": "Disable face search",
|
||||
"DISABLE_FACE_SEARCH_TITLE": "Disable face search?",
|
||||
"DISABLE_FACE_SEARCH_DESCRIPTION": "<p>ente will stop processing face geometry, and will also disable ML search (beta)</p><p>You can reenable face search again if you wish, so this operation is safe.</p>",
|
||||
"ADVANCED": "Advanced",
|
||||
"FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow ente to process face geometry",
|
||||
"LABS": "Labs",
|
||||
"YOURS": "yours",
|
||||
"PASSPHRASE_STRENGTH_WEAK": "Password strength: Weak",
|
||||
"PASSPHRASE_STRENGTH_MODERATE": "Password strength: Moderate",
|
||||
"PASSPHRASE_STRENGTH_STRONG": "Password strength: Strong",
|
||||
"PREFERENCES": "Preferences",
|
||||
"LANGUAGE": "Language",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST": "Invalid export directory",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "<p>The export directory you have selected does not exist.</p><p> Please select a valid directory.</p>",
|
||||
"SUBSCRIPTION_VERIFICATION_ERROR": "Subscription verification failed",
|
||||
"STORAGE_UNITS": {
|
||||
"B": "B",
|
||||
"KB": "KB",
|
||||
"MB": "MB",
|
||||
"GB": "GB",
|
||||
"TB": "TB"
|
||||
},
|
||||
"AFTER_TIME": {
|
||||
"HOUR": "after an hour",
|
||||
"DAY": "after a day",
|
||||
"WEEK": "after a week",
|
||||
"MONTH": "after a month",
|
||||
"YEAR": "after a year"
|
||||
},
|
||||
"COPY_LINK": "Copy link",
|
||||
"DONE": "Done",
|
||||
"LINK_SHARE_TITLE": "Or share a link",
|
||||
"REMOVE_LINK": "Remove link",
|
||||
"CREATE_PUBLIC_SHARING": "Create public link",
|
||||
"PUBLIC_LINK_CREATED": "Public link created",
|
||||
"PUBLIC_LINK_ENABLED": "Public link enabled",
|
||||
"COLLECT_PHOTOS": "Collect photos",
|
||||
"PUBLIC_COLLECT_SUBTEXT": "Allow people with the link to also add photos to the shared album.",
|
||||
"STOP_EXPORT": "Stop",
|
||||
"EXPORT_PROGRESS": "<a>{{progress.success, number}} / {{progress.total, number}}</a> items synced",
|
||||
"MIGRATING_EXPORT": "Preparing...",
|
||||
"RENAMING_COLLECTION_FOLDERS": "Renaming album folders...",
|
||||
"TRASHING_DELETED_FILES": "Trashing deleted files...",
|
||||
"TRASHING_DELETED_COLLECTIONS": "Trashing deleted albums...",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "Export started",
|
||||
"IN_PROGRESS": "Export already in progress",
|
||||
"FINISH": "Export finished",
|
||||
"UP_TO_DATE": "No new files to export"
|
||||
},
|
||||
"CONTINUOUS_EXPORT": "Sync continuously",
|
||||
"TOTAL_ITEMS": "Total items",
|
||||
"PENDING_ITEMS": "Pending items",
|
||||
"EXPORT_STARTING": "Export starting...",
|
||||
"DELETE_ACCOUNT_REASON_LABEL": "What is the main reason you are deleting your account?",
|
||||
"DELETE_ACCOUNT_REASON_PLACEHOLDER": "Select a reason",
|
||||
"DELETE_REASON": {
|
||||
"MISSING_FEATURE": "It's missing a key feature that I need",
|
||||
"BROKEN_BEHAVIOR": "The app or a certain feature does not behave as I think it should",
|
||||
"FOUND_ANOTHER_SERVICE": "I found another service that I like better",
|
||||
"NOT_LISTED": "My reason isn't listed"
|
||||
},
|
||||
"DELETE_ACCOUNT_FEEDBACK_LABEL": "We are sorry to see you go. Please explain why you are leaving to help us improve.",
|
||||
"DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback",
|
||||
"CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Yes, I want to permanently delete this account and all its data",
|
||||
"CONFIRM_DELETE_ACCOUNT": "Confirm Account Deletion",
|
||||
"FEEDBACK_REQUIRED": "Kindly help us with this information",
|
||||
"FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "What does the other service do better?",
|
||||
"RECOVER_TWO_FACTOR": "Recover two-factor",
|
||||
"at": "at",
|
||||
"AUTH_NEXT": "next",
|
||||
"AUTH_DOWNLOAD_MOBILE_APP": "Download our mobile app to manage your secrets",
|
||||
"HIDDEN": "Hidden",
|
||||
"HIDE": "Hide",
|
||||
"UNHIDE": "Unhide",
|
||||
"UNHIDE_TO_COLLECTION": "Unhide to album",
|
||||
"SORT_BY": "Sort by",
|
||||
"NEWEST_FIRST": "Newest first",
|
||||
"OLDEST_FIRST": "Oldest first",
|
||||
"CONVERSION_FAILED_NOTIFICATION_MESSAGE": "This file could not be previewed. Click here to download the original.",
|
||||
"SELECT_COLLECTION": "Select album",
|
||||
"PIN_ALBUM": "Pin album",
|
||||
"UNPIN_ALBUM": "Unpin album",
|
||||
"DOWNLOAD_COMPLETE": "Download complete",
|
||||
"DOWNLOADING_COLLECTION": "Downloading {{name}}",
|
||||
"DOWNLOAD_FAILED": "Download failed",
|
||||
"DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files",
|
||||
"CRASH_REPORTING": "Crash reporting",
|
||||
"CHRISTMAS": "Christmas",
|
||||
"CHRISTMAS_EVE": "Christmas Eve",
|
||||
"NEW_YEAR": "New Year",
|
||||
"NEW_YEAR_EVE": "New Year's Eve",
|
||||
"IMAGE": "Image",
|
||||
"VIDEO": "Video",
|
||||
"LIVE_PHOTO": "Live Photo",
|
||||
"CONVERT": "Convert",
|
||||
"CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?",
|
||||
"CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.",
|
||||
"BRIGHTNESS": "Brightness",
|
||||
"CONTRAST": "Contrast",
|
||||
"SATURATION": "Saturation",
|
||||
"BLUR": "Blur",
|
||||
"INVERT_COLORS": "Invert Colors",
|
||||
"ASPECT_RATIO": "Aspect Ratio",
|
||||
"SQUARE": "Square",
|
||||
"ROTATE_LEFT": "Rotate Left",
|
||||
"ROTATE_RIGHT": "Rotate Right",
|
||||
"FLIP_VERTICALLY": "Flip Vertically",
|
||||
"FLIP_HORIZONTALLY": "Flip Horizontally",
|
||||
"DOWNLOAD_EDITED": "Download Edited",
|
||||
"SAVE_A_COPY_TO_ENTE": "Save a copy to ente",
|
||||
"RESTORE_ORIGINAL": "Restore Original",
|
||||
"TRANSFORM": "Transform",
|
||||
"COLORS": "Colors",
|
||||
"FLIP": "Flip",
|
||||
"ROTATION": "Rotation",
|
||||
"RESET": "Reset",
|
||||
"PHOTO_EDITOR": "Photo Editor",
|
||||
"FASTER_UPLOAD": "Faster uploads",
|
||||
"FASTER_UPLOAD_DESCRIPTION": "Route uploads through nearby servers",
|
||||
"STATUS": "Status",
|
||||
"INDEXED_ITEMS": "Indexed items",
|
||||
"CACHE_DIRECTORY": "Cache folder",
|
||||
"DELETE_PASSKEY": "Delete passkey",
|
||||
"DELETE_PASSKEY_CONFIRMATION": "Are you sure you want to delete this passkey? This action is irreversible.",
|
||||
"RENAME_PASSKEY": "Rename passkey",
|
||||
"ADD_PASSKEY": "Add passkey",
|
||||
"ENTER_PASSKEY_NAME": "Enter passkey name",
|
||||
"PASSKEYS_DESCRIPTION": "Passkeys are a modern and secure second-factor for your Ente account. They use on-device biometric authentication for convenience and security.",
|
||||
"CREATED_AT": "Created at",
|
||||
"PASSKEY_LOGIN_FAILED": "Passkey login failed",
|
||||
"PASSKEY_LOGIN_URL_INVALID": "The login URL is invalid.",
|
||||
"PASSKEY_LOGIN_ERRORED": "An error occurred while logging in with passkey.",
|
||||
"TRY_AGAIN": "Try again",
|
||||
"PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Follow the steps from your browser to continue logging in.",
|
||||
"LOGIN_WITH_PASSKEY": "Login with passkey"
|
||||
}
|
||||
|
1
apps/accounts/public/next.svg
Normal file
1
apps/accounts/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/accounts/public/vercel.svg
Normal file
1
apps/accounts/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 |
3
apps/accounts/sentry.client.config.ts
Normal file
3
apps/accounts/sentry.client.config.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { initSentry } from '@ente/shared/sentry/config/sentry.config.base';
|
||||
|
||||
initSentry('https://bd3656fc40d74d5e8f278132817963a3@sentry.ente.io/2');
|
3
apps/accounts/sentry.properties
Normal file
3
apps/accounts/sentry.properties
Normal file
|
@ -0,0 +1,3 @@
|
|||
defaults.url=https://sentry.ente.io/
|
||||
defaults.org=ente
|
||||
defaults.project=photos-web
|
0
apps/accounts/sentry.server.config.ts
Normal file
0
apps/accounts/sentry.server.config.ts
Normal file
124
apps/accounts/src/pages/_app.tsx
Normal file
124
apps/accounts/src/pages/_app.tsx
Normal file
|
@ -0,0 +1,124 @@
|
|||
import { CacheProvider } from '@emotion/react';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
import { EnteAppProps } from '@ente/shared/apps/types';
|
||||
import { Overlay } from '@ente/shared/components/Container';
|
||||
import DialogBoxV2 from '@ente/shared/components/DialogBoxV2';
|
||||
import {
|
||||
DialogBoxAttributesV2,
|
||||
SetDialogBoxAttributesV2,
|
||||
} from '@ente/shared/components/DialogBoxV2/types';
|
||||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
import AppNavbar from '@ente/shared/components/Navbar/app';
|
||||
import { useLocalState } from '@ente/shared/hooks/useLocalState';
|
||||
import { setupI18n } from '@ente/shared/i18n';
|
||||
import HTTPService from '@ente/shared/network/HTTPService';
|
||||
import { LS_KEYS, getData } from '@ente/shared/storage/localStorage';
|
||||
import { getTheme } from '@ente/shared/themes';
|
||||
import { THEME_COLOR } from '@ente/shared/themes/constants';
|
||||
import createEmotionCache from '@ente/shared/themes/createEmotionCache';
|
||||
import { CssBaseline, useMediaQuery } from '@mui/material';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { useRouter } from 'next/router';
|
||||
import { createContext, useEffect, useState } from 'react';
|
||||
import 'styles/global.css';
|
||||
|
||||
interface AppContextProps {
|
||||
isMobile: boolean;
|
||||
showNavBar: (show: boolean) => void;
|
||||
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
|
||||
}
|
||||
|
||||
export const AppContext = createContext<AppContextProps>({} as AppContextProps);
|
||||
|
||||
// Client-side cache, shared for the whole session of the user in the browser.
|
||||
const clientSideEmotionCache = createEmotionCache();
|
||||
|
||||
export default function App(props: EnteAppProps) {
|
||||
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
|
||||
|
||||
const [showNavbar, setShowNavBar] = useState(false);
|
||||
|
||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
|
||||
useState<DialogBoxAttributesV2>();
|
||||
|
||||
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDialogBoxV2View(true);
|
||||
}, [dialogBoxAttributeV2]);
|
||||
|
||||
const showNavBar = (show: boolean) => setShowNavBar(show);
|
||||
|
||||
const isMobile = useMediaQuery('(max-width:428px)');
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
Component,
|
||||
emotionCache = clientSideEmotionCache,
|
||||
pageProps,
|
||||
} = props;
|
||||
|
||||
const [themeColor] = useLocalState(LS_KEYS.THEME, THEME_COLOR.DARK);
|
||||
|
||||
useEffect(() => {
|
||||
setupI18n().finally(() => setIsI18nReady(true));
|
||||
}, []);
|
||||
|
||||
const setupPackageName = () => {
|
||||
const pkg = getData(LS_KEYS.CLIENT_PACKAGE);
|
||||
if (!pkg) return;
|
||||
HTTPService.setHeaders({
|
||||
'X-Client-Package': pkg.name,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
router.events.on('routeChangeComplete', setupPackageName);
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', setupPackageName);
|
||||
};
|
||||
}, [router.events]);
|
||||
|
||||
const closeDialogBoxV2 = () => setDialogBoxV2View(false);
|
||||
|
||||
const theme = getTheme(themeColor, APPS.PHOTOS);
|
||||
|
||||
return (
|
||||
<CacheProvider value={emotionCache}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline enableColorScheme />
|
||||
<DialogBoxV2
|
||||
sx={{ zIndex: 1600 }}
|
||||
open={dialogBoxV2View}
|
||||
onClose={closeDialogBoxV2}
|
||||
attributes={dialogBoxAttributeV2 as any}
|
||||
/>
|
||||
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
isMobile,
|
||||
showNavBar,
|
||||
setDialogBoxAttributesV2:
|
||||
setDialogBoxAttributesV2 as any,
|
||||
}}>
|
||||
{!isI18nReady && (
|
||||
<Overlay
|
||||
sx={(theme) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 2000,
|
||||
backgroundColor: (theme as any).colors
|
||||
.background.base,
|
||||
})}>
|
||||
<EnteSpinner />
|
||||
</Overlay>
|
||||
)}
|
||||
{showNavbar && <AppNavbar isMobile={isMobile} />}
|
||||
<Component {...pageProps} />
|
||||
</AppContext.Provider>
|
||||
</ThemeProvider>
|
||||
</CacheProvider>
|
||||
);
|
||||
}
|
7
apps/accounts/src/pages/_document.tsx
Normal file
7
apps/accounts/src/pages/_document.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import DocumentPage, {
|
||||
EnteDocumentProps,
|
||||
} from '@ente/shared/next/pages/_document';
|
||||
|
||||
export default function Document(props: EnteDocumentProps) {
|
||||
return <DocumentPage {...props} />;
|
||||
}
|
59
apps/accounts/src/pages/account-handoff.tsx
Normal file
59
apps/accounts/src/pages/account-handoff.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { VerticallyCentered } from '@ente/shared/components/Container';
|
||||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
import { ACCOUNTS_PAGES } from '@ente/shared/constants/pages';
|
||||
import HTTPService from '@ente/shared/network/HTTPService';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import { LS_KEYS, getData, setData } from '@ente/shared/storage/localStorage';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const AccountHandoff = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const retrieveAccountData = () => {
|
||||
try {
|
||||
extractAccountsToken();
|
||||
|
||||
router.push(ACCOUNTS_PAGES.PASSKEYS);
|
||||
} catch (e) {
|
||||
logError(e, 'Failed to deserialize and set passed user data');
|
||||
router.push(ACCOUNTS_PAGES.LOGIN);
|
||||
}
|
||||
};
|
||||
|
||||
const getClientPackageName = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const pkg = urlParams.get('package');
|
||||
if (!pkg) return;
|
||||
setData(LS_KEYS.CLIENT_PACKAGE, { name: pkg });
|
||||
HTTPService.setHeaders({
|
||||
'X-Client-Package': pkg,
|
||||
});
|
||||
};
|
||||
|
||||
const extractAccountsToken = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const token = urlParams.get('token');
|
||||
if (!token) {
|
||||
throw new Error('token not found');
|
||||
}
|
||||
|
||||
const user = getData(LS_KEYS.USER) || {};
|
||||
user.token = token;
|
||||
|
||||
setData(LS_KEYS.USER, user);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getClientPackageName();
|
||||
retrieveAccountData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<VerticallyCentered>
|
||||
<EnteSpinner />
|
||||
</VerticallyCentered>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountHandoff;
|
17
apps/accounts/src/pages/credentials/index.tsx
Normal file
17
apps/accounts/src/pages/credentials/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import CredentialPage from '@ente/accounts/pages/credentials';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppContext } from '../_app';
|
||||
import { useContext } from 'react';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
|
||||
export default function Credential() {
|
||||
const appContext = useContext(AppContext);
|
||||
const router = useRouter();
|
||||
return (
|
||||
<CredentialPage
|
||||
appContext={appContext}
|
||||
router={router}
|
||||
appName={APPS.ACCOUNTS}
|
||||
/>
|
||||
);
|
||||
}
|
17
apps/accounts/src/pages/generate/index.tsx
Normal file
17
apps/accounts/src/pages/generate/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import GeneratePage from '@ente/accounts/pages/generate';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export default function Generate() {
|
||||
const appContext = useContext(AppContext);
|
||||
const router = useRouter();
|
||||
return (
|
||||
<GeneratePage
|
||||
appContext={appContext}
|
||||
router={router}
|
||||
appName={APPS.ACCOUNTS}
|
||||
/>
|
||||
);
|
||||
}
|
13
apps/accounts/src/pages/index.tsx
Normal file
13
apps/accounts/src/pages/index.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const Index = () => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.push('/login');
|
||||
}, []);
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default Index;
|
17
apps/accounts/src/pages/login/index.tsx
Normal file
17
apps/accounts/src/pages/login/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import LoginPage from '@ente/accounts/pages/login';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppContext } from '../_app';
|
||||
import { useContext } from 'react';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
|
||||
export default function Login() {
|
||||
const appContext = useContext(AppContext);
|
||||
const router = useRouter();
|
||||
return (
|
||||
<LoginPage
|
||||
appContext={appContext}
|
||||
router={router}
|
||||
appName={APPS.ACCOUNTS}
|
||||
/>
|
||||
);
|
||||
}
|
71
apps/accounts/src/pages/passkeys/DeletePasskeyModal.tsx
Normal file
71
apps/accounts/src/pages/passkeys/DeletePasskeyModal.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import DialogBoxV2 from '@ente/shared/components/DialogBoxV2';
|
||||
import EnteButton from '@ente/shared/components/EnteButton';
|
||||
import { Button, Stack, Typography } from '@mui/material';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { useContext, useState } from 'react';
|
||||
import { deletePasskey } from 'services/passkeysService';
|
||||
import { PasskeysContext } from '.';
|
||||
import { t } from 'i18next';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DeletePasskeyModal = (props: IProps) => {
|
||||
const { isMobile } = useContext(AppContext);
|
||||
const { selectedPasskey, setShowPasskeyDrawer } =
|
||||
useContext(PasskeysContext);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const doDelete = async () => {
|
||||
if (!selectedPasskey) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await deletePasskey(selectedPasskey.id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
props.onClose();
|
||||
setShowPasskeyDrawer(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogBoxV2
|
||||
fullWidth
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
fullScreen={isMobile}
|
||||
attributes={{
|
||||
title: t('DELETE_PASSKEY'),
|
||||
secondary: {
|
||||
action: props.onClose,
|
||||
text: t('CANCEL'),
|
||||
},
|
||||
}}>
|
||||
<Stack spacing={'8px'}>
|
||||
<Typography>{t('DELETE_PASSKEY_CONFIRMATION')}</Typography>
|
||||
<EnteButton
|
||||
type="submit"
|
||||
size="large"
|
||||
color="critical"
|
||||
loading={loading}
|
||||
onClick={doDelete}>
|
||||
{t('DELETE')}
|
||||
</EnteButton>
|
||||
<Button
|
||||
size="large"
|
||||
color={'secondary'}
|
||||
onClick={props.onClose}>
|
||||
{t('CANCEL')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</DialogBoxV2>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeletePasskeyModal;
|
100
apps/accounts/src/pages/passkeys/ManagePasskeyDrawer.tsx
Normal file
100
apps/accounts/src/pages/passkeys/ManagePasskeyDrawer.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { EnteDrawer } from '@ente/shared/components/EnteDrawer';
|
||||
import { PasskeysContext } from '.';
|
||||
import { Stack } from '@mui/material';
|
||||
import Titlebar from '@ente/shared/components/Titlebar';
|
||||
import { MenuItemGroup } from '@ente/shared/components/Menu/MenuItemGroup';
|
||||
import { EnteMenuItem } from '@ente/shared/components/Menu/EnteMenuItem';
|
||||
import { useContext, useState } from 'react';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import MenuItemDivider from '@ente/shared/components/Menu/MenuItemDivider';
|
||||
import DeletePasskeyModal from './DeletePasskeyModal';
|
||||
import RenamePasskeyModal from './RenamePasskeyModal';
|
||||
import InfoItem from '@ente/shared/components/Info/InfoItem';
|
||||
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
|
||||
import { formatDateTimeFull } from '@ente/shared/time/format';
|
||||
import { t } from 'i18next';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const ManagePasskeyDrawer = (props: IProps) => {
|
||||
const { setShowPasskeyDrawer, refreshPasskeys, selectedPasskey } =
|
||||
useContext(PasskeysContext);
|
||||
|
||||
const [showDeletePasskeyModal, setShowDeletePasskeyModal] = useState(false);
|
||||
const [showRenamePasskeyModal, setShowRenamePasskeyModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EnteDrawer
|
||||
anchor="right"
|
||||
open={props.open}
|
||||
onClose={() => {
|
||||
setShowPasskeyDrawer(false);
|
||||
}}>
|
||||
{selectedPasskey && (
|
||||
<>
|
||||
<Stack spacing={'4px'} py={'12px'}>
|
||||
<Titlebar
|
||||
onClose={() => {
|
||||
setShowPasskeyDrawer(false);
|
||||
}}
|
||||
title="Manage Passkey"
|
||||
onRootClose={() => {
|
||||
setShowPasskeyDrawer(false);
|
||||
}}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={<CalendarTodayIcon />}
|
||||
title={t('CREATED_AT')}
|
||||
caption={
|
||||
`${formatDateTimeFull(
|
||||
selectedPasskey.createdAt / 1000
|
||||
)}` || ''
|
||||
}
|
||||
loading={!selectedPasskey}
|
||||
hideEditOption
|
||||
/>
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
onClick={() => {
|
||||
setShowRenamePasskeyModal(true);
|
||||
}}
|
||||
startIcon={<EditIcon />}
|
||||
label={'Rename Passkey'}
|
||||
/>
|
||||
<MenuItemDivider />
|
||||
<EnteMenuItem
|
||||
onClick={() => {
|
||||
setShowDeletePasskeyModal(true);
|
||||
}}
|
||||
startIcon={<DeleteIcon />}
|
||||
label={'Delete Passkey'}
|
||||
color="critical"
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</EnteDrawer>
|
||||
<DeletePasskeyModal
|
||||
open={showDeletePasskeyModal}
|
||||
onClose={() => {
|
||||
setShowDeletePasskeyModal(false);
|
||||
refreshPasskeys();
|
||||
}}
|
||||
/>
|
||||
<RenamePasskeyModal
|
||||
open={showRenamePasskeyModal}
|
||||
onClose={() => {
|
||||
setShowRenamePasskeyModal(false);
|
||||
refreshPasskeys();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagePasskeyDrawer;
|
29
apps/accounts/src/pages/passkeys/PasskeyListItem.tsx
Normal file
29
apps/accounts/src/pages/passkeys/PasskeyListItem.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { EnteMenuItem } from '@ente/shared/components/Menu/EnteMenuItem';
|
||||
import { Passkey } from 'types/passkey';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import { useContext } from 'react';
|
||||
import { PasskeysContext } from '.';
|
||||
import KeyIcon from '@mui/icons-material/Key';
|
||||
|
||||
interface IProps {
|
||||
passkey: Passkey;
|
||||
}
|
||||
|
||||
const PasskeyListItem = (props: IProps) => {
|
||||
const { setSelectedPasskey, setShowPasskeyDrawer } =
|
||||
useContext(PasskeysContext);
|
||||
|
||||
return (
|
||||
<EnteMenuItem
|
||||
onClick={() => {
|
||||
setSelectedPasskey(props.passkey);
|
||||
setShowPasskeyDrawer(true);
|
||||
}}
|
||||
startIcon={<KeyIcon />}
|
||||
endIcon={<ChevronRightIcon />}
|
||||
label={props.passkey?.friendlyName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeyListItem;
|
26
apps/accounts/src/pages/passkeys/PasskeysList.tsx
Normal file
26
apps/accounts/src/pages/passkeys/PasskeysList.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { MenuItemGroup } from '@ente/shared/components/Menu/MenuItemGroup';
|
||||
import { Passkey } from 'types/passkey';
|
||||
import PasskeyListItem from './PasskeyListItem';
|
||||
import MenuItemDivider from '@ente/shared/components/Menu/MenuItemDivider';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
interface IProps {
|
||||
passkeys: Passkey[];
|
||||
}
|
||||
|
||||
const PasskeyComponent = (props: IProps) => {
|
||||
return (
|
||||
<>
|
||||
<MenuItemGroup>
|
||||
{props.passkeys?.map((passkey, i) => (
|
||||
<Fragment key={passkey.id}>
|
||||
<PasskeyListItem passkey={passkey} />
|
||||
{i < props.passkeys.length - 1 && <MenuItemDivider />}
|
||||
</Fragment>
|
||||
))}
|
||||
</MenuItemGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeyComponent;
|
56
apps/accounts/src/pages/passkeys/RenamePasskeyModal.tsx
Normal file
56
apps/accounts/src/pages/passkeys/RenamePasskeyModal.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import DialogBoxV2 from '@ente/shared/components/DialogBoxV2';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { useContext } from 'react';
|
||||
import { PasskeysContext } from '.';
|
||||
import SingleInputForm from '@ente/shared/components/SingleInputForm';
|
||||
import { t } from 'i18next';
|
||||
import { renamePasskey } from 'services/passkeysService';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const RenamePasskeyModal = (props: IProps) => {
|
||||
const { isMobile } = useContext(AppContext);
|
||||
const { selectedPasskey } = useContext(PasskeysContext);
|
||||
|
||||
const onSubmit = async (inputValue: string) => {
|
||||
if (!selectedPasskey) return;
|
||||
try {
|
||||
await renamePasskey(selectedPasskey.id, inputValue);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogBoxV2
|
||||
fullWidth
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
fullScreen={isMobile}
|
||||
attributes={{
|
||||
title: t('RENAME_PASSKEY'),
|
||||
secondary: {
|
||||
action: props.onClose,
|
||||
text: t('CANCEL'),
|
||||
},
|
||||
}}>
|
||||
<SingleInputForm
|
||||
initialValue={selectedPasskey?.friendlyName}
|
||||
callback={onSubmit}
|
||||
placeholder={t('ENTER_PASSKEY_NAME')}
|
||||
buttonText={t('RENAME')}
|
||||
fieldType="text"
|
||||
secondaryButtonAction={props.onClose}
|
||||
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
|
||||
/>
|
||||
</DialogBoxV2>
|
||||
);
|
||||
};
|
||||
|
||||
export default RenamePasskeyModal;
|
6
apps/accounts/src/pages/passkeys/finish.tsx
Normal file
6
apps/accounts/src/pages/passkeys/finish.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import PasskeysFinishPage from '@ente/accounts/pages/passkeys/finish';
|
||||
const PasskeysFinish = () => {
|
||||
return <PasskeysFinishPage />;
|
||||
};
|
||||
|
||||
export default PasskeysFinish;
|
275
apps/accounts/src/pages/passkeys/flow/index.tsx
Normal file
275
apps/accounts/src/pages/passkeys/flow/index.tsx
Normal file
|
@ -0,0 +1,275 @@
|
|||
import { APPS, CLIENT_PACKAGE_NAMES } from '@ente/shared/apps/constants';
|
||||
import {
|
||||
CenteredFlex,
|
||||
VerticallyCentered,
|
||||
} from '@ente/shared/components/Container';
|
||||
import EnteButton from '@ente/shared/components/EnteButton';
|
||||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
import FormPaper from '@ente/shared/components/Form/FormPaper';
|
||||
import HTTPService from '@ente/shared/network/HTTPService';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import { LS_KEYS, setData } from '@ente/shared/storage/localStorage';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { t } from 'i18next';
|
||||
import _sodium from 'libsodium-wrappers';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
BeginPasskeyAuthenticationResponse,
|
||||
beginPasskeyAuthentication,
|
||||
finishPasskeyAuthentication,
|
||||
} from 'services/passkeysService';
|
||||
|
||||
const PasskeysFlow = () => {
|
||||
const [errored, setErrored] = useState(false);
|
||||
|
||||
const [invalidInfo, setInvalidInfo] = useState(false);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const init = async () => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// get redirect from the query params
|
||||
const redirect = searchParams.get('redirect') as string;
|
||||
|
||||
const redirectURL = new URL(redirect);
|
||||
if (process.env.NEXT_PUBLIC_DISABLE_REDIRECT_CHECK !== 'true') {
|
||||
if (
|
||||
redirect !== '' &&
|
||||
!redirectURL.host.endsWith('.ente.io') &&
|
||||
redirectURL.protocol !== 'ente:' &&
|
||||
redirectURL.protocol !== 'enteauth:'
|
||||
) {
|
||||
setInvalidInfo(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let pkg = CLIENT_PACKAGE_NAMES.get(APPS.PHOTOS);
|
||||
if (redirectURL.protocol === 'enteauth:') {
|
||||
pkg = CLIENT_PACKAGE_NAMES.get(APPS.AUTH);
|
||||
} else if (redirectURL.hostname.startsWith('accounts')) {
|
||||
pkg = CLIENT_PACKAGE_NAMES.get(APPS.ACCOUNTS);
|
||||
}
|
||||
|
||||
setData(LS_KEYS.CLIENT_PACKAGE, { name: pkg });
|
||||
HTTPService.setHeaders({
|
||||
'X-Client-Package': pkg,
|
||||
});
|
||||
|
||||
// get passkeySessionID from the query params
|
||||
const passkeySessionID = searchParams.get('passkeySessionID') as string;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
let beginData: BeginPasskeyAuthenticationResponse;
|
||||
|
||||
try {
|
||||
beginData = await beginAuthentication(passkeySessionID);
|
||||
} catch (e) {
|
||||
logError(e, "Couldn't begin passkey authentication");
|
||||
setErrored(true);
|
||||
return;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
let credential: Credential | null = null;
|
||||
|
||||
let tries = 0;
|
||||
const maxTries = 3;
|
||||
|
||||
while (tries < maxTries) {
|
||||
try {
|
||||
credential = await getCredential(beginData.options.publicKey);
|
||||
} catch (e) {
|
||||
logError(e, "Couldn't get credential");
|
||||
continue;
|
||||
} finally {
|
||||
tries++;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!credential) {
|
||||
setErrored(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
let finishData;
|
||||
|
||||
try {
|
||||
finishData = await finishAuthentication(
|
||||
credential,
|
||||
passkeySessionID,
|
||||
beginData.ceremonySessionID
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, "Couldn't finish passkey authentication");
|
||||
setErrored(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const encodedResponse = _sodium.to_base64(JSON.stringify(finishData));
|
||||
|
||||
window.location.href = `${redirect}?response=${encodedResponse}`;
|
||||
};
|
||||
|
||||
const beginAuthentication = async (sessionId: string) => {
|
||||
const data = await beginPasskeyAuthentication(sessionId);
|
||||
return data;
|
||||
};
|
||||
|
||||
const getCredential = async (
|
||||
publicKey: any,
|
||||
timeoutMillis: number = 60000 // Default timeout of 60 seconds
|
||||
): Promise<Credential | null> => {
|
||||
publicKey.challenge = _sodium.from_base64(
|
||||
publicKey.challenge,
|
||||
_sodium.base64_variants.URLSAFE_NO_PADDING
|
||||
);
|
||||
publicKey.allowCredentials?.forEach(function (listItem: any) {
|
||||
listItem.id = _sodium.from_base64(
|
||||
listItem.id,
|
||||
_sodium.base64_variants.URLSAFE_NO_PADDING
|
||||
);
|
||||
});
|
||||
publicKey.timeout = timeoutMillis;
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey,
|
||||
});
|
||||
|
||||
return credential;
|
||||
};
|
||||
|
||||
const finishAuthentication = async (
|
||||
credential: Credential,
|
||||
sessionId: string,
|
||||
ceremonySessionId: string
|
||||
) => {
|
||||
const data = await finishPasskeyAuthentication(
|
||||
credential,
|
||||
sessionId,
|
||||
ceremonySessionId
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<VerticallyCentered>
|
||||
<EnteSpinner />
|
||||
</VerticallyCentered>
|
||||
);
|
||||
}
|
||||
|
||||
if (invalidInfo) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height="100%">
|
||||
<Box maxWidth="30rem">
|
||||
<FormPaper
|
||||
style={{
|
||||
padding: '1rem',
|
||||
}}>
|
||||
<InfoIcon />
|
||||
<Typography fontWeight="bold" variant="h1">
|
||||
{t('PASSKEY_LOGIN_FAILED')}
|
||||
</Typography>
|
||||
<Typography marginTop="1rem">
|
||||
{t('PASSKEY_LOGIN_URL_INVALID')}
|
||||
</Typography>
|
||||
</FormPaper>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (errored) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height="100%">
|
||||
<Box maxWidth="30rem">
|
||||
<FormPaper
|
||||
style={{
|
||||
padding: '1rem',
|
||||
}}>
|
||||
<InfoIcon />
|
||||
<Typography fontWeight="bold" variant="h1">
|
||||
{t('PASSKEY_LOGIN_FAILED')}
|
||||
</Typography>
|
||||
<Typography marginTop="1rem">
|
||||
{t('PASSKEY_LOGIN_ERRORED')}
|
||||
</Typography>
|
||||
<EnteButton
|
||||
onClick={() => {
|
||||
setErrored(false);
|
||||
init();
|
||||
}}
|
||||
fullWidth
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
}}
|
||||
color="primary"
|
||||
type="button"
|
||||
variant="contained">
|
||||
{t('TRY_AGAIN')}
|
||||
</EnteButton>
|
||||
</FormPaper>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height="100%">
|
||||
<Box maxWidth="30rem">
|
||||
<FormPaper
|
||||
style={{
|
||||
padding: '1rem',
|
||||
}}>
|
||||
<InfoIcon />
|
||||
<Typography fontWeight="bold" variant="h1">
|
||||
{t('LOGIN_WITH_PASSKEY')}
|
||||
</Typography>
|
||||
<Typography marginTop="1rem">
|
||||
{t('PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER')}
|
||||
</Typography>
|
||||
<CenteredFlex marginTop="1rem">
|
||||
<Image
|
||||
alt="ente Logo Circular"
|
||||
height={150}
|
||||
width={150}
|
||||
src="/images/ente-circular.png"
|
||||
/>
|
||||
</CenteredFlex>
|
||||
</FormPaper>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeysFlow;
|
167
apps/accounts/src/pages/passkeys/index.tsx
Normal file
167
apps/accounts/src/pages/passkeys/index.tsx
Normal file
|
@ -0,0 +1,167 @@
|
|||
import { CenteredFlex } from '@ente/shared/components/Container';
|
||||
import FormPaper from '@ente/shared/components/Form/FormPaper';
|
||||
import SingleInputForm from '@ente/shared/components/SingleInputForm';
|
||||
import { ACCOUNTS_PAGES } from '@ente/shared/constants/pages';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import { getToken } from '@ente/shared/storage/localStorage/helpers';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { t } from 'i18next';
|
||||
import _sodium from 'libsodium-wrappers';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Passkey } from 'types/passkey';
|
||||
import {
|
||||
finishPasskeyRegistration,
|
||||
getPasskeyRegistrationOptions,
|
||||
getPasskeys,
|
||||
} from '../../services/passkeysService';
|
||||
import ManagePasskeyDrawer from './ManagePasskeyDrawer';
|
||||
import PasskeysList from './PasskeysList';
|
||||
|
||||
export const PasskeysContext = createContext(
|
||||
{} as {
|
||||
selectedPasskey: Passkey | null;
|
||||
setSelectedPasskey: Dispatch<SetStateAction<Passkey | null>>;
|
||||
setShowPasskeyDrawer: Dispatch<SetStateAction<boolean>>;
|
||||
refreshPasskeys: () => void;
|
||||
}
|
||||
);
|
||||
|
||||
const Passkeys = () => {
|
||||
const { showNavBar } = useContext(AppContext);
|
||||
|
||||
const [selectedPasskey, setSelectedPasskey] = useState<Passkey | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [showPasskeyDrawer, setShowPasskeyDrawer] = useState(false);
|
||||
|
||||
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const checkLoggedIn = () => {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
router.push(ACCOUNTS_PAGES.LOGIN);
|
||||
}
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
checkLoggedIn();
|
||||
const data = await getPasskeys();
|
||||
setPasskeys(data.passkeys || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
showNavBar(true);
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (
|
||||
inputValue: string,
|
||||
setFieldError: (errorMessage: string) => void
|
||||
) => {
|
||||
let response: {
|
||||
options: {
|
||||
publicKey: PublicKeyCredentialCreationOptions;
|
||||
};
|
||||
sessionID: string;
|
||||
};
|
||||
|
||||
try {
|
||||
response = await getPasskeyRegistrationOptions();
|
||||
} catch {
|
||||
setFieldError('Failed to begin registration');
|
||||
return;
|
||||
}
|
||||
|
||||
const options = response.options;
|
||||
|
||||
options.publicKey.challenge = _sodium.from_base64(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
options.publicKey.challenge
|
||||
);
|
||||
options.publicKey.user.id = _sodium.from_base64(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
options.publicKey.user.id
|
||||
);
|
||||
|
||||
// create new credential
|
||||
let newCredential: Credential | null = null;
|
||||
|
||||
try {
|
||||
newCredential = await navigator.credentials.create(options);
|
||||
} catch (e) {
|
||||
logError(e, 'Error creating credential');
|
||||
setFieldError('Failed to create credential');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await finishPasskeyRegistration(
|
||||
inputValue,
|
||||
newCredential,
|
||||
response.sessionID
|
||||
);
|
||||
} catch {
|
||||
setFieldError('Failed to finish registration');
|
||||
return;
|
||||
}
|
||||
|
||||
init();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PasskeysContext.Provider
|
||||
value={{
|
||||
selectedPasskey,
|
||||
setSelectedPasskey,
|
||||
setShowPasskeyDrawer,
|
||||
refreshPasskeys: init,
|
||||
}}>
|
||||
<CenteredFlex>
|
||||
<Box maxWidth="20rem">
|
||||
<Box marginBottom="1rem">
|
||||
<Typography>{t('PASSKEYS_DESCRIPTION')}</Typography>
|
||||
</Box>
|
||||
<FormPaper
|
||||
style={{
|
||||
padding: '1rem',
|
||||
}}>
|
||||
<SingleInputForm
|
||||
fieldType="text"
|
||||
placeholder={t('ENTER_PASSKEY_NAME')}
|
||||
buttonText={t('ADD_PASSKEY')}
|
||||
initialValue={''}
|
||||
callback={handleSubmit}
|
||||
submitButtonProps={{
|
||||
sx: {
|
||||
marginBottom: 1,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormPaper>
|
||||
<Box marginTop="1rem">
|
||||
<PasskeysList passkeys={passkeys} />
|
||||
</Box>
|
||||
</Box>
|
||||
</CenteredFlex>
|
||||
<ManagePasskeyDrawer open={showPasskeyDrawer} />
|
||||
</PasskeysContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Passkeys;
|
17
apps/accounts/src/pages/recover/index.tsx
Normal file
17
apps/accounts/src/pages/recover/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import RecoverPage from '@ente/accounts/pages/recover';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { useContext } from 'react';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
|
||||
export default function Recover() {
|
||||
const appContext = useContext(AppContext);
|
||||
const router = useRouter();
|
||||
return (
|
||||
<RecoverPage
|
||||
appContext={appContext}
|
||||
router={router}
|
||||
appName={APPS.ACCOUNTS}
|
||||
/>
|
||||
);
|
||||
}
|
17
apps/accounts/src/pages/signup/index.tsx
Normal file
17
apps/accounts/src/pages/signup/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import SignupPage from '@ente/accounts/pages/signup';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export default function Sigup() {
|
||||
const appContext = useContext(AppContext);
|
||||
const router = useRouter();
|
||||
return (
|
||||
<SignupPage
|
||||
appContext={appContext}
|
||||
router={router}
|
||||
appName={APPS.ACCOUNTS}
|
||||
/>
|
||||
);
|
||||
}
|
17
apps/accounts/src/pages/two-factor/recover/index.tsx
Normal file
17
apps/accounts/src/pages/two-factor/recover/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import TwoFactorRecoverPage from '@ente/accounts/pages/two-factor/recover';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { useContext } from 'react';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
|
||||
export default function TwoFactorRecover() {
|
||||
const appContext = useContext(AppContext);
|
||||
const router = useRouter();
|
||||
return (
|
||||
<TwoFactorRecoverPage
|
||||
appContext={appContext}
|
||||
router={router}
|
||||
appName={APPS.ACCOUNTS}
|
||||
/>
|
||||
);
|
||||
}
|
17
apps/accounts/src/pages/two-factor/setup/index.tsx
Normal file
17
apps/accounts/src/pages/two-factor/setup/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import TwoFactorSetupPage from '@ente/accounts/pages/two-factor/setup';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { useContext } from 'react';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
|
||||
export default function TwoFactorSetup() {
|
||||
const appContext = useContext(AppContext);
|
||||
const router = useRouter();
|
||||
return (
|
||||
<TwoFactorSetupPage
|
||||
appContext={appContext}
|
||||
router={router}
|
||||
appName={APPS.ACCOUNTS}
|
||||
/>
|
||||
);
|
||||
}
|
17
apps/accounts/src/pages/two-factor/verify/index.tsx
Normal file
17
apps/accounts/src/pages/two-factor/verify/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import TwoFactorVerifyPage from '@ente/accounts/pages/two-factor/verify';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { useContext } from 'react';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
|
||||
export default function TwoFactorVerify() {
|
||||
const appContext = useContext(AppContext);
|
||||
const router = useRouter();
|
||||
return (
|
||||
<TwoFactorVerifyPage
|
||||
appContext={appContext}
|
||||
router={router}
|
||||
appName={APPS.ACCOUNTS}
|
||||
/>
|
||||
);
|
||||
}
|
17
apps/accounts/src/pages/verify/index.tsx
Normal file
17
apps/accounts/src/pages/verify/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import VerifyPage from '@ente/accounts/pages/verify';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { useContext } from 'react';
|
||||
import { APPS } from '@ente/shared/apps/constants';
|
||||
|
||||
export default function Verify() {
|
||||
const appContext = useContext(AppContext);
|
||||
const router = useRouter();
|
||||
return (
|
||||
<VerifyPage
|
||||
appContext={appContext}
|
||||
router={router}
|
||||
appName={APPS.ACCOUNTS}
|
||||
/>
|
||||
);
|
||||
}
|
200
apps/accounts/src/services/passkeysService.ts
Normal file
200
apps/accounts/src/services/passkeysService.ts
Normal file
|
@ -0,0 +1,200 @@
|
|||
import HTTPService from '@ente/shared/network/HTTPService';
|
||||
import { getEndpoint } from '@ente/shared/network/api';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import { getToken } from '@ente/shared/storage/localStorage/helpers';
|
||||
import _sodium from 'libsodium-wrappers';
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
export const getPasskeys = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
const response = await HTTPService.get(
|
||||
`${ENDPOINT}/passkeys`,
|
||||
{},
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
return await response.data;
|
||||
} catch (e) {
|
||||
logError(e, 'get passkeys failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const renamePasskey = async (id: string, name: string) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
const response = await HTTPService.patch(
|
||||
`${ENDPOINT}/passkeys/${id}`,
|
||||
{},
|
||||
{ friendlyName: name },
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
return await response.data;
|
||||
} catch (e) {
|
||||
logError(e, 'rename passkey failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePasskey = async (id: string) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
const response = await HTTPService.delete(
|
||||
`${ENDPOINT}/passkeys/${id}`,
|
||||
{},
|
||||
{},
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
return await response.data;
|
||||
} catch (e) {
|
||||
logError(e, 'delete passkey failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPasskeyRegistrationOptions = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
const response = await HTTPService.get(
|
||||
`${ENDPOINT}/passkeys/registration/begin`,
|
||||
{},
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return await response.data;
|
||||
} catch (e) {
|
||||
logError(e, 'get passkey registration options failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const finishPasskeyRegistration = async (
|
||||
friendlyName: string,
|
||||
credential: Credential,
|
||||
sessionId: string
|
||||
) => {
|
||||
try {
|
||||
const attestationObjectB64 = _sodium.to_base64(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
new Uint8Array(credential.response.attestationObject),
|
||||
_sodium.base64_variants.URLSAFE_NO_PADDING
|
||||
);
|
||||
const clientDataJSONB64 = _sodium.to_base64(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
new Uint8Array(credential.response.clientDataJSON),
|
||||
_sodium.base64_variants.URLSAFE_NO_PADDING
|
||||
);
|
||||
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
|
||||
const response = await HTTPService.post(
|
||||
`${ENDPOINT}/passkeys/registration/finish`,
|
||||
JSON.stringify({
|
||||
id: credential.id,
|
||||
rawId: credential.id,
|
||||
type: credential.type,
|
||||
response: {
|
||||
attestationObject: attestationObjectB64,
|
||||
clientDataJSON: clientDataJSONB64,
|
||||
},
|
||||
}),
|
||||
{
|
||||
friendlyName,
|
||||
sessionID: sessionId,
|
||||
},
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return await response.data;
|
||||
} catch (e) {
|
||||
logError(e, 'finish passkey registration failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export interface BeginPasskeyAuthenticationResponse {
|
||||
ceremonySessionID: string;
|
||||
options: Options;
|
||||
}
|
||||
interface Options {
|
||||
publicKey: PublicKeyCredentialRequestOptions;
|
||||
}
|
||||
|
||||
export const beginPasskeyAuthentication = async (
|
||||
sessionId: string
|
||||
): Promise<BeginPasskeyAuthenticationResponse> => {
|
||||
try {
|
||||
const data = await HTTPService.post(
|
||||
`${ENDPOINT}/users/two-factor/passkeys/begin`,
|
||||
{
|
||||
sessionID: sessionId,
|
||||
}
|
||||
);
|
||||
|
||||
return data.data;
|
||||
} catch (e) {
|
||||
logError(e, 'begin passkey authentication failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const finishPasskeyAuthentication = async (
|
||||
credential: Credential,
|
||||
sessionId: string,
|
||||
ceremonySessionId: string
|
||||
) => {
|
||||
try {
|
||||
const data = await HTTPService.post(
|
||||
`${ENDPOINT}/users/two-factor/passkeys/finish`,
|
||||
{
|
||||
id: credential.id,
|
||||
rawId: credential.id,
|
||||
type: credential.type,
|
||||
response: {
|
||||
authenticatorData: _sodium.to_base64(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
new Uint8Array(credential.response.authenticatorData),
|
||||
_sodium.base64_variants.URLSAFE_NO_PADDING
|
||||
),
|
||||
clientDataJSON: _sodium.to_base64(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
new Uint8Array(credential.response.clientDataJSON),
|
||||
_sodium.base64_variants.URLSAFE_NO_PADDING
|
||||
),
|
||||
signature: _sodium.to_base64(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
new Uint8Array(credential.response.signature),
|
||||
_sodium.base64_variants.URLSAFE_NO_PADDING
|
||||
),
|
||||
userHandle: _sodium.to_base64(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
new Uint8Array(credential.response.userHandle),
|
||||
_sodium.base64_variants.URLSAFE_NO_PADDING
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
sessionID: sessionId,
|
||||
ceremonySessionID: ceremonySessionId,
|
||||
}
|
||||
);
|
||||
|
||||
return data.data;
|
||||
} catch (e) {
|
||||
logError(e, 'finish passkey authentication failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
194
apps/accounts/src/styles/global.css
Normal file
194
apps/accounts/src/styles/global.css
Normal file
|
@ -0,0 +1,194 @@
|
|||
/* inter-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(''), url('/fonts/inter-v11-latin-500.woff2') format('woff2'),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/fonts/inter-v11-latin-500.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
font-display: swap; /*https://web.dev/font-display/?utm_source=lighthouse&utm_medium=devtools#how-to-avoid-showing-invisible-text*/
|
||||
}
|
||||
|
||||
/* inter-600 - latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local(''), url('/fonts/inter-v11-latin-600.woff2') format('woff2'),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/fonts/inter-v11-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* inter-800 - latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: local(''), url('/fonts/inter-v11-latin-800.woff2') format('woff2'),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/fonts/inter-v11-latin-800.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#__next {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.pswp__button--custom {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: none !important;
|
||||
background-image: none !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.pswp__item video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pswp-item-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.pswp-item-container > * {
|
||||
position: absolute;
|
||||
transition: opacity 1s ease;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.pswp-item-container > img {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pswp-item-container > video {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pswp-item-container > div.download-banner {
|
||||
width: 100%;
|
||||
height: 16vh;
|
||||
padding: 2vh 0;
|
||||
background-color: #151414;
|
||||
color: #ddd;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
opacity: 0.8;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.download-banner > a {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.pswp__img {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.pswp__button--arrow--left,
|
||||
.pswp__button--arrow--right {
|
||||
color: #fff;
|
||||
background-color: #333333 !important;
|
||||
border-radius: 50%;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
.pswp__button--arrow--left::before,
|
||||
.pswp__button--arrow--right::before {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.pswp__button--arrow--left {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.pswp__button--arrow--right {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.pswp-custom-caption-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
bottom: 56px;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.pswp__caption--empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bg-upload-progress-bar {
|
||||
background-color: #51cd7c;
|
||||
}
|
||||
|
||||
.carousel-inner {
|
||||
padding-bottom: 50px !important;
|
||||
}
|
||||
|
||||
.carousel-indicators li {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.carousel-indicators .active {
|
||||
background-color: #51cd7c;
|
||||
}
|
||||
|
||||
div.otp-input input {
|
||||
width: 36px !important;
|
||||
height: 36px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
div.otp-input input::placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
div.otp-input input:not(:placeholder-shown),
|
||||
div.otp-input input:focus {
|
||||
border: 2px solid #51cd7c;
|
||||
border-radius: 1px;
|
||||
-webkit-transition: 0.5s;
|
||||
transition: 0.5s;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.flash-message {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotation {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(359deg);
|
||||
}
|
||||
}
|
6
apps/accounts/src/types/passkey.ts
Normal file
6
apps/accounts/src/types/passkey.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export interface Passkey {
|
||||
id: string;
|
||||
userID: number;
|
||||
friendlyName: string;
|
||||
createdAt: number;
|
||||
}
|
25
apps/accounts/tsconfig.json
Normal file
25
apps/accounts/tsconfig.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"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",
|
||||
"../../packages/shared/themes/mui-theme.d.ts",
|
||||
"../../packages/accounts/**/*.tsx"
|
||||
],
|
||||
"exclude": ["node_modules", "out", ".next", "thirdparty"]
|
||||
}
|
|
@ -615,6 +615,5 @@
|
|||
"COLORS": "Colors",
|
||||
"FLIP": "Flip",
|
||||
"ROTATION": "Rotation",
|
||||
"RESET": "Reset",
|
||||
"PHOTO_EDITOR": "Photo Editor"
|
||||
"RESET": "Reset"
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function Slideshow() {
|
|||
castCollection.updationTime !== collection.updationTime
|
||||
) {
|
||||
setCastCollection(collection);
|
||||
await syncPublicFiles(token, collection, () => { });
|
||||
await syncPublicFiles(token, collection, () => {});
|
||||
const files = await getLocalFiles(String(collection.id));
|
||||
setCollectionFiles(
|
||||
files.filter((file) => isFileEligibleForCast(file))
|
||||
|
|
|
@ -637,6 +637,7 @@
|
|||
"VISIT_CAST_ENTE_IO": "Visit cast.ente.io on the device you want to pair.",
|
||||
"CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.",
|
||||
"CACHE_DIRECTORY": "Cache folder",
|
||||
"PASSKEYS": "Passkeys",
|
||||
"FREEHAND": "Freehand",
|
||||
"APPLY_CROP": "Apply Crop",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving."
|
||||
|
|
7
apps/photos/src/components/EnteSpinner.tsx
Normal file
7
apps/photos/src/components/EnteSpinner.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import CircularProgress, {
|
||||
CircularProgressProps,
|
||||
} from '@mui/material/CircularProgress';
|
||||
|
||||
export default function EnteSpinner(props: CircularProgressProps) {
|
||||
return <CircularProgress color="accent" size={32} {...props} />;
|
||||
}
|
|
@ -1,22 +1,28 @@
|
|||
import { useContext, useState } from 'react';
|
||||
import { t } from 'i18next';
|
||||
import { useContext, useState } from 'react';
|
||||
|
||||
// import FixLargeThumbnails from 'components/FixLargeThumbnail';
|
||||
import RecoveryKey from '@ente/shared/components/RecoveryKey';
|
||||
import {
|
||||
ACCOUNTS_PAGES,
|
||||
PHOTOS_PAGES as PAGES,
|
||||
} from '@ente/shared/constants/pages';
|
||||
import TwoFactorModal from 'components/TwoFactor/Modal';
|
||||
import { PHOTOS_PAGES as PAGES } from '@ente/shared/constants/pages';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppContext } from 'pages/_app';
|
||||
// import mlIDbStorage from 'utils/storage/mlIDbStorage';
|
||||
import isElectron from 'is-electron';
|
||||
import { APPS, CLIENT_PACKAGE_NAMES } from '@ente/shared/apps/constants';
|
||||
import ThemeSwitcher from '@ente/shared/components/ThemeSwitcher';
|
||||
import { getAccountsURL } from '@ente/shared/network/api';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import { THEME_COLOR } from '@ente/shared/themes/constants';
|
||||
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
|
||||
import WatchFolder from 'components/WatchFolder';
|
||||
import isElectron from 'is-electron';
|
||||
import { getAccountsToken } from 'services/userService';
|
||||
import { getDownloadAppMessage } from 'utils/ui';
|
||||
|
||||
import { isInternalUser } from 'utils/user';
|
||||
import Preferences from './Preferences';
|
||||
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
|
||||
import ThemeSwitcher from '@ente/shared/components/ThemeSwitcher';
|
||||
import { THEME_COLOR } from '@ente/shared/themes/constants';
|
||||
|
||||
export default function UtilitySection({ closeSidebar }) {
|
||||
const router = useRouter();
|
||||
|
@ -62,6 +68,21 @@ export default function UtilitySection({ closeSidebar }) {
|
|||
router.push(PAGES.CHANGE_EMAIL);
|
||||
};
|
||||
|
||||
const redirectToAccountsPage = async () => {
|
||||
closeSidebar();
|
||||
|
||||
try {
|
||||
const accountsToken = await getAccountsToken();
|
||||
|
||||
window.location.href = `${getAccountsURL()}${ACCOUNTS_PAGES.ACCOUNT_HANDOFF
|
||||
}?package=${CLIENT_PACKAGE_NAMES.get(
|
||||
APPS.PHOTOS
|
||||
)}&token=${accountsToken}`;
|
||||
} catch (e) {
|
||||
logError(e, 'failed to redirect to accounts page');
|
||||
}
|
||||
};
|
||||
|
||||
const redirectToDeduplicatePage = () => router.push(PAGES.DEDUPLICATE);
|
||||
|
||||
const somethingWentWrong = () =>
|
||||
|
@ -112,6 +133,12 @@ export default function UtilitySection({ closeSidebar }) {
|
|||
label={t('TWO_FACTOR')}
|
||||
/>
|
||||
|
||||
<EnteMenuItem
|
||||
variant="secondary"
|
||||
onClick={redirectToAccountsPage}
|
||||
label={t('PASSKEYS')}
|
||||
/>
|
||||
|
||||
<EnteMenuItem
|
||||
variant="secondary"
|
||||
onClick={redirectToChangePasswordPage}
|
||||
|
|
11
apps/photos/src/pages/passkeys/finish/index.tsx
Normal file
11
apps/photos/src/pages/passkeys/finish/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import PasskeysFinishPage from '@ente/accounts/pages/passkeys/finish';
|
||||
|
||||
const PasskeysFinish = () => {
|
||||
return (
|
||||
<>
|
||||
<PasskeysFinishPage />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeysFinish;
|
|
@ -1,22 +1,24 @@
|
|||
import { getEndpoint, getFamilyPortalURL } from '@ente/shared/network/api';
|
||||
import { getData, LS_KEYS } from '@ente/shared/storage/localStorage';
|
||||
import localForage from '@ente/shared/storage/localForage';
|
||||
import { getToken } from '@ente/shared/storage/localStorage/helpers';
|
||||
import HTTPService from '@ente/shared/network/HTTPService';
|
||||
import { getRecoveryKey } from '@ente/shared/crypto/helpers';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import {
|
||||
UserDetails,
|
||||
DeleteChallengeResponse,
|
||||
GetRemoteStoreValueResponse,
|
||||
GetFeatureFlagResponse,
|
||||
} from 'types/user';
|
||||
import { ApiError } from '@ente/shared/error';
|
||||
import { getLocalFamilyData, isPartOfFamily } from 'utils/user/family';
|
||||
import { AxiosResponse, HttpStatusCode } from 'axios';
|
||||
import { setLocalMapEnabled } from '@ente/shared/storage/localStorage/helpers';
|
||||
import { putAttributes } from '@ente/accounts/api/user';
|
||||
import { logoutUser } from '@ente/accounts/services/user';
|
||||
import { getRecoveryKey } from '@ente/shared/crypto/helpers';
|
||||
import { ApiError } from '@ente/shared/error';
|
||||
import HTTPService from '@ente/shared/network/HTTPService';
|
||||
import { getEndpoint, getFamilyPortalURL } from '@ente/shared/network/api';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import localForage from '@ente/shared/storage/localForage';
|
||||
import { getData, LS_KEYS } from '@ente/shared/storage/localStorage';
|
||||
import {
|
||||
getToken,
|
||||
setLocalMapEnabled,
|
||||
} from '@ente/shared/storage/localStorage/helpers';
|
||||
import { AxiosResponse, HttpStatusCode } from 'axios';
|
||||
import {
|
||||
DeleteChallengeResponse,
|
||||
GetFeatureFlagResponse,
|
||||
GetRemoteStoreValueResponse,
|
||||
UserDetails,
|
||||
} from 'types/user';
|
||||
import { getLocalFamilyData, isPartOfFamily } from 'utils/user/family';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
|
@ -66,6 +68,24 @@ export const getFamiliesToken = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getAccountsToken = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/accounts-token`,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return resp.data['accountsToken'];
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get accounts token');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getRoadmapRedirectURL = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
|
|
@ -10,12 +10,15 @@
|
|||
"build:photos": "turbo run build --filter=photos",
|
||||
"build:auth": "turbo run build --filter=auth",
|
||||
"build:cast": "turbo run build --filter=cast",
|
||||
"build:accounts": "turbo run build --filter=accounts",
|
||||
"dev:auth": "turbo run dev --filter=auth",
|
||||
"dev:photos": "turbo run dev --filter=photos",
|
||||
"dev:accounts": "turbo run dev --filter=accounts",
|
||||
"dev:cast": "turbo run dev --filter=cast",
|
||||
"export:photos": "turbo run export --filter=photos",
|
||||
"export:auth": "turbo run export --filter=auth",
|
||||
"export:cast": "turbo run export --filter=cast",
|
||||
"export:accounts": "turbo run export --filter=accounts",
|
||||
"lint": "turbo run lint",
|
||||
"albums": "turbo run albums",
|
||||
"prepare": "husky install"
|
||||
|
|
|
@ -2,57 +2,57 @@ import { useEffect, useState } from 'react';
|
|||
|
||||
import { t } from 'i18next';
|
||||
|
||||
import {
|
||||
clearData,
|
||||
getData,
|
||||
LS_KEYS,
|
||||
setData,
|
||||
} from '@ente/shared/storage/localStorage';
|
||||
import { PAGES } from '../constants/pages';
|
||||
import {
|
||||
SESSION_KEYS,
|
||||
getKey,
|
||||
removeKey,
|
||||
setKey,
|
||||
} from '@ente/shared/storage/sessionStorage';
|
||||
import {
|
||||
decryptAndStoreToken,
|
||||
generateAndSaveIntermediateKeyAttributes,
|
||||
generateLoginSubKey,
|
||||
saveKeyInSessionStore,
|
||||
} from '@ente/shared/crypto/helpers';
|
||||
import {
|
||||
clearData,
|
||||
getData,
|
||||
LS_KEYS,
|
||||
setData,
|
||||
} from '@ente/shared/storage/localStorage';
|
||||
import {
|
||||
getKey,
|
||||
removeKey,
|
||||
SESSION_KEYS,
|
||||
setKey,
|
||||
} from '@ente/shared/storage/sessionStorage';
|
||||
import { PAGES } from '../constants/pages';
|
||||
import { generateSRPSetupAttributes } from '../services/srp';
|
||||
import { logoutUser } from '../services/user';
|
||||
|
||||
import { configureSRP, loginViaSRP } from '../services/srp';
|
||||
import { getSRPAttributes } from '../api/srp';
|
||||
import { SRPAttributes } from '../types/srp';
|
||||
|
||||
import { VerticallyCentered } from '@ente/shared/components/Container';
|
||||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
import FormPaper from '@ente/shared/components/Form/FormPaper';
|
||||
import FormPaperFooter from '@ente/shared/components/Form/FormPaper/Footer';
|
||||
import FormPaperTitle from '@ente/shared/components/Form/FormPaper/Title';
|
||||
import LinkButton from '@ente/shared/components/LinkButton';
|
||||
import VerifyMasterPasswordForm, {
|
||||
VerifyMasterPasswordFormProps,
|
||||
} from '@ente/shared/components/VerifyMasterPasswordForm';
|
||||
import { getAccountsURL } from '@ente/shared/network/api';
|
||||
import {
|
||||
isFirstLogin,
|
||||
setIsFirstLogin,
|
||||
} from '@ente/shared/storage/localStorage/helpers';
|
||||
import { KeyAttributes, User } from '@ente/shared/user/types';
|
||||
import FormPaper from '@ente/shared/components/Form/FormPaper';
|
||||
import FormPaperTitle from '@ente/shared/components/Form/FormPaper/Title';
|
||||
import FormPaperFooter from '@ente/shared/components/Form/FormPaper/Footer';
|
||||
import LinkButton from '@ente/shared/components/LinkButton';
|
||||
import isElectron from 'is-electron';
|
||||
import { VerticallyCentered } from '@ente/shared/components/Container';
|
||||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
import VerifyMasterPasswordForm, {
|
||||
VerifyMasterPasswordFormProps,
|
||||
} from '@ente/shared/components/VerifyMasterPasswordForm';
|
||||
import { getSRPAttributes } from '../api/srp';
|
||||
import { configureSRP, loginViaSRP } from '../services/srp';
|
||||
import { SRPAttributes } from '../types/srp';
|
||||
// import { APPS, getAppName } from '@ente/shared/apps';
|
||||
import { addLocalLog } from '@ente/shared/logging';
|
||||
import { APP_HOMES } from '@ente/shared/apps/constants';
|
||||
import { PageProps } from '@ente/shared/apps/types';
|
||||
import ComlinkCryptoWorker from '@ente/shared/crypto';
|
||||
import { B64EncryptionResult } from '@ente/shared/crypto/types';
|
||||
import { CustomError } from '@ente/shared/error';
|
||||
import InMemoryStore, { MS_KEYS } from '@ente/shared/storage/InMemoryStore';
|
||||
import { PageProps } from '@ente/shared/apps/types';
|
||||
import { APP_HOMES } from '@ente/shared/apps/constants';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import ElectronAPIs from '@ente/shared/electron';
|
||||
import { CustomError } from '@ente/shared/error';
|
||||
import { addLocalLog } from '@ente/shared/logging';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import InMemoryStore, { MS_KEYS } from '@ente/shared/storage/InMemoryStore';
|
||||
|
||||
export default function Credentials({
|
||||
appContext,
|
||||
|
@ -148,9 +148,28 @@ export default function Credentials({
|
|||
token,
|
||||
id,
|
||||
twoFactorSessionID,
|
||||
passkeySessionID,
|
||||
} = await loginViaSRP(srpAttributes, kek);
|
||||
setIsFirstLogin(true);
|
||||
if (twoFactorSessionID) {
|
||||
if (passkeySessionID) {
|
||||
const sessionKeyAttributes =
|
||||
await cryptoWorker.generateKeyAndEncryptToB64(kek);
|
||||
setKey(
|
||||
SESSION_KEYS.KEY_ENCRYPTION_KEY,
|
||||
sessionKeyAttributes
|
||||
);
|
||||
const user = getData(LS_KEYS.USER);
|
||||
setData(LS_KEYS.USER, {
|
||||
...user,
|
||||
passkeySessionID,
|
||||
isTwoFactorEnabled: true,
|
||||
isTwoFactorPasskeysEnabled: true,
|
||||
});
|
||||
InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.ROOT);
|
||||
window.location.href = `${getAccountsURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${window.location.origin
|
||||
}/passkeys/finish`;
|
||||
return;
|
||||
} else if (twoFactorSessionID) {
|
||||
const sessionKeyAttributes =
|
||||
await cryptoWorker.generateKeyAndEncryptToB64(kek);
|
||||
setKey(
|
||||
|
|
46
packages/accounts/pages/passkeys/finish.tsx
Normal file
46
packages/accounts/pages/passkeys/finish.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { PAGES } from '@ente/accounts/constants/pages';
|
||||
import { VerticallyCentered } from '@ente/shared/components/Container';
|
||||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
import InMemoryStore, { MS_KEYS } from '@ente/shared/storage/InMemoryStore';
|
||||
import { LS_KEYS, getData, setData } from '@ente/shared/storage/localStorage';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const PasskeysFinishPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const init = async () => {
|
||||
// get response from query params
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const response = searchParams.get('response');
|
||||
|
||||
if (!response) return;
|
||||
|
||||
// decode response
|
||||
const decodedResponse = JSON.parse(atob(response));
|
||||
|
||||
const { keyAttributes, encryptedToken, token, id } = decodedResponse;
|
||||
setData(LS_KEYS.USER, {
|
||||
...getData(LS_KEYS.USER),
|
||||
token,
|
||||
encryptedToken,
|
||||
id,
|
||||
});
|
||||
setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
|
||||
const redirectURL = InMemoryStore.get(MS_KEYS.REDIRECT_URL);
|
||||
InMemoryStore.delete(MS_KEYS.REDIRECT_URL);
|
||||
router.push(redirectURL ?? PAGES.ROOT);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<VerticallyCentered>
|
||||
<EnteSpinner />
|
||||
</VerticallyCentered>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeysFinishPage;
|
|
@ -1,35 +1,36 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { t } from 'i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { LS_KEYS, getData, setData } from '@ente/shared/storage/localStorage';
|
||||
import { verifyOtt, sendOtt, putAttributes } from '../api/user';
|
||||
import { logoutUser } from '../services/user';
|
||||
import { configureSRP } from '../services/srp';
|
||||
import { UserVerificationResponse } from '@ente/accounts/types/user';
|
||||
import { PageProps } from '@ente/shared/apps/types';
|
||||
import { VerticallyCentered } from '@ente/shared/components/Container';
|
||||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
import FormPaper from '@ente/shared/components/Form/FormPaper';
|
||||
import FormPaperFooter from '@ente/shared/components/Form/FormPaper/Footer';
|
||||
import FormPaperTitle from '@ente/shared/components/Form/FormPaper/Title';
|
||||
import LinkButton from '@ente/shared/components/LinkButton';
|
||||
import SingleInputForm, {
|
||||
SingleInputFormProps,
|
||||
} from '@ente/shared/components/SingleInputForm';
|
||||
import { ApiError } from '@ente/shared/error';
|
||||
import { getAccountsURL } from '@ente/shared/network/api';
|
||||
import InMemoryStore, { MS_KEYS } from '@ente/shared/storage/InMemoryStore';
|
||||
import { clearFiles } from '@ente/shared/storage/localForage/helpers';
|
||||
import { LS_KEYS, getData, setData } from '@ente/shared/storage/localStorage';
|
||||
import {
|
||||
getLocalReferralSource,
|
||||
setIsFirstLogin,
|
||||
} from '@ente/shared/storage/localStorage/helpers';
|
||||
import { clearKeys } from '@ente/shared/storage/sessionStorage';
|
||||
import { PAGES } from '../constants/pages';
|
||||
import { KeyAttributes, User } from '@ente/shared/user/types';
|
||||
import { SRPSetupAttributes } from '../types/srp';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import FormPaperTitle from '@ente/shared/components/Form/FormPaper/Title';
|
||||
import FormPaper from '@ente/shared/components/Form/FormPaper';
|
||||
import FormPaperFooter from '@ente/shared/components/Form/FormPaper/Footer';
|
||||
import LinkButton from '@ente/shared/components/LinkButton';
|
||||
import SingleInputForm, {
|
||||
SingleInputFormProps,
|
||||
} from '@ente/shared/components/SingleInputForm';
|
||||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
import { VerticallyCentered } from '@ente/shared/components/Container';
|
||||
import InMemoryStore, { MS_KEYS } from '@ente/shared/storage/InMemoryStore';
|
||||
import { ApiError } from '@ente/shared/error';
|
||||
import { HttpStatusCode } from 'axios';
|
||||
import { PageProps } from '@ente/shared/apps/types';
|
||||
import { UserVerificationResponse } from '@ente/accounts/types/user';
|
||||
import { putAttributes, sendOtt, verifyOtt } from '../api/user';
|
||||
import { PAGES } from '../constants/pages';
|
||||
import { configureSRP } from '../services/srp';
|
||||
import { logoutUser } from '../services/user';
|
||||
import { SRPSetupAttributes } from '../types/srp';
|
||||
|
||||
export default function VerifyPage({ appContext, router, appName }: PageProps) {
|
||||
const [email, setEmail] = useState('');
|
||||
|
@ -69,8 +70,21 @@ export default function VerifyPage({ appContext, router, appName }: PageProps) {
|
|||
token,
|
||||
id,
|
||||
twoFactorSessionID,
|
||||
passkeySessionID,
|
||||
} = resp.data as UserVerificationResponse;
|
||||
if (twoFactorSessionID) {
|
||||
if (passkeySessionID) {
|
||||
const user = getData(LS_KEYS.USER);
|
||||
setData(LS_KEYS.USER, {
|
||||
...user,
|
||||
passkeySessionID,
|
||||
isTwoFactorEnabled: true,
|
||||
isTwoFactorPasskeysEnabled: true,
|
||||
});
|
||||
setIsFirstLogin(true);
|
||||
window.location.href = `${getAccountsURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${window.location.origin
|
||||
}/passkeys/finish`;
|
||||
router.push(PAGES.CREDENTIALS);
|
||||
} else if (twoFactorSessionID) {
|
||||
setData(LS_KEYS.USER, {
|
||||
email,
|
||||
twoFactorSessionID,
|
||||
|
|
|
@ -6,6 +6,7 @@ export interface UserVerificationResponse {
|
|||
encryptedToken?: string;
|
||||
token?: string;
|
||||
twoFactorSessionID: string;
|
||||
passkeySessionID: string;
|
||||
srpM2?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
import { AUTH_PAGES, PHOTOS_PAGES } from '../constants/pages';
|
||||
import { ACCOUNTS_PAGES, AUTH_PAGES, PHOTOS_PAGES } from '../constants/pages';
|
||||
|
||||
export enum APPS {
|
||||
PHOTOS = 'PHOTOS',
|
||||
AUTH = 'AUTH',
|
||||
ALBUMS = 'ALBUMS',
|
||||
ACCOUNTS = 'ACCOUNTS',
|
||||
}
|
||||
|
||||
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'],
|
||||
[APPS.ACCOUNTS, 'io.ente.accounts.web'],
|
||||
]);
|
||||
|
||||
export const APP_TITLES = new Map([
|
||||
[APPS.ALBUMS, 'Ente Albums'],
|
||||
[APPS.PHOTOS, 'Ente Photos'],
|
||||
[APPS.AUTH, 'Ente Auth'],
|
||||
[APPS.ACCOUNTS, 'Ente Accounts'],
|
||||
]);
|
||||
|
||||
export const APP_HOMES = new Map([
|
||||
[APPS.ALBUMS, '/'],
|
||||
[APPS.PHOTOS, PHOTOS_PAGES.GALLERY],
|
||||
[APPS.AUTH, AUTH_PAGES.AUTH],
|
||||
[APPS.ACCOUNTS, ACCOUNTS_PAGES.PASSKEYS],
|
||||
]);
|
||||
|
||||
export const OTT_CLIENTS = new Map([
|
||||
|
|
42
packages/shared/components/CaptionedText.tsx
Normal file
42
packages/shared/components/CaptionedText.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { ButtonProps, Typography } from '@mui/material';
|
||||
import { VerticallyCenteredFlex } from '@ente/shared/components/Container';
|
||||
|
||||
interface Iprops {
|
||||
mainText: string;
|
||||
subText?: string;
|
||||
subIcon?: React.ReactNode;
|
||||
color?: ButtonProps['color'];
|
||||
}
|
||||
|
||||
const getSubTextColor = (color: ButtonProps['color']) => {
|
||||
switch (color) {
|
||||
case 'critical':
|
||||
return 'critical.main';
|
||||
default:
|
||||
return 'text.faint';
|
||||
}
|
||||
};
|
||||
|
||||
export const CaptionedText = (props: Iprops) => {
|
||||
return (
|
||||
<VerticallyCenteredFlex gap={'4px'}>
|
||||
<Typography> {props.mainText}</Typography>
|
||||
<Typography variant="small" color={getSubTextColor(props.color)}>
|
||||
{'•'}
|
||||
</Typography>
|
||||
{props.subText ? (
|
||||
<Typography
|
||||
variant="small"
|
||||
color={getSubTextColor(props.color)}>
|
||||
{props.subText}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography
|
||||
variant="small"
|
||||
color={getSubTextColor(props.color)}>
|
||||
{props.subIcon}
|
||||
</Typography>
|
||||
)}
|
||||
</VerticallyCenteredFlex>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import { SwitchProps, Switch } from '@mui/material';
|
||||
import { styled } from '@mui/material';
|
||||
const PublicShareSwitch = styled((props: SwitchProps) => (
|
||||
<Switch
|
||||
focusVisibleClassName=".Mui-focusVisible"
|
||||
disableRipple
|
||||
{...props}
|
||||
/>
|
||||
))(({ theme }) => ({
|
||||
width: 40,
|
||||
height: 24,
|
||||
padding: 0,
|
||||
'& .MuiSwitch-switchBase': {
|
||||
padding: 0,
|
||||
margin: 2,
|
||||
transitionDuration: '300ms',
|
||||
'&.Mui-checked': {
|
||||
transform: 'translateX(16px)',
|
||||
color: '#fff',
|
||||
'& + .MuiSwitch-track': {
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark' ? '#2ECA45' : '#65C466',
|
||||
opacity: 1,
|
||||
border: 0,
|
||||
},
|
||||
'&.Mui-disabled + .MuiSwitch-track': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
'&.Mui-focusVisible .MuiSwitch-thumb': {
|
||||
color: '#33cf4d',
|
||||
border: '6px solid #fff',
|
||||
},
|
||||
'&.Mui-disabled .MuiSwitch-thumb': {
|
||||
color:
|
||||
theme.palette.mode === 'light'
|
||||
? theme.palette.grey[100]
|
||||
: theme.palette.grey[600],
|
||||
},
|
||||
'&.Mui-disabled + .MuiSwitch-track': {
|
||||
opacity: theme.palette.mode === 'light' ? 0.7 : 0.3,
|
||||
},
|
||||
},
|
||||
'& .MuiSwitch-thumb': {
|
||||
boxSizing: 'border-box',
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
'& .MuiSwitch-track': {
|
||||
borderRadius: 22 / 2,
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'light'
|
||||
? '#E9E9EA'
|
||||
: theme.colors.fill.muted,
|
||||
opacity: 1,
|
||||
transition: theme.transitions.create(['background-color'], {
|
||||
duration: 500,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
export default PublicShareSwitch;
|
26
packages/shared/components/Directory/changeOption.tsx
Normal file
26
packages/shared/components/Directory/changeOption.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import OverflowMenu from '@ente/shared/components/OverflowMenu/menu';
|
||||
import { OverflowMenuOption } from '@ente/shared/components/OverflowMenu/option';
|
||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||
import { t } from 'i18next';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
|
||||
export default function ChangeDirectoryOption({
|
||||
changeExportDirectory: changeDirectory,
|
||||
}) {
|
||||
return (
|
||||
<OverflowMenu
|
||||
triggerButtonProps={{
|
||||
sx: {
|
||||
ml: 1,
|
||||
},
|
||||
}}
|
||||
ariaControls={'export-option'}
|
||||
triggerButtonIcon={<MoreHoriz />}>
|
||||
<OverflowMenuOption
|
||||
onClick={changeDirectory}
|
||||
startIcon={<FolderIcon />}>
|
||||
{t('CHANGE_FOLDER')}
|
||||
</OverflowMenuOption>
|
||||
</OverflowMenu>
|
||||
);
|
||||
}
|
34
packages/shared/components/Directory/index.tsx
Normal file
34
packages/shared/components/Directory/index.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { styled } from '@mui/material/styles';
|
||||
import LinkButton from '@ente/shared/components/LinkButton';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import ElectronAPIs from '@ente/shared/electron';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
|
||||
const DirectoryPathContainer = styled(LinkButton)(
|
||||
({ width }) => `
|
||||
width: ${width}px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
/* Beginning of string */
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
`
|
||||
);
|
||||
|
||||
export const DirectoryPath = ({ width, path }) => {
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await ElectronAPIs.openDirectory(path);
|
||||
} catch (e) {
|
||||
logError(e, 'openDirectory failed');
|
||||
}
|
||||
};
|
||||
return (
|
||||
<DirectoryPathContainer width={width} onClick={handleClick}>
|
||||
<Tooltip title={path}>
|
||||
<span>{path}</span>
|
||||
</Tooltip>
|
||||
</DirectoryPathContainer>
|
||||
);
|
||||
};
|
10
packages/shared/components/EnteDrawer.tsx
Normal file
10
packages/shared/components/EnteDrawer.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Drawer, styled } from '@mui/material';
|
||||
|
||||
export const EnteDrawer = styled(Drawer)(({ theme }) => ({
|
||||
'& .MuiPaper-root': {
|
||||
maxWidth: '375px',
|
||||
width: '100%',
|
||||
scrollbarWidth: 'thin',
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}));
|
61
packages/shared/components/Info/InfoItem.tsx
Normal file
61
packages/shared/components/Info/InfoItem.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import Edit from '@mui/icons-material/Edit';
|
||||
import { Box, IconButton, Typography } from '@mui/material';
|
||||
import { FlexWrapper } from '@ente/shared/components/Container';
|
||||
import React from 'react';
|
||||
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
|
||||
|
||||
interface Iprops {
|
||||
icon: JSX.Element;
|
||||
title?: string;
|
||||
caption?: string | JSX.Element;
|
||||
openEditor?: any;
|
||||
loading?: boolean;
|
||||
hideEditOption?: any;
|
||||
customEndButton?: any;
|
||||
children?: any;
|
||||
}
|
||||
|
||||
export default function InfoItem({
|
||||
icon,
|
||||
title,
|
||||
caption,
|
||||
openEditor,
|
||||
loading,
|
||||
hideEditOption,
|
||||
customEndButton,
|
||||
children,
|
||||
}: Iprops): JSX.Element {
|
||||
return (
|
||||
<FlexWrapper justifyContent="space-between">
|
||||
<Box display={'flex'} alignItems="flex-start" gap={0.5} pr={1}>
|
||||
<IconButton
|
||||
color="secondary"
|
||||
sx={{ '&&': { cursor: 'default', m: 0.5 } }}
|
||||
disableRipple>
|
||||
{icon}
|
||||
</IconButton>
|
||||
<Box py={0.5}>
|
||||
{children ? (
|
||||
children
|
||||
) : (
|
||||
<>
|
||||
<Typography sx={{ wordBreak: 'break-all' }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="small" color="text.muted">
|
||||
{caption}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{customEndButton
|
||||
? customEndButton
|
||||
: !hideEditOption && (
|
||||
<IconButton onClick={openEditor} color="secondary">
|
||||
{!loading ? <Edit /> : <SmallLoadingSpinner />}
|
||||
</IconButton>
|
||||
)}
|
||||
</FlexWrapper>
|
||||
);
|
||||
}
|
127
packages/shared/components/Menu/EnteMenuItem.tsx
Normal file
127
packages/shared/components/Menu/EnteMenuItem.tsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
import {
|
||||
MenuItem,
|
||||
ButtonProps,
|
||||
Box,
|
||||
Typography,
|
||||
TypographyProps,
|
||||
} from '@mui/material';
|
||||
import { CaptionedText } from '../CaptionedText';
|
||||
import PublicShareSwitch from '../Collections/CollectionShare/publicShare/switch';
|
||||
import {
|
||||
SpaceBetweenFlex,
|
||||
VerticallyCenteredFlex,
|
||||
} from '@ente/shared/components/Container';
|
||||
import React from 'react';
|
||||
import ChangeDirectoryOption from '../Directory/changeOption';
|
||||
|
||||
interface Iprops {
|
||||
onClick: () => void;
|
||||
color?: ButtonProps['color'];
|
||||
variant?:
|
||||
| 'primary'
|
||||
| 'captioned'
|
||||
| 'toggle'
|
||||
| 'secondary'
|
||||
| 'mini'
|
||||
| 'path';
|
||||
fontWeight?: TypographyProps['fontWeight'];
|
||||
startIcon?: React.ReactNode;
|
||||
endIcon?: React.ReactNode;
|
||||
label?: string;
|
||||
subText?: string;
|
||||
subIcon?: React.ReactNode;
|
||||
checked?: boolean;
|
||||
labelComponent?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
export function EnteMenuItem({
|
||||
onClick,
|
||||
color = 'primary',
|
||||
startIcon,
|
||||
endIcon,
|
||||
label,
|
||||
subText,
|
||||
subIcon,
|
||||
checked,
|
||||
variant = 'primary',
|
||||
fontWeight = 'bold',
|
||||
labelComponent,
|
||||
disabled = false,
|
||||
}: Iprops) {
|
||||
const handleButtonClick = () => {
|
||||
if (variant === 'path' || variant === 'toggle') {
|
||||
return;
|
||||
}
|
||||
onClick();
|
||||
};
|
||||
|
||||
const handleIconClick = () => {
|
||||
if (variant !== 'path' && variant !== 'toggle') {
|
||||
return;
|
||||
}
|
||||
onClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
disabled={disabled}
|
||||
onClick={handleButtonClick}
|
||||
sx={{
|
||||
width: '100%',
|
||||
color: (theme) =>
|
||||
variant !== 'captioned' && theme.palette[color].main,
|
||||
...(variant !== 'secondary' &&
|
||||
variant !== 'mini' && {
|
||||
backgroundColor: (theme) => theme.colors.fill.faint,
|
||||
}),
|
||||
'&:hover': {
|
||||
backgroundColor: (theme) => theme.colors.fill.faintPressed,
|
||||
},
|
||||
'& .MuiSvgIcon-root': {
|
||||
fontSize: '20px',
|
||||
},
|
||||
p: 0,
|
||||
borderRadius: '4px',
|
||||
}}>
|
||||
<SpaceBetweenFlex sx={{ pl: '16px', pr: '12px' }}>
|
||||
<VerticallyCenteredFlex sx={{ py: '14px' }} gap={'10px'}>
|
||||
{startIcon && startIcon}
|
||||
<Box px={'2px'}>
|
||||
{labelComponent ? (
|
||||
labelComponent
|
||||
) : variant === 'captioned' ? (
|
||||
<CaptionedText
|
||||
color={color}
|
||||
mainText={label}
|
||||
subText={subText}
|
||||
subIcon={subIcon}
|
||||
/>
|
||||
) : variant === 'mini' ? (
|
||||
<Typography variant="mini" color="text.muted">
|
||||
{label}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography fontWeight={fontWeight}>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</VerticallyCenteredFlex>
|
||||
<VerticallyCenteredFlex gap={'4px'}>
|
||||
{endIcon && endIcon}
|
||||
{variant === 'toggle' && (
|
||||
<PublicShareSwitch
|
||||
checked={checked}
|
||||
onClick={handleIconClick}
|
||||
/>
|
||||
)}
|
||||
{variant === 'path' && (
|
||||
<ChangeDirectoryOption
|
||||
changeExportDirectory={handleIconClick}
|
||||
/>
|
||||
)}
|
||||
</VerticallyCenteredFlex>
|
||||
</SpaceBetweenFlex>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
16
packages/shared/components/Menu/MenuItemDivider.tsx
Normal file
16
packages/shared/components/Menu/MenuItemDivider.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Divider } from '@mui/material';
|
||||
interface Iprops {
|
||||
hasIcon?: boolean;
|
||||
}
|
||||
export default function MenuItemDivider({ hasIcon = false }: Iprops) {
|
||||
return (
|
||||
<Divider
|
||||
sx={{
|
||||
'&&&': {
|
||||
my: 0,
|
||||
ml: hasIcon ? '48px' : '16px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
20
packages/shared/components/Menu/MenuItemGroup.tsx
Normal file
20
packages/shared/components/Menu/MenuItemGroup.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { styled } from '@mui/material';
|
||||
|
||||
export const MenuItemGroup = styled('div')(
|
||||
({ theme }) => `
|
||||
& > .MuiMenuItem-root{
|
||||
border-radius: 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
& > .MuiMenuItem-root:not(:last-of-type) {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
& > .MuiMenuItem-root:not(:first-of-type) {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
background-color: ${theme.colors.fill.faint};
|
||||
border-radius: 8px;
|
||||
`
|
||||
);
|
27
packages/shared/components/Menu/MenuSectionTitle.tsx
Normal file
27
packages/shared/components/Menu/MenuSectionTitle.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Typography } from '@mui/material';
|
||||
import { VerticallyCenteredFlex } from '@ente/shared/components/Container';
|
||||
|
||||
interface Iprops {
|
||||
title: string;
|
||||
icon?: JSX.Element;
|
||||
}
|
||||
|
||||
export default function MenuSectionTitle({ title, icon }: Iprops) {
|
||||
return (
|
||||
<VerticallyCenteredFlex
|
||||
px="8px"
|
||||
py={'6px'}
|
||||
gap={'8px'}
|
||||
sx={{
|
||||
'& > svg': {
|
||||
fontSize: '17px',
|
||||
color: (theme) => theme.colors.stroke.muted,
|
||||
},
|
||||
}}>
|
||||
{icon && icon}
|
||||
<Typography variant="small" color="text.muted">
|
||||
{title}
|
||||
</Typography>
|
||||
</VerticallyCenteredFlex>
|
||||
);
|
||||
}
|
56
packages/shared/components/Titlebar.tsx
Normal file
56
packages/shared/components/Titlebar.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import Close from '@mui/icons-material/Close';
|
||||
import ArrowBack from '@mui/icons-material/ArrowBack';
|
||||
import { Box, IconButton, Typography } from '@mui/material';
|
||||
import { FlexWrapper } from '@ente/shared/components/Container';
|
||||
|
||||
interface Iprops {
|
||||
title: string;
|
||||
caption?: string;
|
||||
onClose: () => void;
|
||||
backIsClose?: boolean;
|
||||
onRootClose?: () => void;
|
||||
actionButton?: JSX.Element;
|
||||
}
|
||||
|
||||
export default function Titlebar({
|
||||
title,
|
||||
caption,
|
||||
onClose,
|
||||
backIsClose,
|
||||
actionButton,
|
||||
onRootClose,
|
||||
}: Iprops): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<FlexWrapper
|
||||
height={48}
|
||||
alignItems={'center'}
|
||||
justifyContent="space-between">
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
color={backIsClose ? 'secondary' : 'primary'}>
|
||||
{backIsClose ? <Close /> : <ArrowBack />}
|
||||
</IconButton>
|
||||
<Box display={'flex'} gap="4px">
|
||||
{actionButton && actionButton}
|
||||
{!backIsClose && (
|
||||
<IconButton onClick={onRootClose} color={'secondary'}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</FlexWrapper>
|
||||
<Box py={0.5} px={2}>
|
||||
<Typography variant="h3" fontWeight={'bold'}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="small"
|
||||
color="text.muted"
|
||||
sx={{ wordBreak: 'break-all', minHeight: '17px' }}>
|
||||
{caption}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
|
||||
export const SmallLoadingSpinner = () => (
|
||||
<EnteSpinner
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
}}
|
||||
/>
|
||||
);
|
|
@ -33,3 +33,18 @@ export enum AUTH_PAGES {
|
|||
ROOT = '/',
|
||||
AUTH = '/auth',
|
||||
}
|
||||
|
||||
export enum ACCOUNTS_PAGES {
|
||||
CREDENTIALS = '/credentials',
|
||||
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 = '/',
|
||||
PASSKEYS = '/passkeys',
|
||||
ACCOUNT_HANDOFF = '/account-handoff',
|
||||
GENERATE = '/generate',
|
||||
}
|
||||
|
|
|
@ -82,6 +82,7 @@ export const CustomError = {
|
|||
PROCESSING_FAILED: 'processing failed',
|
||||
EXPORT_RECORD_JSON_PARSING_FAILED: 'export record json parsing failed',
|
||||
TWO_FACTOR_ENABLED: 'two factor enabled',
|
||||
PASSKEYS_TWO_FACTOR_ENABLED: 'passkeys two factor enabled',
|
||||
CLIENT_ERROR: 'client error',
|
||||
ServerError: 'server error',
|
||||
FILE_NOT_FOUND: 'file not found',
|
||||
|
|
|
@ -191,6 +191,28 @@ class HTTPService {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch request
|
||||
*/
|
||||
public patch(
|
||||
url: string,
|
||||
data?: any,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
data,
|
||||
headers,
|
||||
method: 'PATCH',
|
||||
params,
|
||||
url,
|
||||
},
|
||||
customConfig
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put request
|
||||
*/
|
||||
|
|
|
@ -90,6 +90,47 @@ export const getFamilyPortalURL = () => {
|
|||
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 getAccountsURL = () => {
|
||||
const accountsURL = process.env.NEXT_PUBLIC_ENTE_ACCOUNTS_ENDPOINT;
|
||||
if (isDevDeployment() && accountsURL) {
|
||||
return accountsURL;
|
||||
}
|
||||
return `https://accounts.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'
|
||||
);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* A build is considered as a development build if either the NODE_ENV is
|
||||
* environment variable is set to 'development'.
|
||||
|
|
|
@ -28,6 +28,7 @@ export enum LS_KEYS {
|
|||
OPT_OUT_OF_CRASH_REPORTS = 'optOutOfCrashReports',
|
||||
CF_PROXY_DISABLED = 'cfProxyDisabled',
|
||||
REFERRAL_SOURCE = 'referralSource',
|
||||
CLIENT_PACKAGE = 'clientPackage',
|
||||
}
|
||||
|
||||
export const setData = (key: LS_KEYS, value: object) => {
|
||||
|
|
Loading…
Add table
Reference in a new issue