Compare commits

...

106 Commits

Author SHA1 Message Date
Manav Rathi
99fdbd8d40 photosd-v1.7.1 (#2170) 2024-06-17 13:57:28 +05:30
Manav Rathi
ac4a68d64e photosd-v1.7.1 2024-06-17 13:55:31 +05:30
Manav Rathi
dae5b29ef1 [docs] Add a border to help differentiate the image from the subsequent text (#2169)
Nb: black works fine in dark mode too
2024-06-17 12:24:14 +05:30
Manav Rathi
4451b489e4 Add a border to help differentiate the image from the subsequent text
black works fine in dark mode too
2024-06-17 12:22:47 +05:30
Manav Rathi
b19281ea2b [web] Update cast to use the tsconfig we want (#2168) 2024-06-17 12:08:41 +05:30
Manav Rathi
8e923fe443 All 2024-06-17 11:46:34 +05:30
Manav Rathi
fe47186ace tsc 2024-06-17 11:45:28 +05:30
Manav Rathi
326704a605 tsc 2024-06-17 11:38:13 +05:30
Manav Rathi
d560ed9a33 Reduce state 2024-06-17 11:35:19 +05:30
Manav Rathi
0a8f51832a tsc 2024-06-17 11:34:16 +05:30
Neeraj Gupta
d2112b984d Added coindcx icon (#2148)
## Description
Added coindcx icon
uploaded coindcx svg and updated json file.
2024-06-17 11:24:48 +05:30
Neeraj Gupta
50aad0c5d1 [Auth] Passkey fix for linux (#2154)
## Description

- Updated url to enteauth://passkey
- Support mimetype in appimage so redirection is done properly

## Tests
2024-06-17 11:24:09 +05:30
Manav Rathi
e6e721f0ba [workers] Import upload worker (#2164) 2024-06-17 09:12:15 +05:30
Manav Rathi
80b34f1aef Reduce spurious logs for headers our clients send 2024-06-17 09:03:27 +05:30
Manav Rathi
d5a8586152 Import functionality
Rewritten but referencing the code imported from dashboard
2024-06-17 03:40:20 +05:30
Manav Rathi
bfcd84c940 Whitelist the necessary one 2024-06-16 20:56:28 +05:30
Manav Rathi
a4bc5fa0df OPTIONS 2024-06-16 20:49:44 +05:30
Manav Rathi
ed406e7eb0 Sketch 2024-06-16 20:33:56 +05:30
Manav Rathi
b4dc49ef2f [workers] Import health check worker (#2162) 2024-06-16 19:43:31 +05:30
Manav Rathi
483e3be682 Improvements 2024-06-16 19:40:03 +05:30
Manav Rathi
17f0d77a31 Reorder to fix errors 2024-06-16 19:18:39 +05:30
Manav Rathi
c6f644ef8a [workers] Import health check worker 2024-06-16 19:12:52 +05:30
Prateek Sunal
01b566698f fix(workflow/auth): revert back flutter_distributor to pub.dev source 2024-06-16 17:03:45 +05:30
Manav Rathi
469f884d8c [workers] Import files worker (#2161) 2024-06-16 15:01:26 +05:30
Manav Rathi
9e4412cbee Correct place 2024-06-16 14:52:15 +05:30
Manav Rathi
f4bab262ca Import 2024-06-16 14:47:35 +05:30
Manav Rathi
73fd63616d Sketch 2024-06-16 14:39:05 +05:30
Manav Rathi
9362a4b9d3 Reduce log noise 2024-06-16 14:29:54 +05:30
Manav Rathi
6c5ea59506 [workers] Import thumbnails worker (#2160) 2024-06-16 14:19:02 +05:30
Manav Rathi
90845bdb02 Rename 2024-06-16 14:12:16 +05:30
Manav Rathi
f6729be5ab Fix typo 2024-06-16 14:09:20 +05:30
Manav Rathi
344c5cc399 Desktop origin includes scheme 2024-06-16 14:06:20 +05:30
Manav Rathi
6e1ea29c39 Implement
Rewritten, but referencing the existing worker imported from the dashboard
2024-06-16 13:51:42 +05:30
Manav Rathi
d76c6dd63c Sketch 2024-06-16 13:24:56 +05:30
Manav Rathi
f69daa4608 [workers] Import public albums worker (#2158) 2024-06-16 10:19:42 +05:30
Manav Rathi
290564c973 x-client-package
Albums app is using the old axios layer which is passing "x-client-package",
will also allow that for now
2024-06-16 10:06:22 +05:30
Manav Rathi
b781f33e4b ditto 2024-06-16 09:57:44 +05:30
Manav Rathi
b8bc01561d GET
Rewritten, but referencing the existing worker imported from the dashboard
2024-06-16 09:55:49 +05:30
Manav Rathi
734cb798d3 Handle options
Rewritten, but referencing the existing worker imported from the dashboard
2024-06-16 09:27:59 +05:30
Manav Rathi
ac8ebd0ed3 Skeleton 2024-06-16 08:56:07 +05:30
Manav Rathi
fc5eb296d2 Disable default route 2024-06-16 08:39:59 +05:30
Manav Rathi
c05d8a8e44 [worker] Use tail worker for logging (#2153) 2024-06-15 22:59:50 +05:30
Manav Rathi
24845a4735 Update README 2024-06-15 22:56:32 +05:30
Manav Rathi
2b490fe131 Cleanup 2024-06-15 22:49:35 +05:30
Manav Rathi
07f0cc9342 Need to pass creds in authorization header 2024-06-15 22:35:38 +05:30
Manav Rathi
49ddd287d0 Only log interesting events 2024-06-15 20:18:10 +05:30
Manav Rathi
bffcd11100 console.log 2024-06-15 19:47:20 +05:30
Manav Rathi
25d6ebdb19 Add a check for an upcoming restriction 2024-06-15 19:42:10 +05:30
Manav Rathi
64a539adb0 Hook it up 2024-06-15 19:23:05 +05:30
Manav Rathi
3646809f06 Promise<void>
Ref: https://dev.to/krasun/pushing-cloudflare-worker-logs-to-grafana-loki-1elg
2024-06-15 19:18:39 +05:30
Manav Rathi
fb0e857514 Push the entire event (it contains the worker name too) 2024-06-15 19:16:17 +05:30
Manav Rathi
a1059c543b Fill in 2024-06-15 19:09:42 +05:30
Manav Rathi
8fe2b9cb27 The protocol 2024-06-15 18:27:44 +05:30
Manav Rathi
5e080a90e3 Skeletal tail worker 2024-06-15 17:51:46 +05:30
Vishnu Mohandas
08255b3f8a ente -> Ente (#2151) 2024-06-15 16:21:45 +05:30
vishnukvmd
f032739461 ente -> Ente 2024-06-15 16:21:10 +05:30
Manav Rathi
841da80c97 [workers] Minor cleanup, in prep for moving more of their siblings here (#2149) 2024-06-15 12:28:34 +05:30
Manav Rathi
60b1c32567 Note 2024-06-15 12:09:21 +05:30
Manav Rathi
bd6ac2c4fc Sync 2024-06-15 12:03:32 +05:30
Manav Rathi
eaccba5f22 Explicit header whitelist 2024-06-15 12:02:29 +05:30
Manav Rathi
562313b218 Tweaks 2024-06-15 11:54:26 +05:30
Manav Rathi
0650d176ee Latest yarn
Corepack will automatically install the latest one
2024-06-15 11:44:39 +05:30
Manav Rathi
6bbd944de4 Update compt date
> When you start your project, you should always set compatibility_date to the
> current date. You should occasionally update the compatibility_date field.
>
> https://developers.cloudflare.com/workers/configuration/compatibility-dates/
2024-06-15 11:41:09 +05:30
Manav Rathi
8aaad79897 yarn add --dev '@cloudflare/workers-types@latest' 2024-06-15 11:39:56 +05:30
Manav Rathi
d499549734 Use syntax recommended in docs
https://developers.cloudflare.com/workers/configuration/routing/custom-domains
2024-06-15 11:26:54 +05:30
Nikunj Kumar Nakum
db22c5bc97 Update custom-icons.json
updated coindcx icon
2024-06-15 10:51:25 +05:30
Nikunj Kumar Nakum
34f49362fd Added CoinDCX icon
Uploaded coindcx svg file
2024-06-15 10:46:49 +05:30
Manav Rathi
af21ff640d (CF's) fetch can return a promise 2024-06-15 10:03:39 +05:30
Manav Rathi
69e69c2e0f Formatting and other minor tweaks 2024-06-15 09:54:28 +05:30
Vishnu Mohandas
a0445fb4f6 v901 (#2142) 2024-06-15 00:42:56 +05:30
Prateek Sunal
8161403d84 fix(workflow/auth): use custom distributor repo for appimage 2024-06-15 00:39:51 +05:30
Prateek Sunal
0713e34aec chore(auth): bump packages 2024-06-15 00:35:46 +05:30
Prateek Sunal
b504f554b3 fix(auth): add mimetype to appimage 2024-06-15 00:35:27 +05:30
vishnukvmd
3d6af698b6 v901 2024-06-15 00:00:07 +05:30
Prateek Sunal
ff3ddb3d8d fix(auth): update deep link for linux 2024-06-14 22:52:29 +05:30
Neeraj Gupta
16817eceac [photos] Update bundle name from ente Photos -> Ente Photos (#2141)
## Description

## Tests
2024-06-14 16:02:15 +05:30
Neeraj Gupta
500e40035f [photos] Update bundle name from ente Photos -> Ente Photos 2024-06-14 15:59:44 +05:30
Neeraj Gupta
366da2c328 [photos] Bump version v0.9.0 (#2140)
## Description

## Tests
2024-06-14 15:59:21 +05:30
Neeraj Gupta
203d46b2cf [photos] Bump version v0.9.0 2024-06-14 15:56:15 +05:30
Manav Rathi
0e772fcfb7 [desktop] Fix duplicate file uploads when initializing a folder watch (#2138)
This didn't happen always, it was a race condition dependending on when
the `this.eventQueue = []` in `syncWithDisk` happened.
2024-06-14 15:25:58 +05:30
Manav Rathi
bbd6745372 Add CHANGELOG entries 2024-06-14 15:18:57 +05:30
Manav Rathi
dd1e0a9b1d Fix duplicate file uploads when initializing a folder watch
This didn't happen always, it was a race condition dependending on when the
`this.eventQueue = []` in `syncWithDisk` happened.
2024-06-14 15:11:36 +05:30
Neeraj Gupta
940231e38d [mob][auth] Fix handling of passkey when email verification is turned on (#2137)
## Description

## Tests
2024-06-14 14:41:39 +05:30
Neeraj Gupta
4c8db02de5 [auth] Bump version to v3.0.12 2024-06-14 14:39:51 +05:30
Neeraj Gupta
8af5aadd1b [mob] Bump photos version to v0.8.139 2024-06-14 14:39:25 +05:30
Neeraj Gupta
205feab4c2 [mob][auth] Fix passkey authn flow when emailVerification is enabled 2024-06-14 14:38:44 +05:30
Manav Rathi
60ab2b4427 [web] New translations (#2128)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-web)
2024-06-14 14:37:35 +05:30
Crowdin Bot
612329f584 New Crowdin translations by GitHub Action 2024-06-14 09:06:18 +00:00
Manav Rathi
a5f4a676a7 [web] Passkeys misc loose ends (#2136) 2024-06-14 14:35:32 +05:30
Manav Rathi
9608cfaa4e Don't show retry button if trying to use an already claimed session 2024-06-14 14:30:10 +05:30
Manav Rathi
ddd4d3e16c "Scripts may only close windows that were opened by them" 2024-06-14 14:04:42 +05:30
Manav Rathi
df0d48af73 [desktop] Add a check status button to the passkey waiting page (#2132) 2024-06-14 13:55:54 +05:30
Manav Rathi
c82193cae6 Enable passkeys for everyone 2024-06-14 13:51:21 +05:30
Manav Rathi
2c0928bd02 Change to photos favicon
he accounts favicon does not show on a white background (second image is the
hover state showing that the icon is actually there). For now, changing it to
the photos favicon, until we have an app neutral favicon.
2024-06-14 13:49:45 +05:30
Manav Rathi
8c8ffa9397 Add a hint to retry on other devices 2024-06-14 13:42:30 +05:30
Manav Rathi
3689ecb6e7 Add a message 2024-06-14 13:26:05 +05:30
Manav Rathi
ca080ad6b2 Split the flow 2024-06-14 13:07:00 +05:30
Manav Rathi
b2e56fc01e Lint fix 2024-06-14 12:23:09 +05:30
Manav Rathi
228dd90bce Make the retry code (almost) exactly the same as it was before
in an attempt at superstition (since rationality doesn't seem to work with
Safari).
2024-06-14 12:11:43 +05:30
Manav Rathi
93380d05b4 Add TODO 2024-06-14 12:04:34 +05:30
Manav Rathi
4123197c6d Use 2024-06-14 11:46:55 +05:30
Manav Rathi
cc3f398a78 Happy path 2024-06-14 11:41:50 +05:30
Manav Rathi
dd0f7d3142 Handle errors 2024-06-14 11:17:51 +05:30
Manav Rathi
325c963b7a Mix 2024-06-14 11:03:13 +05:30
Manav Rathi
fbf29585eb UI 2024-06-14 10:51:58 +05:30
Manav Rathi
8a2cc858ae API method 2024-06-14 10:10:09 +05:30
110 changed files with 1551 additions and 374 deletions

View File

@@ -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

View File

@@ -67,6 +67,9 @@
{
"title": "Cloudflare"
},
{
"title": "CoinDCX"
},
{
"title": "ConfigCat"
},

View 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

View File

@@ -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

View File

@@ -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) {

View File

@@ -27,3 +27,6 @@ include:
- libffi.so.8
- libtiff.so.5
- libjpeg.so.8
supported_mime_type:
- x-scheme-handler/enteauth

View File

@@ -31,4 +31,4 @@ categories:
startup_notify: false
supported_mime_type:
- x-scheme-handler/ente
- x-scheme-handler/enteauth

View File

@@ -28,4 +28,4 @@ categories:
startup_notify: false
supported_mime_type:
- x-scheme-handler/ente
- x-scheme-handler/enteauth

View File

@@ -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"

View File

@@ -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:

View File

@@ -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

View File

@@ -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",

View File

@@ -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,
});

View File

@@ -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">
![Error when VC++ runtime is not installed](windows-vc.png){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

View File

@@ -1,3 +1,4 @@
{
"tabWidth": 4
"tabWidth": 4,
"proseWrap": "always"
}

View File

@@ -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.

View File

@@ -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"
}

View File

@@ -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 });
};

View File

@@ -1 +1 @@
{ "extends": "../tsconfig.base.json", "include": ["src/**/*.ts"] }
{ "extends": "../tsconfig.base.json", "include": ["src"] }

View File

@@ -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" }
]

View 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"
}

View 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;
};

View File

@@ -0,0 +1 @@
{ "extends": "../tsconfig.base.json", "include": ["src"] }

View 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" }
]

View File

@@ -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"
}

View File

@@ -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;
};

View File

@@ -1 +1 @@
{ "extends": "../tsconfig.base.json", "include": ["src/**/*.ts"] }
{ "extends": "../tsconfig.base.json", "include": ["src"] }

View File

@@ -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

View 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"
}

View 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})`);
};

View File

@@ -0,0 +1 @@
{ "extends": "../tsconfig.base.json", "include": ["src"] }

View 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 * * * *" ]

View 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"
}

View 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;
};

View File

@@ -0,0 +1 @@
{ "extends": "../tsconfig.base.json", "include": ["src"] }

View 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" }
]

View 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"
}

View 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]],
},
],
}),
});

View File

@@ -0,0 +1 @@
{ "extends": "../tsconfig.base.json", "include": ["src"] }

View 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)}"

View 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"
}

View 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;
};

View File

@@ -0,0 +1 @@
{ "extends": "../tsconfig.base.json", "include": ["src"] }

View 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" }
]

View File

@@ -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,

View 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"
}

View 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;
};

View File

@@ -0,0 +1 @@
{ "extends": "../tsconfig.base.json", "include": ["src"] }

View 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" }]

View File

@@ -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">
[{

View File

@@ -1 +1 @@
ente Photos
Ente Photos

View File

@@ -1 +1 @@
ente Photos
Ente Photos

View File

@@ -1 +1 @@
ente Photos
Ente Photos

View File

@@ -1 +1 @@
ente Photos
Ente Photos

View File

@@ -1 +1 @@
ente Photos
Ente Photos

View File

@@ -1 +1 @@
ente Photos
Ente Photos

View File

@@ -1 +1 @@
ente Photos
Ente Photos

View File

@@ -1 +1 @@
ente Photos
Ente Photos

View File

@@ -1 +1 @@
ente Photos
Ente Photos

View File

@@ -1 +1 @@
ente Photos
Ente Photos

View File

@@ -1 +1 @@
ente Photos
Ente Photos

View File

@@ -1 +1 @@
ente Photos
Ente Photos

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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));

View File

@@ -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);

View File

@@ -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} />

View File

@@ -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);
}
};

View File

@@ -141,6 +141,7 @@ const advertiseCode = (cast: Cast) => {
const collectionID =
data &&
typeof data == "object" &&
"collectionID" in data &&
typeof data["collectionID"] == "string"
? data["collectionID"]
: undefined;

View File

@@ -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());

View File

@@ -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

View File

@@ -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);
}

View File

@@ -1,5 +1,5 @@
import type { Metadata } from "@/media/types/file";
import {
import type {
EncryptedMagicMetadata,
MagicMetadataCore,
VISIBILITY_STATE,

View File

@@ -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"
]
}

View File

@@ -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"

View File

@@ -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}
/>
);
}

View File

@@ -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).

View File

@@ -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}
/>
);
}

View File

@@ -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";
};

View File

@@ -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;
};

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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"

View File

@@ -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.",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -622,13 +622,18 @@
"passkey_add_failed": "",
"passkey_login_failed": "Échec de la connexion via code d'accès",
"passkey_login_invalid_url": "LURL 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.",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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