Compare commits
106 Commits
auth-v3.0.
...
photosd-v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99fdbd8d40 | ||
|
|
ac4a68d64e | ||
|
|
dae5b29ef1 | ||
|
|
4451b489e4 | ||
|
|
b19281ea2b | ||
|
|
8e923fe443 | ||
|
|
fe47186ace | ||
|
|
326704a605 | ||
|
|
d560ed9a33 | ||
|
|
0a8f51832a | ||
|
|
d2112b984d | ||
|
|
50aad0c5d1 | ||
|
|
e6e721f0ba | ||
|
|
80b34f1aef | ||
|
|
d5a8586152 | ||
|
|
bfcd84c940 | ||
|
|
a4bc5fa0df | ||
|
|
ed406e7eb0 | ||
|
|
b4dc49ef2f | ||
|
|
483e3be682 | ||
|
|
17f0d77a31 | ||
|
|
c6f644ef8a | ||
|
|
01b566698f | ||
|
|
469f884d8c | ||
|
|
9e4412cbee | ||
|
|
f4bab262ca | ||
|
|
73fd63616d | ||
|
|
9362a4b9d3 | ||
|
|
6c5ea59506 | ||
|
|
90845bdb02 | ||
|
|
f6729be5ab | ||
|
|
344c5cc399 | ||
|
|
6e1ea29c39 | ||
|
|
d76c6dd63c | ||
|
|
f69daa4608 | ||
|
|
290564c973 | ||
|
|
b781f33e4b | ||
|
|
b8bc01561d | ||
|
|
734cb798d3 | ||
|
|
ac8ebd0ed3 | ||
|
|
fc5eb296d2 | ||
|
|
c05d8a8e44 | ||
|
|
24845a4735 | ||
|
|
2b490fe131 | ||
|
|
07f0cc9342 | ||
|
|
49ddd287d0 | ||
|
|
bffcd11100 | ||
|
|
25d6ebdb19 | ||
|
|
64a539adb0 | ||
|
|
3646809f06 | ||
|
|
fb0e857514 | ||
|
|
a1059c543b | ||
|
|
8fe2b9cb27 | ||
|
|
5e080a90e3 | ||
|
|
08255b3f8a | ||
|
|
f032739461 | ||
|
|
841da80c97 | ||
|
|
60b1c32567 | ||
|
|
bd6ac2c4fc | ||
|
|
eaccba5f22 | ||
|
|
562313b218 | ||
|
|
0650d176ee | ||
|
|
6bbd944de4 | ||
|
|
8aaad79897 | ||
|
|
d499549734 | ||
|
|
db22c5bc97 | ||
|
|
34f49362fd | ||
|
|
af21ff640d | ||
|
|
69e69c2e0f | ||
|
|
a0445fb4f6 | ||
|
|
8161403d84 | ||
|
|
0713e34aec | ||
|
|
b504f554b3 | ||
|
|
3d6af698b6 | ||
|
|
ff3ddb3d8d | ||
|
|
16817eceac | ||
|
|
500e40035f | ||
|
|
366da2c328 | ||
|
|
203d46b2cf | ||
|
|
0e772fcfb7 | ||
|
|
bbd6745372 | ||
|
|
dd1e0a9b1d | ||
|
|
940231e38d | ||
|
|
4c8db02de5 | ||
|
|
8af5aadd1b | ||
|
|
205feab4c2 | ||
|
|
60ab2b4427 | ||
|
|
612329f584 | ||
|
|
a5f4a676a7 | ||
|
|
9608cfaa4e | ||
|
|
ddd4d3e16c | ||
|
|
df0d48af73 | ||
|
|
c82193cae6 | ||
|
|
2c0928bd02 | ||
|
|
8c8ffa9397 | ||
|
|
3689ecb6e7 | ||
|
|
ca080ad6b2 | ||
|
|
b2e56fc01e | ||
|
|
228dd90bce | ||
|
|
93380d05b4 | ||
|
|
4123197c6d | ||
|
|
cc3f398a78 | ||
|
|
dd0f7d3142 | ||
|
|
325c963b7a | ||
|
|
fbf29585eb | ||
|
|
8a2cc858ae |
@@ -6,7 +6,7 @@ FEATURES
|
||||
|
||||
- Secure Backups
|
||||
Auth provides end-to-end encrypted cloud backups so that you don't have to worry
|
||||
about losing your tokens. We use the same protocols ente Photos uses to encrypt
|
||||
about losing your tokens. We use the same protocols Ente Photos uses to encrypt
|
||||
and preserve your data.
|
||||
|
||||
- Multi Device Synchronization
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
{
|
||||
"title": "Cloudflare"
|
||||
},
|
||||
{
|
||||
"title": "CoinDCX"
|
||||
},
|
||||
{
|
||||
"title": "ConfigCat"
|
||||
},
|
||||
|
||||
4
auth/assets/custom-icons/icons/coindcx.svg
Normal file
4
auth/assets/custom-icons/icons/coindcx.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 500 500">
|
||||
<path fill="#182954" d="m75.705 269.386 12.606 10.812a40.902 40.902 0 0 1-8.642 8.853 53.365 53.365 0 0 1-13.599 7.73 45.769 45.769 0 0 1-16.998 3.094 49.02 49.02 0 0 1-25.212-6.466A45.84 45.84 0 0 1 6.72 275.84a50.83 50.83 0 0 1-6.212-25.287 52.621 52.621 0 0 1 3.525-19.394 49.28 49.28 0 0 1 10.2-16.022 46.603 46.603 0 0 1 15.44-10.812 49.626 49.626 0 0 1 19.969-3.938 45.9 45.9 0 0 1 23.51 5.48A49.016 49.016 0 0 1 88.308 219.5l-12.744 11.244A39.368 39.368 0 0 0 64.938 220.2a27.358 27.358 0 0 0-15.296-3.933 27.636 27.636 0 0 0-16.147 4.632 30.695 30.695 0 0 0-10.478 12.508 38.957 38.957 0 0 0-3.688 16.879 36.724 36.724 0 0 0 3.684 16.442 29.719 29.719 0 0 0 10.184 11.793 27.208 27.208 0 0 0 15.44 4.358c4.608.197 9.203-.62 13.456-2.391a27.765 27.765 0 0 0 8.214-5.622l5.381-5.481M93.275 264.047a35.477 35.477 0 0 1 4.535-17.71 34.84 34.84 0 0 1 12.748-12.929 39.497 39.497 0 0 1 18.838-4.778 39.497 39.497 0 0 1 18.838 4.778 34.846 34.846 0 0 1 12.749 12.928 36.889 36.889 0 0 1 4.532 17.709 36.891 36.891 0 0 1-4.532 17.708 36.519 36.519 0 0 1-13.365 13.153 36.875 36.875 0 0 1-18.181 4.837 36.88 36.88 0 0 1-18.203-4.756 36.513 36.513 0 0 1-13.424-13.092 35.479 35.479 0 0 1-4.535-17.707v-.141zm35.979 21.224a16.949 16.949 0 0 0 10.623-3.23c2.804-2.121 5-4.93 6.375-8.151a24.848 24.848 0 0 0 2.124-9.698 24.293 24.293 0 0 0-2.124-9.697 20.265 20.265 0 0 0-6.375-8.15 19.056 19.056 0 0 0-10.623-3.233 19.057 19.057 0 0 0-10.625 3.233 20.118 20.118 0 0 0-6.231 8.009 24.296 24.296 0 0 0-2.125 9.697 24.713 24.713 0 0 0 2.125 9.839 19.985 19.985 0 0 0 6.374 8.15 16.949 16.949 0 0 0 10.624 3.231M168.905 202.628h16.856v17.71h-16.856v-17.71zm0 28.11h16.856v66.758h-16.856v-66.758zM192.416 297.495V230.88h16.147l.42 7.589a35.937 35.937 0 0 1 7.505-5.905 23.656 23.656 0 0 1 12.749-3.094 24.38 24.38 0 0 1 10.396 1.612 24.22 24.22 0 0 1 8.726 5.836 29.047 29.047 0 0 1 6.66 20.097v40.477H238.02v-40.335a13.257 13.257 0 0 0-.76-5.278 13.337 13.337 0 0 0-2.78-4.561 12.19 12.19 0 0 0-4.164-2.694 12.27 12.27 0 0 0-4.902-.82 14.974 14.974 0 0 0-6.377 1.24 14.87 14.87 0 0 0-5.236 3.82 18.046 18.046 0 0 0-4.534 12.51v36.118l-16.851.004z"/>
|
||||
<path fill="#FA4A29" d="m463.25 246.618 29.754-44.007h-28.187l-15.44 24.596-15.883-24.596h-31.163l1.416 1.967-.993-.416a63.329 63.329 0 0 0-23.083-4.046 50.453 50.453 0 0 0-25.92 6.607 46.609 46.609 0 0 0-14.308 12.929 40.334 40.334 0 0 0-15.582-11.806 65.028 65.028 0 0 0-26.344-5.077h-36.686v94.727h36.544a64.026 64.026 0 0 0 26.344-5.202A41.612 41.612 0 0 0 339.3 280.63c3.87 5.299 8.846 9.709 14.59 12.928a51.44 51.44 0 0 0 25.777 6.325 55.023 55.023 0 0 0 24.646-5.34l-1.982 2.953h27.76l18.558-29.108 19.122 29.108h31.73l-36.252-50.878zm-147.452 21.624a25.772 25.772 0 0 1-8.902 5.504 25.916 25.916 0 0 1-10.376 1.523h-10.334v-50.573h10.338c3.62-.305 7.264.165 10.685 1.378a25.427 25.427 0 0 1 9.147 5.65 26.146 26.146 0 0 1 6.374 18.271 24.821 24.821 0 0 1-1.597 9.836 24.965 24.965 0 0 1-5.343 8.436l.008-.025zm101.549 6.911-12.04-11.228a38.572 38.572 0 0 1-10.197 9.149 27.09 27.09 0 0 1-13.6 2.952 25.509 25.509 0 0 1-13.314-3.372 22.838 22.838 0 0 1-8.8-9.415 29.459 29.459 0 0 1-3.118-13.63c-.091-4.623.929-9.2 2.975-13.353a23.258 23.258 0 0 1 8.642-9.415 25.653 25.653 0 0 1 13.738-3.513 24.798 24.798 0 0 1 12.748 3.23 32.061 32.061 0 0 1 9.639 8.733l12.606-12.508 18.415 26.28-17.694 26.09z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -6,7 +6,7 @@ FEATURES
|
||||
|
||||
- Secure Backups
|
||||
ente provides end-to-end encrypted cloud backups so that you don't have to worry
|
||||
about losing your tokens. We use the same protocols ente Photos uses to encrypt
|
||||
about losing your tokens. We use the same protocols Ente Photos uses to encrypt
|
||||
and preserve your data.
|
||||
|
||||
- Multi Device Synchronization
|
||||
|
||||
@@ -297,31 +297,41 @@ class UserService {
|
||||
await dialog.show();
|
||||
try {
|
||||
final userPassword = _config.getVolatilePassword();
|
||||
if (userPassword == null) throw Exception("volatile password is null");
|
||||
|
||||
await _saveConfiguration(response);
|
||||
|
||||
Widget page;
|
||||
if (_config.getEncryptedToken() != null) {
|
||||
await _config.decryptSecretsAndGetKeyEncKey(
|
||||
userPassword,
|
||||
_config.getKeyAttributes()!,
|
||||
if (userPassword == null) {
|
||||
await dialog.hide();
|
||||
// ignore: unawaited_futures
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const PasswordReentryPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
page = const HomePage();
|
||||
} else {
|
||||
throw Exception("unexpected response during passkey verification");
|
||||
}
|
||||
await dialog.hide();
|
||||
Widget page;
|
||||
if (_config.getEncryptedToken() != null) {
|
||||
await _config.decryptSecretsAndGetKeyEncKey(
|
||||
userPassword,
|
||||
_config.getKeyAttributes()!,
|
||||
);
|
||||
page = const HomePage();
|
||||
} else {
|
||||
throw Exception("unexpected response during passkey verification");
|
||||
}
|
||||
await dialog.hide();
|
||||
|
||||
// ignore: unawaited_futures
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return page;
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
// ignore: unawaited_futures
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return page;
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
await dialog.hide();
|
||||
@@ -351,9 +361,12 @@ class UserService {
|
||||
await dialog.hide();
|
||||
if (response.statusCode == 200) {
|
||||
Widget page;
|
||||
final String passkeySessionID = response.data["passkeySessionID"];
|
||||
final String twoFASessionID = response.data["twoFactorSessionID"];
|
||||
if (twoFASessionID.isNotEmpty) {
|
||||
page = TwoFactorAuthenticationPage(twoFASessionID);
|
||||
} else if (passkeySessionID.isNotEmpty) {
|
||||
page = PasskeyPage(passkeySessionID);
|
||||
} else {
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
|
||||
@@ -27,3 +27,6 @@ include:
|
||||
- libffi.so.8
|
||||
- libtiff.so.5
|
||||
- libjpeg.so.8
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/enteauth
|
||||
@@ -31,4 +31,4 @@ categories:
|
||||
startup_notify: false
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/ente
|
||||
- x-scheme-handler/enteauth
|
||||
|
||||
@@ -28,4 +28,4 @@ categories:
|
||||
startup_notify: false
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/ente
|
||||
- x-scheme-handler/enteauth
|
||||
|
||||
@@ -45,10 +45,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: archive
|
||||
sha256: "0763b45fa9294197a2885c8567927e2830ade852e5c896fd4ab7e0e348d0f373"
|
||||
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.0"
|
||||
version: "3.6.1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -117,10 +117,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
|
||||
sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
version: "4.0.2"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -133,18 +133,18 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
|
||||
sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.9"
|
||||
version: "2.4.11"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799"
|
||||
sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
version: "7.3.1"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -415,10 +415,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_saver
|
||||
sha256: bdebc720e17b3e01aba59da69b6d47020a7e5ba7d5c75bd9194f9618d5f16ef4
|
||||
sha256: d375b351e3331663abbaf99747abd72f159260c58fbbdbca9f926f02c01bdc48
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.12"
|
||||
version: "0.2.13"
|
||||
fixnum:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -444,10 +444,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_bloc
|
||||
sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2
|
||||
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.5"
|
||||
version: "8.1.6"
|
||||
flutter_context_menu:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -586,18 +586,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f"
|
||||
sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.19"
|
||||
version: "2.0.20"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685
|
||||
sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
version: "9.2.2"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
@@ -611,34 +611,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c
|
||||
sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.1.2"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e"
|
||||
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.1.2"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20"
|
||||
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.2.1"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108"
|
||||
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.1.2"
|
||||
flutter_slidable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -685,10 +685,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluttertoast
|
||||
sha256: "81b68579e23fcbcada2db3d50302813d2371664afe6165bc78148050ab94bf66"
|
||||
sha256: "7eae679e596a44fdf761853a706f74979f8dd3cd92cf4e23cae161fda091b847"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.5"
|
||||
version: "8.2.6"
|
||||
freezed_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -725,10 +725,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: gradient_borders
|
||||
sha256: "69eeaff519d145a4c6c213ada1abae386bcc8981a4970d923e478ce7ba19e309"
|
||||
sha256: b1cd969552c83f458ff755aa68e13a0327d09f06c3f42f471b423b01427f21f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -757,10 +757,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hashlib_codecs
|
||||
sha256: "49e2a471f74b15f1854263e58c2ac11f2b631b5b12c836f9708a35397d36d626"
|
||||
sha256: a1c7b5d89ff29e81fd8e8c0b35966db4c935e149fc4ebe1ebf71e358c15863ab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.4.0"
|
||||
hex:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -805,10 +805,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
|
||||
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.7"
|
||||
version: "4.2.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -893,18 +893,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: e0e5b1ea247c5a0951c13a7ee13dc1beae69750e6a2e1910d1ed6a3cd4d56943
|
||||
sha256: "48dfb2d954da8ef6a77adfc93a29998f7729e9308eaa817e91dea4500317b2c8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.38"
|
||||
version: "1.0.39"
|
||||
local_auth_darwin:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth_darwin
|
||||
sha256: "33381a15b0de2279523eca694089393bb146baebdce72a404555d03174ebc1e9"
|
||||
sha256: e424ebf90d5233452be146d4a7da4bcd7a70278b67791592f3fde1bda8eef9e2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
version: "1.3.1"
|
||||
local_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -973,10 +973,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: mocktail
|
||||
sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6
|
||||
sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "1.0.4"
|
||||
modal_bottom_sheet:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1085,18 +1085,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
|
||||
sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
version: "2.2.5"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
|
||||
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1141,10 +1141,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
version: "3.1.5"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1157,10 +1157,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "79fbafed02cfdbe85ef3fd06c7f4bc2cbcba0177e61b765264853d4253b21744"
|
||||
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.9.0"
|
||||
version: "3.9.1"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1205,10 +1205,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
|
||||
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
version: "1.3.0"
|
||||
qr:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1245,18 +1245,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry
|
||||
sha256: e572d33a3ff1d69549f33ee828a8ff514047d43ca8eea4ab093d72461205aa3e
|
||||
sha256: "57514bc72d441ffdc463f498d6886aa586a2494fa467a1eb9d649c28010d7ee3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.20.1"
|
||||
version: "7.20.2"
|
||||
sentry_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry_flutter
|
||||
sha256: ac8cf6bb849f3560353ae33672e17b2713809a4e8de0d3cf372e9e9c42013757
|
||||
sha256: "9723d58470ca43a360681ddd26abb71ca7b815f706bc8d3747afd054cf639ded"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.20.1"
|
||||
version: "7.20.2"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1285,18 +1285,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
|
||||
sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.2.3"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
|
||||
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
version: "2.4.0"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1341,10 +1341,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
|
||||
sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "2.0.0"
|
||||
shortid:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1370,10 +1370,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sodium_libs
|
||||
sha256: f7f6719b7ab3e8512ce7a5ecd7bc8d865482431cdd5a07a46b55b13c152b54e1
|
||||
sha256: "441444f6f433032bae3444c2ef5ed2cf5bc0def77f104abdff20aedcf79a7c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1+1"
|
||||
version: "2.2.1+5"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1411,10 +1411,10 @@ packages:
|
||||
description:
|
||||
path: sqflite
|
||||
ref: HEAD
|
||||
resolved-ref: f281785e12e8b1abf2f9d41a587fc83d810724cf
|
||||
resolved-ref: "3309d399dd7d695bbfa7c05f643bb16765cef4ee"
|
||||
url: "https://github.com/tekartik/sqflite"
|
||||
source: git
|
||||
version: "2.3.3"
|
||||
version: "2.3.3+1"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1435,18 +1435,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "1abbeb84bf2b1a10e5e1138c913123c8aa9d83cd64e5f9a0dd847b3c83063202"
|
||||
sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.4.3"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: fb2a106a2ea6042fe57de2c47074cc31539a941819c91e105b864744605da3f5
|
||||
sha256: "9f89a7e7dc36eac2035808427eba1c3fbd79e59c3a22093d8dace6d36b1fe89e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.21"
|
||||
version: "0.5.23"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1547,10 +1547,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tray_manager
|
||||
sha256: e0ac9a88b2700f366b8629b97e8663b6ef450a2f169560a685dc167bfe9c9c29
|
||||
sha256: c9a63fd88bd3546287a7eb8ccc978d707eef82c775397af17dda3a4f4c039e64
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
version: "0.2.3"
|
||||
tuple:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1579,26 +1579,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e"
|
||||
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.6"
|
||||
version: "6.3.0"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775"
|
||||
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
version: "6.3.3"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5"
|
||||
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.5"
|
||||
version: "6.3.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1611,10 +1611,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
|
||||
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.2.0"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1703,22 +1703,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42"
|
||||
sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.5"
|
||||
version: "3.0.0"
|
||||
win32:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: win32
|
||||
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
|
||||
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.0"
|
||||
version: "5.5.1"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1768,5 +1776,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
dart: ">=3.4.0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 3.0.11+311
|
||||
version: 3.0.12+312
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v1.7.1 (Unreleased)
|
||||
## v1.7.1
|
||||
|
||||
- Support for passkeys as a second factor authentication mechanism.
|
||||
- Remember the window size across app restarts.
|
||||
- Revert changes to the Linux icon.
|
||||
- Fix an issue where deleted items in watched folders would not move to
|
||||
- Fix an issue causing deleted items in watched folders to not move to
|
||||
uncategorized.
|
||||
- Fix duplicate file uploads when initializing a folder watch (sometimes).
|
||||
|
||||
## v1.7.0
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ente",
|
||||
"version": "1.7.1-rc",
|
||||
"version": "1.7.1",
|
||||
"private": true,
|
||||
"description": "Desktop client for Ente Photos",
|
||||
"repository": "github:ente-io/photos-desktop",
|
||||
|
||||
@@ -23,6 +23,12 @@ export const createWatcher = (mainWindow: BrowserWindow) => {
|
||||
const folderPaths = folderWatches().map((watch) => watch.folderPath);
|
||||
|
||||
const watcher = chokidar.watch(folderPaths, {
|
||||
// Don't emit "add" events for matching paths when instantiating the
|
||||
// watch (we do a full disk scan on launch on our own, and also getting
|
||||
// the same events from the watcher causes duplicates).
|
||||
ignoreInitial: true,
|
||||
// Ask the watcher to wait for a the file size to stabilize before
|
||||
// telling us about a new file. By default, it waits for 2 seconds.
|
||||
awaitWriteFinish: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -20,8 +20,12 @@ start it, then you might need to install the VC++ runtime from Microsoft.
|
||||
|
||||
This is what the error looks like:
|
||||
|
||||
<div style="border: 1px solid black">
|
||||
|
||||
{width=500px}
|
||||
|
||||
</div>
|
||||
|
||||
You can install the Microsoft VC++ redistributable runtime from here:<br/>
|
||||
https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#latest-microsoft-visual-c-redistributable-version
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"tabWidth": 4
|
||||
"tabWidth": 4,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
# Cloudflare Workers
|
||||
|
||||
Source code for our [Cloudflare
|
||||
Workers](https://developers.cloudflare.com/workers/).
|
||||
Source code for our
|
||||
[Cloudflare Workers](https://developers.cloudflare.com/workers/).
|
||||
|
||||
Each worker is a self contained directory with its each `package.json`.
|
||||
|
||||
## Deploying
|
||||
|
||||
* Switch to a worker directory, e.g. `cd github-discord-notifier`.
|
||||
- Switch to a worker directory, e.g. `cd github-discord-notifier`.
|
||||
|
||||
* Install dependencies (if needed) with `yarn`
|
||||
- Install dependencies (if needed) with `yarn`
|
||||
|
||||
* Login into wrangler (if needed) using `yarn wrangler login`
|
||||
> If you have previously deployed, then you will have an old `yarn.lock`. In
|
||||
> this case it is safe to delete and recreate using `rm yarn.lock && yarn`.
|
||||
|
||||
* Deploy! `yarn wrangler deploy`
|
||||
- Login into wrangler (if needed) using `yarn wrangler login`
|
||||
|
||||
- Deploy! `yarn wrangler deploy`
|
||||
|
||||
Wrangler is the CLI provided by Cloudflare to manage workers. Apart from
|
||||
deploying, it also allows us to stream logs from running workers by using `yarn
|
||||
wrangler tail`.
|
||||
deploying, it also allows us to stream logs from running workers by using
|
||||
`yarn wrangler tail`.
|
||||
|
||||
## Creating a new worker
|
||||
|
||||
@@ -30,3 +33,12 @@ To import an existing worker from the Cloudflare dashboard, use
|
||||
```sh
|
||||
npm create cloudflare@2 existing-worker-name -- --type pre-existing --existing-script existing-worker-name
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
Attach the tail worker to your worker by adding
|
||||
|
||||
tail_consumers = [{ service = "tail" }]
|
||||
|
||||
in its `wrangler.toml`. Then any `console.(log|warn|error)` statements and
|
||||
uncaught exceptions in your worker will be logged to Grafana.
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
"name": "cast-albums",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240314.0",
|
||||
"@cloudflare/workers-types": "^4.20240614.0",
|
||||
"typescript": "^5",
|
||||
"wrangler": "^3"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
|
||||
@@ -1,50 +1,69 @@
|
||||
/** Proxy file and thumbnail requests from the cast web app */
|
||||
/** Proxy file and thumbnail requests for the cast web app. */
|
||||
|
||||
export default {
|
||||
async fetch(request: Request) {
|
||||
switch (request.method) {
|
||||
case "GET":
|
||||
return handleGET(request);
|
||||
case "OPTIONS":
|
||||
return handleOPTIONS(request);
|
||||
case "GET":
|
||||
return handleGET(request);
|
||||
default:
|
||||
throw new Error(
|
||||
`HTTP 405 Method Not Allowed: ${request.method}`
|
||||
);
|
||||
console.log(`Unsupported HTTP method ${request.method}`);
|
||||
return new Response(null, { status: 405 });
|
||||
}
|
||||
},
|
||||
} satisfies ExportedHandler;
|
||||
|
||||
const handleOPTIONS = (request: Request) => {
|
||||
const origin = request.headers.get("Origin");
|
||||
if (!isAllowedOrigin(origin)) console.warn("Unknown origin", origin);
|
||||
return new Response("", {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
"Access-Control-Allow-Headers": "X-Cast-Access-Token",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isAllowedOrigin = (origin: string | null) => {
|
||||
const allowed = ["cast.ente.io", "cast.ente.sh", "localhost"];
|
||||
|
||||
if (!origin) return false;
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
return allowed.includes(url.hostname);
|
||||
} catch {
|
||||
// origin is likely an invalid URL
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleGET = async (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
const urlParams = new URLSearchParams(url.search);
|
||||
const token =
|
||||
request.headers.get("X-Cast-Access-Token") ??
|
||||
urlParams.get("castToken");
|
||||
|
||||
const fileID = urlParams.get("fileID");
|
||||
const fileID = url.searchParams.get("fileID");
|
||||
if (!fileID) return new Response(null, { status: 400 });
|
||||
|
||||
let castToken = request.headers.get("X-Cast-Access-Token");
|
||||
if (!castToken) {
|
||||
console.warn("Using deprecated castToken query param");
|
||||
castToken = url.searchParams.get("castToken");
|
||||
}
|
||||
|
||||
if (!castToken) {
|
||||
console.error("No cast token provided");
|
||||
return new Response(null, { status: 400 });
|
||||
}
|
||||
|
||||
const pathname = url.pathname;
|
||||
const params = new URLSearchParams({ castToken });
|
||||
|
||||
let response = await fetch(
|
||||
`https://api.ente.io/cast/files${pathname}${fileID}?castToken=${token}`
|
||||
`https://api.ente.io/cast/files${pathname}${fileID}?${params.toString()}`
|
||||
);
|
||||
|
||||
response = new Response(response.body, response);
|
||||
response.headers.set("Access-Control-Allow-Origin", "*");
|
||||
return response;
|
||||
};
|
||||
|
||||
const handleOPTIONS = (request: Request) => {
|
||||
let corsHeaders: Record<string, string> = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET,OPTIONS",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
};
|
||||
|
||||
const acrh = request.headers.get("Access-Control-Request-Headers");
|
||||
if (acrh) {
|
||||
corsHeaders["Access-Control-Allow-Headers"] = acrh;
|
||||
}
|
||||
|
||||
return new Response("", { headers: corsHeaders });
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
{ "extends": "../tsconfig.base.json", "include": ["src/**/*.ts"] }
|
||||
{ "extends": "../tsconfig.base.json", "include": ["src"] }
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
name = "cast-albums"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-03-14"
|
||||
compatibility_date = "2024-06-14"
|
||||
|
||||
[[routes]]
|
||||
pattern = "cast-albums.ente.io"
|
||||
zone_name = "ente.io"
|
||||
custom_domain = true
|
||||
routes = [
|
||||
{ pattern = "cast-albums.ente.io", custom_domain = true }
|
||||
]
|
||||
|
||||
tail_consumers = [
|
||||
{ service = "tail" }
|
||||
]
|
||||
|
||||
10
infra/workers/files/package.json
Normal file
10
infra/workers/files/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "files",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240614.0",
|
||||
"typescript": "^5",
|
||||
"wrangler": "^3"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
101
infra/workers/files/src/index.ts
Normal file
101
infra/workers/files/src/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/** Proxy requests for files. */
|
||||
|
||||
export default {
|
||||
async fetch(request: Request) {
|
||||
switch (request.method) {
|
||||
case "OPTIONS":
|
||||
return handleOPTIONS(request);
|
||||
case "GET":
|
||||
return handleGET(request);
|
||||
default:
|
||||
console.log(`Unsupported HTTP method ${request.method}`);
|
||||
return new Response(null, { status: 405 });
|
||||
}
|
||||
},
|
||||
} satisfies ExportedHandler;
|
||||
|
||||
const handleOPTIONS = (request: Request) => {
|
||||
const origin = request.headers.get("Origin");
|
||||
if (!isAllowedOrigin(origin)) console.warn("Unknown origin", origin);
|
||||
const headers = request.headers.get("Access-Control-Request-Headers");
|
||||
if (!areAllowedHeaders(headers))
|
||||
console.warn("Unknown header in list", headers);
|
||||
return new Response("", {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
// "Access-Control-Allow-Headers": "X-Auth-Token, X-Client-Package",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isAllowedOrigin = (origin: string | null) => {
|
||||
const desktopApp = "ente://app";
|
||||
const allowedHostnames = [
|
||||
"web.ente.io",
|
||||
"photos.ente.io",
|
||||
"photos.ente.sh",
|
||||
"localhost",
|
||||
];
|
||||
|
||||
if (!origin) return false;
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
return origin == desktopApp || allowedHostnames.includes(url.hostname);
|
||||
} catch {
|
||||
// origin is likely an invalid URL
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const areAllowedHeaders = (headers: string | null) => {
|
||||
const allowed = ["x-auth-token", "x-client-package"];
|
||||
|
||||
if (!headers) return true;
|
||||
for (const header of headers.split(",")) {
|
||||
if (!allowed.includes(header.trim().toLowerCase())) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleGET = async (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Random bots keep trying to pentest causing noise in the logs. If the
|
||||
// request doesn't have a fileID, we can just safely ignore it thereafter.
|
||||
const fileID = url.searchParams.get("fileID");
|
||||
if (!fileID) return new Response(null, { status: 400 });
|
||||
|
||||
let token = request.headers.get("X-Auth-Token");
|
||||
if (!token) {
|
||||
console.warn("Using deprecated token query param");
|
||||
token = url.searchParams.get("token");
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.error("No token provided");
|
||||
// return new Response(null, { status: 400 });
|
||||
}
|
||||
|
||||
// We forward the auth token as a query parameter to museum. This is so that
|
||||
// it does not get preserved when museum does a redirect to the presigned S3
|
||||
// URL that serves the actual thumbnail.
|
||||
//
|
||||
// See: [Note: Passing credentials for self-hosted file fetches]
|
||||
const params = new URLSearchParams();
|
||||
if (token) params.set("token", token);
|
||||
|
||||
let response = await fetch(
|
||||
`https://api.ente.io/files/download/${fileID}?${params.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": request.headers.get("User-Agent") ?? "",
|
||||
},
|
||||
}
|
||||
);
|
||||
response = new Response(response.body, response);
|
||||
response.headers.set("Access-Control-Allow-Origin", "*");
|
||||
return response;
|
||||
};
|
||||
1
infra/workers/files/tsconfig.json
Normal file
1
infra/workers/files/tsconfig.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "extends": "../tsconfig.base.json", "include": ["src"] }
|
||||
11
infra/workers/files/wrangler.toml
Normal file
11
infra/workers/files/wrangler.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
name = "files"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-06-14"
|
||||
|
||||
routes = [
|
||||
{ pattern = "files.ente.io", custom_domain = true }
|
||||
]
|
||||
|
||||
tail_consumers = [
|
||||
{ service = "tail" }
|
||||
]
|
||||
@@ -2,8 +2,9 @@
|
||||
"name": "github-discord-notifier",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240314.0",
|
||||
"@cloudflare/workers-types": "^4.20240614.0",
|
||||
"typescript": "^5",
|
||||
"wrangler": "^3"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Forward notifications from GitHub to Discord.
|
||||
*
|
||||
* This worker receives webhooks from GitHub, filters out the ones we don't
|
||||
* need, and forwards them to a Discord webhook.
|
||||
* need, and forwards the rest to a Discord webhook.
|
||||
*/
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
@@ -33,13 +33,13 @@ const handleRequest = async (request: Request, discordWebhookURL: string) => {
|
||||
// doesn't work for get silently ignored (Discord responds with a 204).
|
||||
// https://github.com/discord/discord-api-docs/issues/6203#issuecomment-1608151265
|
||||
|
||||
let response = await fetch(`${discordWebhookURL}/github`, {
|
||||
const response = await fetch(`${discordWebhookURL}/github`, {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: requestBody,
|
||||
});
|
||||
|
||||
if (response.status === 429) {
|
||||
if (response.status == 429) {
|
||||
// Sometimes Discord starts returning 429 Rate Limited responses when we
|
||||
// try to invoke the webhook.
|
||||
//
|
||||
@@ -79,7 +79,7 @@ const handleRequest = async (request: Request, discordWebhookURL: string) => {
|
||||
const action = requestJSON["action"];
|
||||
|
||||
if (activityURL && ["created", "opened"].includes(action)) {
|
||||
response = await fetch(discordWebhookURL, {
|
||||
return fetch(discordWebhookURL, {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: JSON.stringify({
|
||||
@@ -89,12 +89,5 @@ const handleRequest = async (request: Request, discordWebhookURL: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const responseBody = await response.text();
|
||||
const newResponse = new Response(responseBody, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
});
|
||||
|
||||
return newResponse;
|
||||
return response;
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
{ "extends": "../tsconfig.base.json", "include": ["src/**/*.ts"] }
|
||||
{ "extends": "../tsconfig.base.json", "include": ["src"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name = "github-discord-notifier"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-03-14"
|
||||
compatibility_date = "2024-06-14"
|
||||
|
||||
[vars]
|
||||
# Added as a secret via the Cloudflare dashboard
|
||||
|
||||
10
infra/workers/health-check/package.json
Normal file
10
infra/workers/health-check/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "health-check",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240614.0",
|
||||
"typescript": "^5",
|
||||
"wrangler": "^3"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
48
infra/workers/health-check/src/index.ts
Normal file
48
infra/workers/health-check/src/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/** Ping api.ente.io every minute and yell if it doesn't pong. */
|
||||
|
||||
export default {
|
||||
async scheduled(_, env: Env, ctx: ExecutionContext) {
|
||||
ctx.waitUntil(ping(env, ctx));
|
||||
},
|
||||
} satisfies ExportedHandler<Env>;
|
||||
|
||||
interface Env {
|
||||
NOTIFY_URL: string;
|
||||
CHAT_ID: string;
|
||||
}
|
||||
|
||||
const ping = async (env: Env, ctx: ExecutionContext) => {
|
||||
const notify = async (msg: string) =>
|
||||
sendMessage(`${msg} on ${Date()}`, env);
|
||||
|
||||
try {
|
||||
let timeout = setTimeout(() => {
|
||||
ctx.waitUntil(notify("Ping timed out"));
|
||||
}, 5000);
|
||||
const res = await fetch("https://api.ente.io/ping", {
|
||||
headers: {
|
||||
"User-Agent": "health-check",
|
||||
},
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
if (!res.ok) await notify(`Ping failed (HTTP ${res.status})`);
|
||||
} catch (e) {
|
||||
await notify(`Ping failed (${e instanceof Error ? e.message : e})`);
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = async (message: string, env: Env) => {
|
||||
console.log(message);
|
||||
const res = await fetch(env.NOTIFY_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_id: parseInt(env.CHAT_ID),
|
||||
parse_mode: "html",
|
||||
text: message,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to sendMessage (HTTP ${res.status})`);
|
||||
};
|
||||
1
infra/workers/health-check/tsconfig.json
Normal file
1
infra/workers/health-check/tsconfig.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "extends": "../tsconfig.base.json", "include": ["src"] }
|
||||
17
infra/workers/health-check/wrangler.toml
Normal file
17
infra/workers/health-check/wrangler.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
name = "health-check"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-06-14"
|
||||
|
||||
# Disable the default route, this worker does not handle fetch.
|
||||
workers_dev = false
|
||||
|
||||
tail_consumers = [{ service = "tail" }]
|
||||
|
||||
[vars]
|
||||
# Added as a secret via the Cloudflare dashboard
|
||||
# NOTIFY_URL = ""
|
||||
# CHAT_ID = ""
|
||||
|
||||
[triggers]
|
||||
# Every minute
|
||||
crons = [ "*/1 * * * *" ]
|
||||
10
infra/workers/public-albums/package.json
Normal file
10
infra/workers/public-albums/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "public-albums",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240614.0",
|
||||
"typescript": "^5",
|
||||
"wrangler": "^3"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
98
infra/workers/public-albums/src/index.ts
Normal file
98
infra/workers/public-albums/src/index.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/** Proxy requests for files and thumbnails in public albums. */
|
||||
|
||||
export default {
|
||||
async fetch(request: Request) {
|
||||
switch (request.method) {
|
||||
case "OPTIONS":
|
||||
return handleOPTIONS(request);
|
||||
case "GET":
|
||||
return handleGET(request);
|
||||
default:
|
||||
console.log(`Unsupported HTTP method ${request.method}`);
|
||||
return new Response(null, { status: 405 });
|
||||
}
|
||||
},
|
||||
} satisfies ExportedHandler;
|
||||
|
||||
const handleOPTIONS = (request: Request) => {
|
||||
const origin = request.headers.get("Origin");
|
||||
if (!isAllowedOrigin(origin)) console.warn("Unknown origin", origin);
|
||||
const headers = request.headers.get("Access-Control-Request-Headers");
|
||||
if (!areAllowedHeaders(headers))
|
||||
console.warn("Unknown header in list", headers);
|
||||
return new Response("", {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
// "Access-Control-Allow-Headers": "X-Auth-Access-Token, X-Auth-Access-Token-JWT",
|
||||
// "Access-Control-Allow-Headers": "X-Auth-Access-Token, X-Auth-Access-Token-JWT, x-client-package",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isAllowedOrigin = (origin: string | null) => {
|
||||
const allowed = ["albums.ente.io", "albums.ente.sh", "localhost"];
|
||||
|
||||
if (!origin) return false;
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
return allowed.includes(url.hostname);
|
||||
} catch {
|
||||
// origin is likely an invalid URL
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const areAllowedHeaders = (headers: string | null) => {
|
||||
// TODO(MR): Stop sending "x-client-package"
|
||||
const allowed = [
|
||||
"x-auth-access-token",
|
||||
"x-auth-access-token-jwt",
|
||||
"x-client-package",
|
||||
];
|
||||
|
||||
if (!headers) return true;
|
||||
for (const header of headers.split(",")) {
|
||||
if (!allowed.includes(header.trim().toLowerCase())) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleGET = async (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
const fileID = url.searchParams.get("fileID");
|
||||
if (!fileID) return new Response(null, { status: 400 });
|
||||
|
||||
let accessToken = request.headers.get("X-Auth-Access-Token");
|
||||
if (accessToken === undefined) {
|
||||
console.warn("Using deprecated accessToken query param");
|
||||
accessToken = url.searchParams.get("accessToken");
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
console.error("No accessToken provided");
|
||||
// return new Response(null, { status: 400 });
|
||||
}
|
||||
|
||||
let accessTokenJWT = request.headers.get("X-Auth-Access-Token-JWT");
|
||||
if (accessTokenJWT === undefined) {
|
||||
console.warn("Using deprecated accessTokenJWT query param");
|
||||
accessTokenJWT = url.searchParams.get("accessTokenJWT");
|
||||
}
|
||||
|
||||
const pathname = url.pathname;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (accessToken) params.set("accessToken", accessToken);
|
||||
if (accessTokenJWT) params.set("accessTokenJWT", accessTokenJWT);
|
||||
|
||||
let response = await fetch(
|
||||
`https://api.ente.io/public-collection/files${pathname}${fileID}?${params.toString()}`
|
||||
);
|
||||
response = new Response(response.body, response);
|
||||
response.headers.set("Access-Control-Allow-Origin", "*");
|
||||
return response;
|
||||
};
|
||||
1
infra/workers/public-albums/tsconfig.json
Normal file
1
infra/workers/public-albums/tsconfig.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "extends": "../tsconfig.base.json", "include": ["src"] }
|
||||
11
infra/workers/public-albums/wrangler.toml
Normal file
11
infra/workers/public-albums/wrangler.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
name = "public-albums"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-06-14"
|
||||
|
||||
routes = [
|
||||
{ pattern = "public-albums.ente.io", custom_domain = true }
|
||||
]
|
||||
|
||||
tail_consumers = [
|
||||
{ service = "tail" }
|
||||
]
|
||||
10
infra/workers/tail/package.json
Normal file
10
infra/workers/tail/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "tail",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240614.0",
|
||||
"typescript": "^5",
|
||||
"wrangler": "^3"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
72
infra/workers/tail/src/index.ts
Normal file
72
infra/workers/tail/src/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* A tail worker that forwards all `console.log` (and siblings) to Loki.
|
||||
*
|
||||
* https://developers.cloudflare.com/workers/observability/logging/tail-workers/
|
||||
*/
|
||||
export default {
|
||||
async tail(events: TraceItem[], env: Env) {
|
||||
// If the tail worker itself throws an exception (it shouldn't, unless
|
||||
// Loki is down), we don't catch it so that it counts as an "error" in
|
||||
// the worker stats.
|
||||
await handleTail(events, env);
|
||||
},
|
||||
} satisfies ExportedHandler<Env>;
|
||||
|
||||
interface Env {
|
||||
/** The URL of the Loki instance to push logs to. */
|
||||
LOKI_PUSH_URL: string;
|
||||
/**
|
||||
* The value of the "Basic" authorization.
|
||||
*
|
||||
* [Note: HTTP basic authorization in worker fetch]
|
||||
*
|
||||
* Usually a Loki push URL is specified with the credentials inline, say
|
||||
* `http://user:pass@loki/path`. However, I cannot get that to work with the
|
||||
* `fetch` inside a Cloudflare worker. Instead, the credentials need to be
|
||||
* separately provided as the Authorization header of the form:
|
||||
*
|
||||
* Authorization: Basic ${btoa(user:pass)}
|
||||
*
|
||||
* The LOKI_AUTH secret is the "${btoa(user:pass)}" value.
|
||||
*/
|
||||
LOKI_AUTH: string;
|
||||
}
|
||||
|
||||
const handleTail = async (events: TraceItem[], env: Env) => {
|
||||
for (const event of events.filter(hasLogOrException))
|
||||
await pushLogLine(Date.now(), JSON.stringify(event), env);
|
||||
};
|
||||
|
||||
/** Return true if the {@link event} has at least one log or exception. */
|
||||
const hasLogOrException = (event: TraceItem) =>
|
||||
event.logs.length ?? event.exceptions.length;
|
||||
|
||||
/**
|
||||
* Send a log entry to (Grafana) Loki
|
||||
*
|
||||
* For more details about the protocol, see
|
||||
* https://grafana.com/docs/loki/latest/reference/loki-http-api/#ingest-logs
|
||||
*
|
||||
* @param timestampMs Unix epoch (in milliseconds) when the event occurred.
|
||||
*
|
||||
* @param logLine The message to log.
|
||||
*
|
||||
* @param env The worker environment; we need it for the Loki URL and
|
||||
* credentials.
|
||||
*/
|
||||
const pushLogLine = async (timestampMs: number, logLine: string, env: Env) =>
|
||||
await fetch(env.LOKI_PUSH_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Basic ${env.LOKI_AUTH}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
streams: [
|
||||
{
|
||||
stream: { job: "worker" },
|
||||
values: [[`${timestampMs * 1e6}`, logLine]],
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
1
infra/workers/tail/tsconfig.json
Normal file
1
infra/workers/tail/tsconfig.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "extends": "../tsconfig.base.json", "include": ["src"] }
|
||||
11
infra/workers/tail/wrangler.toml
Normal file
11
infra/workers/tail/wrangler.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
name = "tail"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-06-14"
|
||||
|
||||
# Disable the default route, this worker does not handle fetch.
|
||||
workers_dev = false
|
||||
|
||||
[vars]
|
||||
# Added as a secret via the Cloudflare dashboard
|
||||
# LOKI_PUSH_URL = "https://${loki_base_url}>/loki/api/v1/push"
|
||||
# LOKI_AUTH = "${btoa(user:pass)}"
|
||||
10
infra/workers/thumbnails/package.json
Normal file
10
infra/workers/thumbnails/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "thumbnails",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240614.0",
|
||||
"typescript": "^5",
|
||||
"wrangler": "^3"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
89
infra/workers/thumbnails/src/index.ts
Normal file
89
infra/workers/thumbnails/src/index.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/** Proxy requests for thumbnails. */
|
||||
|
||||
export default {
|
||||
async fetch(request: Request) {
|
||||
switch (request.method) {
|
||||
case "OPTIONS":
|
||||
return handleOPTIONS(request);
|
||||
case "GET":
|
||||
return handleGET(request);
|
||||
default:
|
||||
console.log(`Unsupported HTTP method ${request.method}`);
|
||||
return new Response(null, { status: 405 });
|
||||
}
|
||||
},
|
||||
} satisfies ExportedHandler;
|
||||
|
||||
const handleOPTIONS = (request: Request) => {
|
||||
const origin = request.headers.get("Origin");
|
||||
if (!isAllowedOrigin(origin)) console.warn("Unknown origin", origin);
|
||||
const headers = request.headers.get("Access-Control-Request-Headers");
|
||||
if (!areAllowedHeaders(headers))
|
||||
console.warn("Unknown header in list", headers);
|
||||
return new Response("", {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
// "Access-Control-Allow-Headers": "X-Auth-Token, X-Client-Package",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isAllowedOrigin = (origin: string | null) => {
|
||||
const desktopApp = "ente://app";
|
||||
const allowedHostnames = [
|
||||
"web.ente.io",
|
||||
"photos.ente.io",
|
||||
"photos.ente.sh",
|
||||
"localhost",
|
||||
];
|
||||
|
||||
if (!origin) return false;
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
return origin == desktopApp || allowedHostnames.includes(url.hostname);
|
||||
} catch {
|
||||
// origin is likely an invalid URL
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const areAllowedHeaders = (headers: string | null) => {
|
||||
const allowed = ["x-auth-token", "x-client-package"];
|
||||
|
||||
if (!headers) return true;
|
||||
for (const header of headers.split(",")) {
|
||||
if (!allowed.includes(header.trim().toLowerCase())) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleGET = async (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
const fileID = url.searchParams.get("fileID");
|
||||
if (!fileID) return new Response(null, { status: 400 });
|
||||
|
||||
let token = request.headers.get("X-Auth-Token");
|
||||
if (!token) {
|
||||
console.warn("Using deprecated token query param");
|
||||
token = url.searchParams.get("token");
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.error("No token provided");
|
||||
// return new Response(null, { status: 400 });
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (token) params.set("token", token);
|
||||
|
||||
let response = await fetch(
|
||||
`https://api.ente.io/files/preview/${fileID}?${params.toString()}`
|
||||
);
|
||||
response = new Response(response.body, response);
|
||||
response.headers.set("Access-Control-Allow-Origin", "*");
|
||||
return response;
|
||||
};
|
||||
1
infra/workers/thumbnails/tsconfig.json
Normal file
1
infra/workers/thumbnails/tsconfig.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "extends": "../tsconfig.base.json", "include": ["src"] }
|
||||
11
infra/workers/thumbnails/wrangler.toml
Normal file
11
infra/workers/thumbnails/wrangler.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
name = "thumbnails"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-06-14"
|
||||
|
||||
routes = [
|
||||
{ pattern = "thumbnails.ente.io", custom_domain = true }
|
||||
]
|
||||
|
||||
tail_consumers = [
|
||||
{ service = "tail" }
|
||||
]
|
||||
@@ -3,30 +3,30 @@
|
||||
/* TSConfig docs: https://aka.ms/tsconfig.json */
|
||||
"compilerOptions": {
|
||||
/* tsc is used for by us for type checking, not compilation (the
|
||||
Cloudflare workers runtime natively supports TypeScript) */
|
||||
Cloudflare workers runtime natively supports TypeScript). */
|
||||
"noEmit": true,
|
||||
|
||||
/* The Workers runtime supports the latest and greatest */
|
||||
/* The Workers runtime supports the latest and greatest. */
|
||||
/* https://developers.cloudflare.com/workers/reference/languages/#javascript--typescript */
|
||||
"lib": ["esnext"],
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
|
||||
/* Types that are implicitly available */
|
||||
/* Types that are implicitly available. */
|
||||
/* https://www.npmjs.com/package/@cloudflare/workers-types */
|
||||
"types": ["@cloudflare/workers-types"],
|
||||
|
||||
/* Tell TypeScript how to lookup the file for a given import */
|
||||
/* Tell TypeScript how to lookup the file for a given import. */
|
||||
"moduleResolution": "node",
|
||||
|
||||
/* Speed things up by not type checking `node_modules` */
|
||||
/* Speed things up by not type checking `node_modules`. */
|
||||
"skipLibCheck": true,
|
||||
/* Require the `type` modifier when importing types */
|
||||
/* Require the `type` modifier when importing types. */
|
||||
"verbatimModuleSyntax": true,
|
||||
/* Enable importing .json files */
|
||||
/* Enable importing .json files. */
|
||||
"resolveJsonModule": true,
|
||||
|
||||
/* strict and then some */
|
||||
/* strict, and then some. */
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedParameters": true,
|
||||
|
||||
10
infra/workers/uploader/package.json
Normal file
10
infra/workers/uploader/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "uploader",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240614.0",
|
||||
"typescript": "^5",
|
||||
"wrangler": "^3"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
123
infra/workers/uploader/src/index.ts
Normal file
123
infra/workers/uploader/src/index.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Proxy file uploads.
|
||||
*
|
||||
* See: https://ente.io/blog/tech/making-uploads-faster/
|
||||
*/
|
||||
|
||||
export default {
|
||||
async fetch(request: Request) {
|
||||
switch (request.method) {
|
||||
case "OPTIONS":
|
||||
return handleOPTIONS(request);
|
||||
case "POST":
|
||||
return handlePOSTOrPUT(request);
|
||||
case "PUT":
|
||||
return handlePOSTOrPUT(request);
|
||||
default:
|
||||
console.log(`Unsupported HTTP method ${request.method}`);
|
||||
return new Response(null, { status: 405 });
|
||||
}
|
||||
},
|
||||
} satisfies ExportedHandler;
|
||||
|
||||
const handleOPTIONS = (request: Request) => {
|
||||
const origin = request.headers.get("Origin");
|
||||
if (!isAllowedOrigin(origin)) console.warn("Unknown origin", origin);
|
||||
const headers = request.headers.get("Access-Control-Request-Headers");
|
||||
if (!areAllowedHeaders(headers))
|
||||
console.warn("Unknown header in list", headers);
|
||||
return new Response("", {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, PUT, OPTIONS",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
// "Access-Control-Allow-Headers": "Content-Type", "UPLOAD-URL, X-Client-Package",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
"Access-Control-Expose-Headers": "X-Request-Id, CF-Ray",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isAllowedOrigin = (origin: string | null) => {
|
||||
const desktopApp = "ente://app";
|
||||
const allowedHostnames = [
|
||||
"web.ente.io",
|
||||
"photos.ente.io",
|
||||
"photos.ente.sh",
|
||||
"localhost",
|
||||
];
|
||||
|
||||
if (!origin) return false;
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
return origin == desktopApp || allowedHostnames.includes(url.hostname);
|
||||
} catch {
|
||||
// origin is likely an invalid URL
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const areAllowedHeaders = (headers: string | null) => {
|
||||
const allowed = ["Content-Type", "UPLOAD-URL", "X-Client-Package"];
|
||||
|
||||
if (!headers) return true;
|
||||
for (const header of headers.split(",")) {
|
||||
if (!allowed.includes(header.trim().toLowerCase())) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handlePOSTOrPUT = async (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
const uploadURL = request.headers.get("UPLOAD-URL");
|
||||
if (!uploadURL) {
|
||||
console.error("No uploadURL provided");
|
||||
return new Response(null, { status: 400 });
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
switch (url.pathname) {
|
||||
case "/file-upload":
|
||||
response = await fetch(uploadURL, {
|
||||
method: request.method,
|
||||
body: request.body,
|
||||
});
|
||||
break;
|
||||
case "/multipart-upload":
|
||||
response = await fetch(uploadURL, {
|
||||
method: request.method,
|
||||
body: request.body,
|
||||
});
|
||||
if (response.ok) {
|
||||
const etag = response.headers.get("etag");
|
||||
if (etag === null) {
|
||||
console.log("No etag in response", response);
|
||||
response = new Response(null, { status: 500 });
|
||||
} else {
|
||||
response = new Response(JSON.stringify({ etag }));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "/multipart-complete":
|
||||
response = await fetch(uploadURL, {
|
||||
method: request.method,
|
||||
body: request.body,
|
||||
headers: {
|
||||
"Content-Type": "text/xml",
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
response = new Response(null, { status: 404 });
|
||||
break;
|
||||
}
|
||||
|
||||
response = new Response(response.body, response);
|
||||
response.headers.set("Access-Control-Allow-Origin", "*");
|
||||
response.headers.set(
|
||||
"Access-Control-Expose-Headers",
|
||||
"X-Request-Id, CF-Ray"
|
||||
);
|
||||
return response;
|
||||
};
|
||||
1
infra/workers/uploader/tsconfig.json
Normal file
1
infra/workers/uploader/tsconfig.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "extends": "../tsconfig.base.json", "include": ["src"] }
|
||||
7
infra/workers/uploader/wrangler.toml
Normal file
7
infra/workers/uploader/wrangler.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
name = "uploader"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-06-14"
|
||||
|
||||
routes = [{ pattern = "uploader.ente.io", custom_domain = true }]
|
||||
|
||||
tail_consumers = [{ service = "tail" }]
|
||||
@@ -1,5 +1,5 @@
|
||||
<resources>
|
||||
<string name="app_name">ente Photos</string>
|
||||
<string name="app_name">Ente Photos</string>
|
||||
<string name="backup">Backup</string>
|
||||
<string name="asset_statements" translatable="false">
|
||||
[{
|
||||
|
||||
@@ -1 +1 @@
|
||||
ente Photos
|
||||
Ente Photos
|
||||
|
||||
@@ -1 +1 @@
|
||||
ente Photos
|
||||
Ente Photos
|
||||
|
||||
@@ -1 +1 @@
|
||||
ente Photos
|
||||
Ente Photos
|
||||
|
||||
@@ -1 +1 @@
|
||||
ente Photos
|
||||
Ente Photos
|
||||
|
||||
@@ -1 +1 @@
|
||||
ente Photos
|
||||
Ente Photos
|
||||
|
||||
@@ -1 +1 @@
|
||||
ente Photos
|
||||
Ente Photos
|
||||
|
||||
@@ -1 +1 @@
|
||||
ente Photos
|
||||
Ente Photos
|
||||
@@ -1 +1 @@
|
||||
ente Photos
|
||||
Ente Photos
|
||||
@@ -1 +1 @@
|
||||
ente Photos
|
||||
Ente Photos
|
||||
@@ -1 +1 @@
|
||||
ente Photos
|
||||
Ente Photos
|
||||
@@ -1 +1 @@
|
||||
ente Photos
|
||||
Ente Photos
|
||||
@@ -1 +1 @@
|
||||
ente Photos
|
||||
Ente Photos
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>ente Photos</string>
|
||||
<string>Ente Photos</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@@ -340,21 +340,31 @@ class UserService {
|
||||
await dialog.show();
|
||||
try {
|
||||
final userPassword = Configuration.instance.getVolatilePassword();
|
||||
if (userPassword == null) throw Exception("volatile password is null");
|
||||
|
||||
await _saveConfiguration(response);
|
||||
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
||||
userPassword,
|
||||
Configuration.instance.getKeyAttributes()!,
|
||||
if (userPassword == null) {
|
||||
await dialog.hide();
|
||||
// ignore: unawaited_futures
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const PasswordReentryPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
throw Exception("unexpected response during passkey verification");
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
||||
userPassword,
|
||||
Configuration.instance.getKeyAttributes()!,
|
||||
);
|
||||
} else {
|
||||
throw Exception("unexpected response during passkey verification");
|
||||
}
|
||||
await dialog.hide();
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
Bus.instance.fire(AccountConfiguredEvent());
|
||||
}
|
||||
await dialog.hide();
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
Bus.instance.fire(AccountConfiguredEvent());
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
await dialog.hide();
|
||||
@@ -384,10 +394,14 @@ class UserService {
|
||||
await dialog.hide();
|
||||
if (response.statusCode == 200) {
|
||||
Widget page;
|
||||
final String passkeySessionID = response.data["passkeySessionID"];
|
||||
final String twoFASessionID = response.data["twoFactorSessionID"];
|
||||
|
||||
if (twoFASessionID.isNotEmpty) {
|
||||
await setTwoFactor(value: true);
|
||||
page = TwoFactorAuthenticationPage(twoFASessionID);
|
||||
} else if (passkeySessionID.isNotEmpty) {
|
||||
page = PasskeyPage(passkeySessionID);
|
||||
} else {
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
|
||||
@@ -12,7 +12,7 @@ description: ente photos application
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.8.138+658
|
||||
version: 0.9.1+901
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
||||
@@ -50,7 +50,7 @@ const (
|
||||
SubscriptionEndedEmailTemplate = "subscription_ended.html"
|
||||
|
||||
// Subject for `SubscriptionEndedEmailTemplate`.
|
||||
SubscriptionEndedEmailSubject = "Your subscription to ente Photos has ended"
|
||||
SubscriptionEndedEmailSubject = "Your subscription to Ente Photos has ended"
|
||||
)
|
||||
|
||||
// PaymentProvider represents the payment provider via which a purchase was made
|
||||
|
||||
@@ -206,7 +206,7 @@
|
||||
style="font-family: inherit; text-align: inherit">
|
||||
<span
|
||||
style="font-family: helvetica, sans-serif">Your subscription to
|
||||
ente Photos has ended. Thank you for trying out ente.</span>
|
||||
Ente Photos has ended. Thank you for trying out ente.</span>
|
||||
</div>
|
||||
<div
|
||||
style="font-family: inherit; text-align: inherit">
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 422 B After Width: | Height: | Size: 1.1 KiB |
@@ -6,17 +6,20 @@ import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import EnteButton from "@ente/shared/components/EnteButton";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
import KeyIcon from "@mui/icons-material/Key";
|
||||
import { Paper, Typography, styled } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
beginPasskeyAuthentication,
|
||||
finishPasskeyAuthentication,
|
||||
isWebAuthnSupported,
|
||||
isWhitelistedRedirect,
|
||||
passkeyAuthenticationSuccessRedirectURL,
|
||||
passkeySessionAlreadyClaimedErrorMessage,
|
||||
redirectToPasskeyRecoverPage,
|
||||
signChallenge,
|
||||
type BeginPasskeyAuthenticationResponse,
|
||||
} from "services/passkey";
|
||||
|
||||
const Page = () => {
|
||||
@@ -29,14 +32,39 @@ const Page = () => {
|
||||
| "loading" /* Can happen multiple times in the flow */
|
||||
| "webAuthnNotSupported" /* Unrecoverable error */
|
||||
| "unknownRedirect" /* Unrecoverable error */
|
||||
| "unrecoverableFailure" /* Unrocevorable error - generic */
|
||||
| "failed" /* Recoverable error */
|
||||
| "sessionAlreadyClaimed" /* Unrecoverable error */
|
||||
| "unrecoverableFailure" /* Unrecoverable error - generic */
|
||||
| "failedDuringSignChallenge" /* Recoverable error in signChallenge */
|
||||
| "failed" /* Recoverable error otherwise */
|
||||
| "needUserFocus" /* See docs for `Continuation` */
|
||||
| "waitingForUser" /* ...to authenticate with their passkey */
|
||||
| "redirectingWeb" /* Redirect back to the requesting app (HTTP) */
|
||||
| "redirectingApp"; /* Other redirects (mobile / desktop redirect) */
|
||||
|
||||
const [status, setStatus] = useState<Status>("loading");
|
||||
|
||||
/**
|
||||
* Safari keeps on saying "NotAllowedError: The document is not focused"
|
||||
* even though it just opened the page and brought it to the front.
|
||||
*
|
||||
* Because of their incompetence, we need to break our entire flow into two
|
||||
* parts, and stash away a lot of state when we're in the "needUserFocus"
|
||||
* state.
|
||||
*/
|
||||
interface Continuation {
|
||||
redirectURL: URL;
|
||||
clientPackage: string;
|
||||
passkeySessionID: string;
|
||||
beginResponse: BeginPasskeyAuthenticationResponse;
|
||||
}
|
||||
const [continuation, setContinuation] = useState<
|
||||
Continuation | undefined
|
||||
>();
|
||||
|
||||
// Safari throws sometimes
|
||||
// (no reason, just to show their incompetence). The retry doesn't seem to
|
||||
// help mostly, but cargo cult anyway.
|
||||
|
||||
// The URL we're redirecting to on success.
|
||||
//
|
||||
// This will only be set when status is "redirecting*".
|
||||
@@ -44,8 +72,8 @@ const Page = () => {
|
||||
URL | undefined
|
||||
>();
|
||||
|
||||
/** (re)start the authentication flow */
|
||||
const authenticate = async () => {
|
||||
/** Phase 1 of {@link authenticate}. */
|
||||
const authenticateBegin = useCallback(async () => {
|
||||
if (!isWebAuthnSupported()) {
|
||||
setStatus("webAuthnNotSupported");
|
||||
return;
|
||||
@@ -86,21 +114,64 @@ const Page = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
let authorizationResponse: TwoFactorAuthorizationResponse;
|
||||
let beginResponse: BeginPasskeyAuthenticationResponse;
|
||||
try {
|
||||
const { ceremonySessionID, options } =
|
||||
await beginPasskeyAuthentication(passkeySessionID);
|
||||
beginResponse = await beginPasskeyAuthentication(passkeySessionID);
|
||||
} catch (e) {
|
||||
log.error("Failed to begin passkey authentication", e);
|
||||
setStatus(
|
||||
e instanceof Error &&
|
||||
e.message == passkeySessionAlreadyClaimedErrorMessage
|
||||
? "sessionAlreadyClaimed"
|
||||
: "failed",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("waitingForUser");
|
||||
return {
|
||||
redirectURL,
|
||||
passkeySessionID,
|
||||
clientPackage,
|
||||
beginResponse,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const credential = await signChallenge(options.publicKey);
|
||||
/**
|
||||
* Phase 2 of {@link authenticate}, separated by a potential user
|
||||
* interaction.
|
||||
*/
|
||||
const authenticateContinue = useCallback(async (cont: Continuation) => {
|
||||
const { redirectURL, passkeySessionID, clientPackage, beginResponse } =
|
||||
cont;
|
||||
const { ceremonySessionID, options } = beginResponse;
|
||||
|
||||
setStatus("waitingForUser");
|
||||
|
||||
let credential: Credential | undefined;
|
||||
try {
|
||||
credential = await signChallenge(options.publicKey);
|
||||
if (!credential) {
|
||||
setStatus("failed");
|
||||
setStatus("failedDuringSignChallenge");
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("Failed to get credentials", e);
|
||||
if (
|
||||
e instanceof Error &&
|
||||
e.name == "NotAllowedError" &&
|
||||
e.message == "The document is not focused."
|
||||
) {
|
||||
setStatus("needUserFocus");
|
||||
} else {
|
||||
setStatus("failedDuringSignChallenge");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("loading");
|
||||
setStatus("loading");
|
||||
|
||||
let authorizationResponse: TwoFactorAuthorizationResponse;
|
||||
try {
|
||||
authorizationResponse = await finishPasskeyAuthentication({
|
||||
passkeySessionID,
|
||||
ceremonySessionID,
|
||||
@@ -108,7 +179,7 @@ const Page = () => {
|
||||
credential,
|
||||
});
|
||||
} catch (e) {
|
||||
log.error("Passkey authentication failed", e);
|
||||
log.error("Failed to finish passkey authentication", e);
|
||||
setStatus("failed");
|
||||
return;
|
||||
}
|
||||
@@ -122,16 +193,27 @@ const Page = () => {
|
||||
authorizationResponse,
|
||||
),
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/** (re)start the authentication flow */
|
||||
const authenticate = useCallback(async () => {
|
||||
const cont = await authenticateBegin();
|
||||
if (cont) {
|
||||
setContinuation(cont);
|
||||
await authenticateContinue(cont);
|
||||
}
|
||||
}, [authenticateBegin, authenticateContinue]);
|
||||
|
||||
useEffect(() => {
|
||||
void authenticate();
|
||||
}, []);
|
||||
}, [authenticate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (successRedirectURL) redirectToURL(successRedirectURL);
|
||||
}, [successRedirectURL]);
|
||||
|
||||
const handleVerify = () => void authenticateContinue(ensure(continuation));
|
||||
|
||||
const handleRetry = () => void authenticate();
|
||||
|
||||
const handleRecover = (() => {
|
||||
@@ -156,10 +238,19 @@ const Page = () => {
|
||||
loading: <Loading />,
|
||||
unknownRedirect: <UnknownRedirect />,
|
||||
webAuthnNotSupported: <WebAuthnNotSupported />,
|
||||
sessionAlreadyClaimed: <SessionAlreadyClaimed />,
|
||||
unrecoverableFailure: <UnrecoverableFailure />,
|
||||
failedDuringSignChallenge: (
|
||||
<RetriableFailed
|
||||
duringSignChallenge
|
||||
onRetry={handleRetry}
|
||||
onRecover={handleRecover}
|
||||
/>
|
||||
),
|
||||
failed: (
|
||||
<RetriableFailed onRetry={handleRetry} onRecover={handleRecover} />
|
||||
),
|
||||
needUserFocus: <Verify onVerify={handleVerify} />,
|
||||
waitingForUser: <WaitingForUser />,
|
||||
redirectingWeb: <RedirectingWeb />,
|
||||
redirectingApp: <RedirectingApp onRetry={handleRedirectAgain} />,
|
||||
@@ -194,6 +285,26 @@ const WebAuthnNotSupported: React.FC = () => {
|
||||
return <Failed message={t("passkeys_not_supported")} />;
|
||||
};
|
||||
|
||||
const SessionAlreadyClaimed: React.FC = () => {
|
||||
return (
|
||||
<Content>
|
||||
<SessionAlreadyClaimed_>
|
||||
<InfoIcon color="secondary" />
|
||||
<Typography>
|
||||
{t("passkey_login_already_claimed_session")}
|
||||
</Typography>
|
||||
</SessionAlreadyClaimed_>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
const SessionAlreadyClaimed_ = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
`;
|
||||
|
||||
const UnrecoverableFailure: React.FC = () => {
|
||||
return <Failed message={t("passkey_login_generic_error")} />;
|
||||
};
|
||||
@@ -237,7 +348,47 @@ const ContentPaper = styled(Paper)`
|
||||
gap: 1rem;
|
||||
`;
|
||||
|
||||
interface VerifyProps {
|
||||
/** Called when the user presses the "Verify" button. */
|
||||
onVerify: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gain focus for the current page by requesting the user to explicitly click a
|
||||
* button. For more details, see the documentation for `Continuation`.
|
||||
*/
|
||||
const Verify: React.FC<VerifyProps> = ({ onVerify }) => {
|
||||
return (
|
||||
<Content>
|
||||
<KeyIcon color="secondary" fontSize="large" />
|
||||
<Typography variant="h3">{t("passkey")}</Typography>
|
||||
<Typography color="text.muted">
|
||||
{t("passkey_verify_description")}
|
||||
</Typography>
|
||||
<ButtonStack>
|
||||
<EnteButton
|
||||
onClick={onVerify}
|
||||
fullWidth
|
||||
color="accent"
|
||||
type="button"
|
||||
variant="contained"
|
||||
>
|
||||
{t("VERIFY")}
|
||||
</EnteButton>
|
||||
</ButtonStack>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
interface RetriableFailedProps {
|
||||
/**
|
||||
* Set this attribute to indicate that this failure occurred during the
|
||||
* actual passkey verification (`navigator.credentials.get`).
|
||||
*
|
||||
* We customize the error message for such cases to give a hint to the user
|
||||
* that they can try on their other devices too.
|
||||
*/
|
||||
duringSignChallenge?: boolean;
|
||||
/** Callback invoked when the user presses the try again button. */
|
||||
onRetry: () => void;
|
||||
/**
|
||||
@@ -251,6 +402,7 @@ interface RetriableFailedProps {
|
||||
}
|
||||
|
||||
const RetriableFailed: React.FC<RetriableFailedProps> = ({
|
||||
duringSignChallenge,
|
||||
onRetry,
|
||||
onRecover,
|
||||
}) => {
|
||||
@@ -259,7 +411,9 @@ const RetriableFailed: React.FC<RetriableFailedProps> = ({
|
||||
<InfoIcon color="secondary" fontSize="large" />
|
||||
<Typography variant="h3">{t("passkey_login_failed")}</Typography>
|
||||
<Typography color="text.muted">
|
||||
{t("passkey_login_generic_error")}
|
||||
{duringSignChallenge
|
||||
? t("passkey_login_credential_hint")
|
||||
: t("passkey_login_generic_error")}
|
||||
</Typography>
|
||||
<ButtonStack>
|
||||
<EnteButton
|
||||
@@ -339,8 +493,6 @@ interface RedirectingAppProps {
|
||||
}
|
||||
|
||||
const RedirectingApp: React.FC<RedirectingAppProps> = ({ onRetry }) => {
|
||||
const handleClose = window.close;
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<InfoIcon color="accent" fontSize="large" />
|
||||
@@ -352,15 +504,6 @@ const RedirectingApp: React.FC<RedirectingAppProps> = ({ onRetry }) => {
|
||||
{t("redirect_close_instructions")}
|
||||
</Typography>
|
||||
<ButtonStack>
|
||||
<EnteButton
|
||||
onClick={handleClose}
|
||||
fullWidth
|
||||
color="secondary"
|
||||
type="button"
|
||||
variant="contained"
|
||||
>
|
||||
{t("CLOSE")}
|
||||
</EnteButton>
|
||||
<EnteButton
|
||||
onClick={onRetry}
|
||||
fullWidth
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { isDevBuild } from "@/next/env";
|
||||
import log from "@/next/log";
|
||||
import { clientPackageName } from "@/next/types/app";
|
||||
import { TwoFactorAuthorizationResponse } from "@/next/types/credentials";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { wait } from "@/utils/promise";
|
||||
import { nullToUndefined } from "@/utils/transform";
|
||||
import {
|
||||
fromB64URLSafeNoPadding,
|
||||
@@ -370,6 +368,13 @@ export interface BeginPasskeyAuthenticationResponse {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The passkey session which we are trying to start an authentication ceremony
|
||||
* for has already finished elsewhere.
|
||||
*/
|
||||
export const passkeySessionAlreadyClaimedErrorMessage =
|
||||
"Passkey session already claimed";
|
||||
|
||||
/**
|
||||
* Create a authentication ceremony session and return a challenge and a list of
|
||||
* public key credentials that can be used to attest that challenge.
|
||||
@@ -381,6 +386,9 @@ export interface BeginPasskeyAuthenticationResponse {
|
||||
*
|
||||
* @param passkeySessionID A session created by the requesting app that can be
|
||||
* used to initiate a passkey authentication ceremony on the accounts app.
|
||||
*
|
||||
* @throws In addition to arbitrary errors, it throws errors with the message
|
||||
* {@link passkeySessionAlreadyClaimedErrorMessage}.
|
||||
*/
|
||||
export const beginPasskeyAuthentication = async (
|
||||
passkeySessionID: string,
|
||||
@@ -390,7 +398,11 @@ export const beginPasskeyAuthentication = async (
|
||||
method: "POST",
|
||||
body: JSON.stringify({ sessionID: passkeySessionID }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
if (!res.ok) {
|
||||
if (res.status == 409)
|
||||
throw new Error(passkeySessionAlreadyClaimedErrorMessage);
|
||||
throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
// See: [Note: Converting binary data in WebAuthn API payloads]
|
||||
|
||||
@@ -424,30 +436,7 @@ export const beginPasskeyAuthentication = async (
|
||||
*/
|
||||
export const signChallenge = async (
|
||||
publicKey: PublicKeyCredentialRequestOptions,
|
||||
) => {
|
||||
const go = async () => await navigator.credentials.get({ publicKey });
|
||||
|
||||
try {
|
||||
return await go();
|
||||
} catch (e) {
|
||||
// Safari throws "NotAllowedError: The document is not focused" for the
|
||||
// first request sometimes (no reason, just to show their incompetence).
|
||||
// "NotAllowedError" is also the error name that is thrown when the user
|
||||
// explicitly cancels, so we can't even filter it out by name and also
|
||||
// to do a message match.
|
||||
if (
|
||||
e instanceof Error &&
|
||||
e.name == "NotAllowedError" &&
|
||||
e.message == "The document is not focused."
|
||||
) {
|
||||
log.warn("Working around Safari bug by retrying after failure", e);
|
||||
await wait(2000);
|
||||
return await go();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
) => nullToUndefined(await navigator.credentials.get({ publicKey }));
|
||||
|
||||
interface FinishPasskeyAuthenticationOptions {
|
||||
passkeySessionID: string;
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
HorizontalFlex,
|
||||
VerticallyCentered,
|
||||
} from "@ente/shared/components/Container";
|
||||
import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
|
||||
import { EnteLogo } from "@ente/shared/components/EnteLogo";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import { sessionExpiredDialogAttributes } from "@ente/shared/components/LoginComponents";
|
||||
import NavbarBase from "@ente/shared/components/Navbar/base";
|
||||
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
|
||||
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
|
||||
@@ -140,19 +140,6 @@ const Page: React.FC = () => {
|
||||
|
||||
export default Page;
|
||||
|
||||
const sessionExpiredDialogAttributes = (
|
||||
action: () => void,
|
||||
): DialogBoxAttributesV2 => ({
|
||||
title: t("SESSION_EXPIRED"),
|
||||
content: t("SESSION_EXPIRED_MESSAGE"),
|
||||
nonClosable: true,
|
||||
proceed: {
|
||||
text: t("LOGIN"),
|
||||
action,
|
||||
variant: "accent",
|
||||
},
|
||||
});
|
||||
|
||||
const AuthNavbar: React.FC = () => {
|
||||
const { isMobile, logout } = ensure(useContext(AppContext));
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ export default function Index() {
|
||||
}, [publicKeyB64, privateKeyB64, pairingCode]);
|
||||
|
||||
const pollTick = async () => {
|
||||
if (!publicKeyB64 || !privateKeyB64 || !pairingCode) return;
|
||||
|
||||
const registration = { publicKeyB64, privateKeyB64, pairingCode };
|
||||
try {
|
||||
const data = await getCastData(registration);
|
||||
|
||||
@@ -9,9 +9,8 @@ import { isChromecast } from "services/chromecast";
|
||||
import { imageURLGenerator } from "services/render";
|
||||
|
||||
export default function Slideshow() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [imageURL, setImageURL] = useState<string | undefined>();
|
||||
const [isEmpty, setIsEmpty] = useState(false);
|
||||
const [imageURL, setImageURL] = useState<string | undefined>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -35,7 +34,6 @@ export default function Slideshow() {
|
||||
}
|
||||
|
||||
setImageURL(url);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("Failed to prepare generator", e);
|
||||
@@ -50,8 +48,8 @@ export default function Slideshow() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (loading) return <PairingComplete />;
|
||||
if (isEmpty) return <NoItems />;
|
||||
if (!imageURL) return <PairingComplete />;
|
||||
|
||||
return isChromecast() ? (
|
||||
<SlideViewChromecast url={imageURL} />
|
||||
|
||||
@@ -19,8 +19,8 @@ export const storeCastData = (payload: unknown) => {
|
||||
// Iterate through all the keys of the payload object and save them to
|
||||
// localStorage. We don't validate here, we'll validate when we read these
|
||||
// values back in `readCastData`.
|
||||
for (const key in payload) {
|
||||
window.localStorage.setItem(key, payload[key]);
|
||||
for (const [key, value] of Object.entries(payload)) {
|
||||
window.localStorage.setItem(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ const advertiseCode = (cast: Cast) => {
|
||||
const collectionID =
|
||||
data &&
|
||||
typeof data == "object" &&
|
||||
"collectionID" in data &&
|
||||
typeof data["collectionID"] == "string"
|
||||
? data["collectionID"]
|
||||
: undefined;
|
||||
|
||||
@@ -13,7 +13,7 @@ import FileType from "file-type";
|
||||
* For the list of returned extensions, see (for our installed version):
|
||||
* https://github.com/sindresorhus/file-type/blob/main/core.d.ts
|
||||
*/
|
||||
export const detectMediaMIMEType = async (file: File): Promise<string> => {
|
||||
export const detectMediaMIMEType = async (file: File) => {
|
||||
const chunkSizeForTypeDetection = 4100;
|
||||
const fileChunk = file.slice(0, chunkSizeForTypeDetection);
|
||||
const chunk = new Uint8Array(await fileChunk.arrayBuffer());
|
||||
|
||||
@@ -81,7 +81,7 @@ export const register = async (): Promise<Registration> => {
|
||||
await generateKeyPair();
|
||||
|
||||
// Register keypair with museum to get a pairing code.
|
||||
let pairingCode: string;
|
||||
let pairingCode: string | undefined;
|
||||
// eslint has fixed this spurious warning, but we're not on the latest
|
||||
// version yet, so add a disable.
|
||||
// https://github.com/eslint/eslint/pull/18286
|
||||
|
||||
@@ -7,6 +7,7 @@ import { nameAndExtension } from "@/next/file";
|
||||
import log from "@/next/log";
|
||||
import type { ComlinkWorker } from "@/next/worker/comlink-worker";
|
||||
import { shuffled } from "@/utils/array";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { wait } from "@/utils/promise";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { ApiError } from "@ente/shared/error";
|
||||
@@ -15,7 +16,7 @@ import { apiOrigin, customAPIOrigin } from "@ente/shared/network/api";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { CastData } from "services/cast-data";
|
||||
import { detectMediaMIMEType } from "services/detect-type";
|
||||
import {
|
||||
import type {
|
||||
EncryptedEnteFile,
|
||||
EnteFile,
|
||||
FileMagicMetadata,
|
||||
@@ -133,7 +134,7 @@ export const imageURLGenerator = async function* (castData: CastData) {
|
||||
// The last to last element is the one that was shown prior to that,
|
||||
// and now can be safely revoked.
|
||||
if (previousURLs.length > 1)
|
||||
URL.revokeObjectURL(previousURLs.shift());
|
||||
URL.revokeObjectURL(ensure(previousURLs.shift()));
|
||||
|
||||
previousURLs.push(url);
|
||||
|
||||
@@ -207,8 +208,8 @@ const decryptEnteFile = async (
|
||||
metadata.decryptionHeader,
|
||||
fileKey,
|
||||
);
|
||||
let fileMagicMetadata: FileMagicMetadata;
|
||||
let filePubMagicMetadata: FilePublicMagicMetadata;
|
||||
let fileMagicMetadata: FileMagicMetadata | undefined;
|
||||
let filePubMagicMetadata: FilePublicMagicMetadata | undefined;
|
||||
if (magicMetadata?.data) {
|
||||
fileMagicMetadata = {
|
||||
...encryptedFile.magicMetadata,
|
||||
@@ -242,6 +243,8 @@ const decryptEnteFile = async (
|
||||
if (file.pubMagicMetadata?.data.editedName) {
|
||||
file.metadata.title = file.pubMagicMetadata.data.editedName;
|
||||
}
|
||||
// @ts-expect-error TODO: The core types need to be updated to allow the
|
||||
// possibility of missing metadata fiels.
|
||||
return file;
|
||||
};
|
||||
|
||||
@@ -254,7 +257,7 @@ const isFileEligible = (file: EnteFile) => {
|
||||
// extension. To detect the actual type, we need to sniff the MIME type, but
|
||||
// that requires downloading and decrypting the file first.
|
||||
const [, extension] = nameAndExtension(file.metadata.title);
|
||||
if (isNonWebImageFileExtension(extension)) {
|
||||
if (extension && isNonWebImageFileExtension(extension)) {
|
||||
// Of the known non-web types, we support HEIC.
|
||||
return isHEICExtension(extension);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Metadata } from "@/media/types/file";
|
||||
import {
|
||||
import type {
|
||||
EncryptedMagicMetadata,
|
||||
MagicMetadataCore,
|
||||
VISIBILITY_STATE,
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"extends": "@/build-config/tsconfig-next.json",
|
||||
"compilerOptions": {
|
||||
/* Set the base directory from which to resolve bare module names */
|
||||
"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
|
||||
|
||||
/* TODO(MR): Enable this */
|
||||
"noUncheckedIndexedAccess": false,
|
||||
/* MUI doesn't play great with exactOptionalPropertyTypes currently. */
|
||||
"exactOptionalPropertyTypes": 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"]
|
||||
"src",
|
||||
"../../packages/next/global-electron.d.ts",
|
||||
"../../packages/shared/themes/mui-theme.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -528,13 +528,11 @@ const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
|
||||
label={t("TWO_FACTOR")}
|
||||
/>
|
||||
|
||||
{isInternalUserViaEmailCheck() && (
|
||||
<EnteMenuItem
|
||||
variant="secondary"
|
||||
onClick={redirectToAccountsPage}
|
||||
label={t("passkeys")}
|
||||
/>
|
||||
)}
|
||||
<EnteMenuItem
|
||||
variant="secondary"
|
||||
onClick={redirectToAccountsPage}
|
||||
label={t("passkeys")}
|
||||
/>
|
||||
|
||||
<EnteMenuItem
|
||||
variant="secondary"
|
||||
|
||||
@@ -67,10 +67,11 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>();
|
||||
const [user, setUser] = useState<User>();
|
||||
const [passkeyVerificationData, setPasskeyVerificationData] = useState<
|
||||
[string, string] | undefined
|
||||
{ passkeySessionID: string; url: string } | undefined
|
||||
>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
@@ -180,8 +181,8 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
appName,
|
||||
passkeySessionID,
|
||||
);
|
||||
setPasskeyVerificationData([passkeySessionID, url]);
|
||||
openPasskeyVerificationURL(passkeySessionID, url);
|
||||
setPasskeyVerificationData({ passkeySessionID, url });
|
||||
openPasskeyVerificationURL({ passkeySessionID, url });
|
||||
throw Error(CustomError.TWO_FACTOR_ENABLED);
|
||||
} else if (twoFactorSessionID) {
|
||||
const sessionKeyAttributes =
|
||||
@@ -295,11 +296,11 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
return (
|
||||
<VerifyingPasskey
|
||||
email={user?.email}
|
||||
passkeySessionID={passkeyVerificationData?.passkeySessionID}
|
||||
onRetry={() =>
|
||||
openPasskeyVerificationURL(...passkeyVerificationData)
|
||||
openPasskeyVerificationURL(passkeyVerificationData)
|
||||
}
|
||||
onRecover={() => router.push("/passkeys/recover")}
|
||||
onLogout={logout}
|
||||
appContext={appContext}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,6 +90,10 @@ const saveCredentialsAndNavigateTo = async (
|
||||
// - The plaintext "token" will be passed during fresh signups, where we
|
||||
// don't yet have keys to encrypt it, the account itself is being created
|
||||
// as we go through this flow.
|
||||
// TODO(MR): Conceptually this cannot happen. During a _real_ fresh signup
|
||||
// we'll never enter the passkey verification flow. Remove this code after
|
||||
// making sure that it doesn't get triggered in cases where an existing
|
||||
// user goes through the new user flow.
|
||||
//
|
||||
// - The encrypted `encryptedToken` will be present otherwise (i.e. if the
|
||||
// user is signing into an existing account).
|
||||
|
||||
@@ -42,7 +42,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [resend, setResend] = useState(0);
|
||||
const [passkeyVerificationData, setPasskeyVerificationData] = useState<
|
||||
[string, string] | undefined
|
||||
{ passkeySessionID: string; url: string } | undefined
|
||||
>();
|
||||
|
||||
const router = useRouter();
|
||||
@@ -98,8 +98,8 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
appName,
|
||||
passkeySessionID,
|
||||
);
|
||||
setPasskeyVerificationData([passkeySessionID, url]);
|
||||
openPasskeyVerificationURL(passkeySessionID, url);
|
||||
setPasskeyVerificationData({ passkeySessionID, url });
|
||||
openPasskeyVerificationURL({ passkeySessionID, url });
|
||||
} else if (twoFactorSessionID) {
|
||||
setData(LS_KEYS.USER, {
|
||||
email,
|
||||
@@ -193,11 +193,11 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
return (
|
||||
<VerifyingPasskey
|
||||
email={email}
|
||||
passkeySessionID={passkeyVerificationData?.passkeySessionID}
|
||||
onRetry={() =>
|
||||
openPasskeyVerificationURL(...passkeyVerificationData)
|
||||
openPasskeyVerificationURL(passkeyVerificationData)
|
||||
}
|
||||
onRecover={() => router.push("/passkeys/recover")}
|
||||
onLogout={logout}
|
||||
appContext={appContext}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { clientPackageHeaderIfPresent } from "@/next/http";
|
||||
import log from "@/next/log";
|
||||
import type { AppName } from "@/next/types/app";
|
||||
import { clientPackageName } from "@/next/types/app";
|
||||
import { TwoFactorAuthorizationResponse } from "@/next/types/credentials";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { getRecoveryKey } from "@ente/shared/crypto/helpers";
|
||||
import {
|
||||
@@ -10,6 +13,8 @@ import {
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { accountsAppURL, apiOrigin } from "@ente/shared/network/api";
|
||||
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
|
||||
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
|
||||
/**
|
||||
@@ -46,16 +51,23 @@ export const passkeyVerificationRedirectURL = (
|
||||
return `${accountsAppURL()}/passkeys/verify?${params.toString()}`;
|
||||
};
|
||||
|
||||
interface OpenPasskeyVerificationURLOptions {
|
||||
/**
|
||||
* The passkeySessionID for which we are redirecting.
|
||||
*
|
||||
* This is compared to the saved session id in the browser's session storage
|
||||
* to allow us to ignore redirects to the passkey flow finish page except
|
||||
* the ones for this specific session we're awaiting.
|
||||
*/
|
||||
passkeySessionID: string;
|
||||
/** The URL to redirect to or open in the system browser. */
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open or redirect to a passkey verification URL previously constructed using
|
||||
* {@link passkeyVerificationRedirectURL}.
|
||||
*
|
||||
* @param passkeySessionID The passkeySessionID for which we are redirecting.
|
||||
* This is saved to session storage to allow us to ignore subsequent redirects
|
||||
* to the passkey flow finish page except the ones for this specific session.
|
||||
*
|
||||
* @param url The URL to redirect to or open in the system browser.
|
||||
*
|
||||
* [Note: Passkey verification in the desktop app]
|
||||
*
|
||||
* Our desktop app bundles the web app and serves it over a custom protocol.
|
||||
@@ -75,10 +87,10 @@ export const passkeyVerificationRedirectURL = (
|
||||
* authentication happens at accounts.ente.io, and on success there is
|
||||
* redirected back to the desktop app.
|
||||
*/
|
||||
export const openPasskeyVerificationURL = (
|
||||
passkeySessionID: string,
|
||||
url: string,
|
||||
) => {
|
||||
export const openPasskeyVerificationURL = ({
|
||||
passkeySessionID,
|
||||
url,
|
||||
}: OpenPasskeyVerificationURLOptions) => {
|
||||
sessionStorage.setItem("inflightPasskeySessionID", passkeySessionID);
|
||||
|
||||
if (globalThis.electron) window.open(url);
|
||||
@@ -192,3 +204,78 @@ const getAccountsToken = async () => {
|
||||
);
|
||||
return resp.data["accountsToken"];
|
||||
};
|
||||
|
||||
/**
|
||||
* The passkey session whose status we are trying to check has already expired.
|
||||
* The user should attempt to login again.
|
||||
*/
|
||||
export const passkeySessionExpiredErrorMessage = "Passkey session has expired";
|
||||
|
||||
/**
|
||||
* Check if the user has already authenticated using their passkey for the given
|
||||
* session.
|
||||
*
|
||||
* This is useful in case the automatic redirect back from accounts.ente.io to
|
||||
* the desktop app does not work for some reason. In such cases, the user can
|
||||
* press the "Check status" button: we'll make an API call to see if the
|
||||
* authentication has already completed, and if so, get the same "response"
|
||||
* object we'd have gotten as a query parameter in a redirect in
|
||||
* {@link saveCredentialsAndNavigateTo} on the "/passkeys/finish" page.
|
||||
*
|
||||
* @param sessionID The passkey session whose session we wish to check the
|
||||
* status of.
|
||||
*
|
||||
* @returns A {@link TwoFactorAuthorizationResponse} if the passkey
|
||||
* authentication has completed, and `undefined` otherwise.
|
||||
*
|
||||
* @throws In addition to arbitrary errors, it throws errors with the message
|
||||
* {@link passkeySessionExpiredErrorMessage}.
|
||||
*/
|
||||
export const checkPasskeyVerificationStatus = async (
|
||||
sessionID: string,
|
||||
): Promise<TwoFactorAuthorizationResponse | undefined> => {
|
||||
const url = `${apiOrigin()}/users/two-factor/passkeys/get-token`;
|
||||
const params = new URLSearchParams({ sessionID });
|
||||
const res = await fetch(`${url}?${params.toString()}`, {
|
||||
headers: clientPackageHeaderIfPresent(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (res.status == 404 || res.status == 410)
|
||||
throw new Error(passkeySessionExpiredErrorMessage);
|
||||
if (res.status == 400) return undefined; /* verification pending */
|
||||
throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
}
|
||||
return TwoFactorAuthorizationResponse.parse(await res.json());
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract credentials from a successful passkey verification response and save
|
||||
* them to local storage for use by subsequent steps (or normal functioning) of
|
||||
* the app.
|
||||
*
|
||||
* @param response The result of a successful
|
||||
* {@link checkPasskeyVerificationStatus}.
|
||||
*
|
||||
* @returns the slug that we should navigate to now.
|
||||
*/
|
||||
export const saveCredentialsAndNavigateTo = (
|
||||
response: TwoFactorAuthorizationResponse,
|
||||
) => {
|
||||
// This method somewhat duplicates `saveCredentialsAndNavigateTo` in the
|
||||
// /passkeys/finish page.
|
||||
const { id, encryptedToken, keyAttributes } = response;
|
||||
|
||||
setData(LS_KEYS.USER, {
|
||||
...getData(LS_KEYS.USER),
|
||||
encryptedToken,
|
||||
id,
|
||||
});
|
||||
setData(LS_KEYS.KEY_ATTRIBUTES, ensure(keyAttributes));
|
||||
|
||||
// TODO(MR): Remove the cast.
|
||||
const redirectURL = InMemoryStore.get(MS_KEYS.REDIRECT_URL) as
|
||||
| string
|
||||
| undefined;
|
||||
InMemoryStore.delete(MS_KEYS.REDIRECT_URL);
|
||||
return redirectURL ?? "/credentials";
|
||||
};
|
||||
|
||||
@@ -48,3 +48,13 @@ export const authenticatedRequestHeaders = (): Record<string, string> => {
|
||||
if (_clientPackage) headers["X-Client-Package"] = _clientPackage;
|
||||
return headers;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a headers object with "X-Client-Package" header if we have the client
|
||||
* package value available to us from local storage.
|
||||
*/
|
||||
export const clientPackageHeaderIfPresent = (): Record<string, string> => {
|
||||
const headers: Record<string, string> = {};
|
||||
if (_clientPackage) headers["X-Client-Package"] = _clientPackage;
|
||||
return headers;
|
||||
};
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "",
|
||||
"passkey_login_failed": "",
|
||||
"passkey_login_invalid_url": "",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "",
|
||||
"try_again": "",
|
||||
"check_status": "",
|
||||
"passkey_login_instructions": "",
|
||||
"passkey_login": "",
|
||||
"passkey": "",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "",
|
||||
"verification_still_pending": "",
|
||||
"passkey_verified": "",
|
||||
"redirecting_back_to_app": "",
|
||||
"redirect_close_instructions": "",
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "",
|
||||
"passkey_login_failed": "",
|
||||
"passkey_login_invalid_url": "",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "",
|
||||
"try_again": "",
|
||||
"check_status": "",
|
||||
"passkey_login_instructions": "",
|
||||
"passkey_login": "",
|
||||
"passkey": "",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "",
|
||||
"verification_still_pending": "",
|
||||
"passkey_verified": "",
|
||||
"redirecting_back_to_app": "",
|
||||
"redirect_close_instructions": "",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"SENT": "Gesendet!",
|
||||
"password": "Passwort",
|
||||
"link_password_description": "Passwort zum Entsperren des Albums eingeben",
|
||||
"unlock": "",
|
||||
"unlock": "Freischalten",
|
||||
"SET_PASSPHRASE": "Passwort setzen",
|
||||
"VERIFY_PASSPHRASE": "Einloggen",
|
||||
"INCORRECT_PASSPHRASE": "Falsches Passwort",
|
||||
@@ -85,7 +85,7 @@
|
||||
"NEXT": "Weitere (→)",
|
||||
"title_photos": "Ente Fotos",
|
||||
"title_auth": "Ente Auth",
|
||||
"title_accounts": "",
|
||||
"title_accounts": "Ente Konten",
|
||||
"UPLOAD_FIRST_PHOTO": "Lade dein erstes Foto hoch",
|
||||
"IMPORT_YOUR_FOLDERS": "Importiere deiner Ordner",
|
||||
"UPLOAD_DROPZONE_MESSAGE": "Loslassen, um Dateien zu sichern",
|
||||
@@ -566,7 +566,7 @@
|
||||
"VIDEO": "Video",
|
||||
"LIVE_PHOTO": "Live-Foto",
|
||||
"editor": {
|
||||
"crop": ""
|
||||
"crop": "Zuschneiden"
|
||||
},
|
||||
"CONVERT": "Konvertieren",
|
||||
"CONFIRM_EDITOR_CLOSE_MESSAGE": "Editor wirklich schließen?",
|
||||
@@ -610,8 +610,8 @@
|
||||
"APPLY_CROP": "Zuschnitt anwenden",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": "Es muss mindestens eine Transformation oder Farbanpassung vorgenommen werden, bevor gespeichert werden kann.",
|
||||
"passkeys": "Passkeys",
|
||||
"passkey_fetch_failed": "",
|
||||
"manage_passkey": "",
|
||||
"passkey_fetch_failed": "Ihre Passkeys konnten nicht abgerufen werden.",
|
||||
"manage_passkey": "Passkey verwalten",
|
||||
"delete_passkey": "Passkey löschen",
|
||||
"delete_passkey_confirmation": "Bist du sicher, dass du diesen Passkey löschen willst? Dieser Vorgang ist nicht umkehrbar.",
|
||||
"rename_passkey": "Passkey umbenennen",
|
||||
@@ -619,19 +619,24 @@
|
||||
"enter_passkey_name": "Passkey-Namen eingeben",
|
||||
"passkeys_description": "Passkeys sind ein moderner und sicherer zweiter Faktor für dein Ente-Konto. Sie nutzen die biometrische Authentifizierung des Geräts für Komfort und Sicherheit.",
|
||||
"CREATED_AT": "Erstellt am",
|
||||
"passkey_add_failed": "",
|
||||
"passkey_add_failed": "Ein Passkey konnte nicht hinzugefügt werden",
|
||||
"passkey_login_failed": "Passkey-Anmeldung fehlgeschlagen",
|
||||
"passkey_login_invalid_url": "Die Anmelde-URL ist ungültig.",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "Ein Fehler trat auf beim Anmelden mit dem Passkey auf.",
|
||||
"passkeys_not_supported": "",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "Passkeys werden in diesem Browser nicht unterstützt",
|
||||
"try_again": "Erneut versuchen",
|
||||
"check_status": "Status überprüfen",
|
||||
"passkey_login_instructions": "Folge den Schritten in deinem Browser, um mit dem Anmelden fortzufahren.",
|
||||
"passkey_login": "Mit Passkey anmelden",
|
||||
"passkey": "",
|
||||
"waiting_for_verification": "",
|
||||
"passkey_verified": "",
|
||||
"redirecting_back_to_app": "",
|
||||
"redirect_close_instructions": "",
|
||||
"passkey": "Passkey",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "Warte auf Bestätigung...",
|
||||
"verification_still_pending": "Verifizierung steht noch aus",
|
||||
"passkey_verified": "Passwort verifiziert",
|
||||
"redirecting_back_to_app": "Sie werden zurück zur App weitergeleitet...",
|
||||
"redirect_close_instructions": "Sie werden zurück zur App weitergeleitet.",
|
||||
"redirect_again": "",
|
||||
"autogenerated_first_album_name": "Mein erstes Album",
|
||||
"autogenerated_default_album_name": "Neues Album"
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "Could not add passkey",
|
||||
"passkey_login_failed": "Passkey login failed",
|
||||
"passkey_login_invalid_url": "The login URL is invalid.",
|
||||
"passkey_login_already_claimed_session": "This session has already been verified.",
|
||||
"passkey_login_generic_error": "An error occurred while logging in with passkey.",
|
||||
"passkey_login_credential_hint": "If your passkeys are on a different device, you can open this page on that device to verify.",
|
||||
"passkeys_not_supported": "Passkeys are not supported in this browser",
|
||||
"try_again": "Try again",
|
||||
"check_status": "Check status",
|
||||
"passkey_login_instructions": "Follow the steps from your browser to continue logging in.",
|
||||
"passkey_login": "Login with passkey",
|
||||
"passkey": "Passkey",
|
||||
"passkey_verify_description": "Verify your passkey to login into your account.",
|
||||
"waiting_for_verification": "Waiting for verification...",
|
||||
"verification_still_pending": "Verification is still pending",
|
||||
"passkey_verified": "Passkey verified",
|
||||
"redirecting_back_to_app": "Redirecting you back to the app...",
|
||||
"redirect_close_instructions": "You can close this window after the app opens.",
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "",
|
||||
"passkey_login_failed": "",
|
||||
"passkey_login_invalid_url": "",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "",
|
||||
"try_again": "Inténtelo de nuevo",
|
||||
"check_status": "",
|
||||
"passkey_login_instructions": "",
|
||||
"passkey_login": "",
|
||||
"passkey": "",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "",
|
||||
"verification_still_pending": "",
|
||||
"passkey_verified": "",
|
||||
"redirecting_back_to_app": "",
|
||||
"redirect_close_instructions": "",
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "",
|
||||
"passkey_login_failed": "",
|
||||
"passkey_login_invalid_url": "",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "",
|
||||
"try_again": "",
|
||||
"check_status": "",
|
||||
"passkey_login_instructions": "",
|
||||
"passkey_login": "",
|
||||
"passkey": "",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "",
|
||||
"verification_still_pending": "",
|
||||
"passkey_verified": "",
|
||||
"redirecting_back_to_app": "",
|
||||
"redirect_close_instructions": "",
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "",
|
||||
"passkey_login_failed": "",
|
||||
"passkey_login_invalid_url": "",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "",
|
||||
"try_again": "",
|
||||
"check_status": "",
|
||||
"passkey_login_instructions": "",
|
||||
"passkey_login": "",
|
||||
"passkey": "",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "",
|
||||
"verification_still_pending": "",
|
||||
"passkey_verified": "",
|
||||
"redirecting_back_to_app": "",
|
||||
"redirect_close_instructions": "",
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "",
|
||||
"passkey_login_failed": "Échec de la connexion via code d'accès",
|
||||
"passkey_login_invalid_url": "L’URL de connexion est invalide.",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "Une erreur s'est produite lors de la connexion avec le code d'accès.",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "",
|
||||
"try_again": "Réessayer",
|
||||
"check_status": "",
|
||||
"passkey_login_instructions": "Suivez les étapes de votre navigateur pour poursuivre la connexion.",
|
||||
"passkey_login": "Se connecter avec le code d'accès",
|
||||
"passkey": "",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "",
|
||||
"verification_still_pending": "",
|
||||
"passkey_verified": "Vérification du code d'accès",
|
||||
"redirecting_back_to_app": "Redirection vers l'application...",
|
||||
"redirect_close_instructions": "Vous pouvez fermer cette fenêtre après l'ouverture de l'application.",
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "",
|
||||
"passkey_login_failed": "",
|
||||
"passkey_login_invalid_url": "",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "",
|
||||
"try_again": "",
|
||||
"check_status": "",
|
||||
"passkey_login_instructions": "",
|
||||
"passkey_login": "",
|
||||
"passkey": "",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "",
|
||||
"verification_still_pending": "",
|
||||
"passkey_verified": "",
|
||||
"redirecting_back_to_app": "",
|
||||
"redirect_close_instructions": "",
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "",
|
||||
"passkey_login_failed": "",
|
||||
"passkey_login_invalid_url": "",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "",
|
||||
"try_again": "",
|
||||
"check_status": "",
|
||||
"passkey_login_instructions": "",
|
||||
"passkey_login": "",
|
||||
"passkey": "",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "",
|
||||
"verification_still_pending": "",
|
||||
"passkey_verified": "",
|
||||
"redirecting_back_to_app": "",
|
||||
"redirect_close_instructions": "",
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "",
|
||||
"passkey_login_failed": "",
|
||||
"passkey_login_invalid_url": "",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "",
|
||||
"try_again": "",
|
||||
"check_status": "",
|
||||
"passkey_login_instructions": "",
|
||||
"passkey_login": "",
|
||||
"passkey": "",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "",
|
||||
"verification_still_pending": "",
|
||||
"passkey_verified": "",
|
||||
"redirecting_back_to_app": "",
|
||||
"redirect_close_instructions": "",
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "",
|
||||
"passkey_login_failed": "",
|
||||
"passkey_login_invalid_url": "",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "",
|
||||
"try_again": "",
|
||||
"check_status": "",
|
||||
"passkey_login_instructions": "",
|
||||
"passkey_login": "",
|
||||
"passkey": "",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "",
|
||||
"verification_still_pending": "",
|
||||
"passkey_verified": "",
|
||||
"redirecting_back_to_app": "",
|
||||
"redirect_close_instructions": "",
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "",
|
||||
"passkey_login_failed": "Passkey login mislukt",
|
||||
"passkey_login_invalid_url": "De inlog-URL is ongeldig.",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "Er is een fout opgetreden tijdens het inloggen met een passkey.",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "",
|
||||
"try_again": "Probeer opnieuw",
|
||||
"check_status": "",
|
||||
"passkey_login_instructions": "Volg de stappen van je browser om door te gaan met inloggen.",
|
||||
"passkey_login": "Inloggen met passkey",
|
||||
"passkey": "",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "",
|
||||
"verification_still_pending": "",
|
||||
"passkey_verified": "",
|
||||
"redirecting_back_to_app": "",
|
||||
"redirect_close_instructions": "",
|
||||
|
||||
@@ -622,13 +622,18 @@
|
||||
"passkey_add_failed": "Não foi possível adicionar chave de acesso",
|
||||
"passkey_login_failed": "Falha ao iniciar sessão com a chave de acesso",
|
||||
"passkey_login_invalid_url": "URL de login inválida.",
|
||||
"passkey_login_already_claimed_session": "",
|
||||
"passkey_login_generic_error": "Ocorreu um erro ao entrar com a chave de acesso.",
|
||||
"passkey_login_credential_hint": "",
|
||||
"passkeys_not_supported": "As chaves de acesso não são suportadas neste navegador",
|
||||
"try_again": "Tente novamente",
|
||||
"check_status": "",
|
||||
"passkey_login_instructions": "Siga os passos do seu navegador para continuar acessando.",
|
||||
"passkey_login": "Entrar com a chave de acesso",
|
||||
"passkey": "",
|
||||
"passkey_verify_description": "",
|
||||
"waiting_for_verification": "",
|
||||
"verification_still_pending": "",
|
||||
"passkey_verified": "Chave de acesso verificada",
|
||||
"redirecting_back_to_app": "Redirecionando você de volta para o aplicativo...",
|
||||
"redirect_close_instructions": "Você pode fechar esta janela após a aplicação ser aberta.",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user