Ver código fonte

Merge remote-tracking branch 'origin/main' into beta

Prateek Sunal 1 ano atrás
pai
commit
5634b50528
100 arquivos alterados com 1618 adições e 636 exclusões
  1. 3 0
      .gitattributes
  2. 5 4
      .github/workflows/auth-crowdin.yml
  3. 2 4
      .github/workflows/auth-lint.yml
  4. 1 1
      .github/workflows/auth-release.yml
  5. 3 0
      .github/workflows/cli-release.yml
  6. 47 0
      .github/workflows/docs-deploy.yml
  7. 37 0
      .github/workflows/docs-verify-build.yml
  8. 5 4
      .github/workflows/mobile-crowdin.yml
  9. 2 4
      .github/workflows/mobile-lint.yml
  10. 5 3
      .github/workflows/mobile-release.yml
  11. 2 4
      .github/workflows/server-lint.yml
  12. 11 4
      .github/workflows/web-crowdin.yml
  13. 43 0
      .github/workflows/web-deploy-accounts.yml
  14. 43 0
      .github/workflows/web-deploy-auth.yml
  15. 43 0
      .github/workflows/web-deploy-cast.yml
  16. 43 0
      .github/workflows/web-deploy-photos.yml
  17. 2 13
      .github/workflows/web-lint.yml
  18. 94 0
      .github/workflows/web-nightly.yml
  19. 52 0
      .github/workflows/web-preview.yml
  20. 0 11
      .gitmodules
  21. 2 2
      CONTRIBUTING.md
  22. 1 1
      README.md
  23. 1 1
      SUPPORT.md
  24. 3 0
      auth/.gitignore
  25. 1 1
      auth/README.md
  26. 3 0
      auth/analysis_options.yaml
  27. 8 16
      auth/assets/custom-icons/_data/custom-icons.json
  28. 14 2
      auth/docs/release.md
  29. 1 1
      auth/flutter
  30. 0 6
      auth/ios/Podfile.lock
  31. 8 1
      auth/lib/core/configuration.dart
  32. 3 3
      auth/lib/core/logging/super_logging.dart
  33. 24 16
      auth/lib/core/network.dart
  34. 3 0
      auth/lib/events/endpoint_updated_event.dart
  35. 15 49
      auth/lib/gateway/authenticator.dart
  36. 1 0
      auth/lib/l10n/arb/app_bg.arb
  37. 12 1
      auth/lib/l10n/arb/app_de.arb
  38. 11 3
      auth/lib/l10n/arb/app_en.arb
  39. 12 1
      auth/lib/l10n/arb/app_ja.arb
  40. 12 1
      auth/lib/l10n/arb/app_pt.arb
  41. 73 1
      auth/lib/l10n/arb/app_sv.arb
  42. 12 1
      auth/lib/l10n/arb/app_zh.arb
  43. 2 1
      auth/lib/main.dart
  44. 13 0
      auth/lib/models/account/two_factor.dart
  45. 141 107
      auth/lib/onboarding/view/onboarding_page.dart
  46. 3 4
      auth/lib/services/authenticator_service.dart
  47. 1 0
      auth/lib/services/local_authentication_service.dart
  48. 22 0
      auth/lib/services/passkey_service.dart
  49. 6 3
      auth/lib/services/update_service.dart
  50. 39 5
      auth/lib/services/user_service.dart
  51. 0 4
      auth/lib/store/user_store.dart
  52. 1 1
      auth/lib/ui/account/delete_account_page.dart
  53. 1 0
      auth/lib/ui/account/logout_dialog.dart
  54. 6 1
      auth/lib/ui/account/password_entry_page.dart
  55. 1 0
      auth/lib/ui/account/password_reentry_page.dart
  56. 5 4
      auth/lib/ui/account/request_pwd_verification_page.dart
  57. 2 1
      auth/lib/ui/account/sessions_page.dart
  58. 1 0
      auth/lib/ui/account/verify_recovery_page.dart
  59. 2 1
      auth/lib/ui/code_widget.dart
  60. 1 0
      auth/lib/ui/common/progress_dialog.dart
  61. 2 2
      auth/lib/ui/home_page.dart
  62. 64 28
      auth/lib/ui/passkey_page.dart
  63. 3 1
      auth/lib/ui/settings/about_section_widget.dart
  64. 5 0
      auth/lib/ui/settings/account_section_widget.dart
  65. 0 117
      auth/lib/ui/settings/app_update_dialog.dart
  66. 1 0
      auth/lib/ui/settings/danger_section_widget.dart
  67. 1 0
      auth/lib/ui/settings/data/import/google_auth_import.dart
  68. 8 8
      auth/lib/ui/settings/data/import/import_service.dart
  69. 1 1
      auth/lib/ui/settings/data/import_page.dart
  70. 90 0
      auth/lib/ui/settings/developer_settings_page.dart
  71. 27 0
      auth/lib/ui/settings/developer_settings_widget.dart
  72. 1 0
      auth/lib/ui/settings/general_section_widget.dart
  73. 29 1
      auth/lib/ui/settings/security_section_widget.dart
  74. 1 0
      auth/lib/ui/settings/social_section_widget.dart
  75. 2 0
      auth/lib/ui/settings/support_section_widget.dart
  76. 2 0
      auth/lib/ui/settings_page.dart
  77. 1 0
      auth/lib/ui/tools/lock_screen.dart
  78. 7 2
      auth/lib/ui/two_factor_authentication_page.dart
  79. 4 0
      auth/lib/ui/two_factor_recovery_page.dart
  80. 62 5
      auth/lib/utils/email_util.dart
  81. 4 4
      auth/lib/utils/toast_util.dart
  82. 4 0
      auth/migration-guides/README.md
  83. 2 62
      auth/migration-guides/authy.md
  84. 2 63
      auth/migration-guides/encrypted_export.md
  85. 16 16
      auth/pubspec.lock
  86. 0 2
      auth/pubspec.yaml
  87. 49 27
      cli/README.md
  88. 40 3
      cli/cmd/account.go
  89. 90 0
      cli/cmd/admin.go
  90. 8 2
      cli/cmd/root.go
  91. 1 1
      cli/cmd/version.go
  92. 10 0
      cli/config.yaml.example
  93. 28 0
      cli/docs/generated/ente.md
  94. 19 0
      cli/docs/generated/ente_account.md
  95. 19 0
      cli/docs/generated/ente_account_add.md
  96. 21 0
      cli/docs/generated/ente_account_get-token.md
  97. 19 0
      cli/docs/generated/ente_account_list.md
  98. 22 0
      cli/docs/generated/ente_account_update.md
  99. 22 0
      cli/docs/generated/ente_admin.md
  100. 21 0
      cli/docs/generated/ente_admin_disable-2fa.md

+ 3 - 0
.gitattributes

@@ -0,0 +1,3 @@
+# Set line endings of shell scripts to LF, even on Windows, otherwise execution
+# within Docker fails.
+*.sh text eol=lf

+ 5 - 4
.github/workflows/auth-crowdin.yml

@@ -3,15 +3,16 @@ name: "Sync Crowdin translations (auth)"
 on:
 on:
     push:
     push:
         paths:
         paths:
-            # Run action when auth's intl_en.arb is changed
+            # Run workflow when auth's intl_en.arb is changed
             - "mobile/lib/l10n/arb/app_en.arb"
             - "mobile/lib/l10n/arb/app_en.arb"
             # Or the workflow itself is changed
             # Or the workflow itself is changed
             - ".github/workflows/auth-crowdin.yml"
             - ".github/workflows/auth-crowdin.yml"
         branches: [main]
         branches: [main]
     schedule:
     schedule:
-        # Run every 24 hours - https://crontab.guru/#0_*/24_*_*_*
-        - cron: "0 */24 * * *"
-    workflow_dispatch: # Allow manually running the action
+        # See: [Note: Run workflow on specific days of the week]
+        - cron: "50 1 * * 2,5"
+    # Also allow manually running the workflow
+    workflow_dispatch:
 
 
 jobs:
 jobs:
     synchronize-with-crowdin:
     synchronize-with-crowdin:

+ 2 - 4
.github/workflows/auth-lint.yml

@@ -1,11 +1,9 @@
 name: "Lint (auth)"
 name: "Lint (auth)"
 
 
 on:
 on:
-    # Run on every push to branches (this also covers pull requests)
+    # Run on every push to a branch other than main that changes auth/
     push:
     push:
-        # See: [Note: Specify branch when specifying a path filter]
-        branches: ["**"]
-        # Only run if something changes in these paths
+        branches-ignore: [main]
         paths:
         paths:
             - "auth/**"
             - "auth/**"
             - ".github/workflows/auth-lint.yml"
             - ".github/workflows/auth-lint.yml"

+ 1 - 1
.github/workflows/auth-release.yml

@@ -122,7 +122,7 @@ jobs:
               with:
               with:
                   serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
                   serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
                   packageName: io.ente.auth
                   packageName: io.ente.auth
-                  releaseFiles: build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab
+                  releaseFiles: auth/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab
                   track: internal
                   track: internal
 
 
     build-windows:
     build-windows:

+ 3 - 0
.github/workflows/cli-release.yml

@@ -47,5 +47,8 @@ jobs:
                   release_name: ${{ github.ref_name }}
                   release_name: ${{ github.ref_name }}
                   goversion: "1.20"
                   goversion: "1.20"
                   project_path: "./cli"
                   project_path: "./cli"
+                  pre_command: export CGO_ENABLED=0
+                  build_flags: "-trimpath"
+                  ldflags: "-X main.AppVersion=${{ github.ref_name }} -s -w"
                   md5sum: false
                   md5sum: false
                   sha256sum: true
                   sha256sum: true

+ 47 - 0
.github/workflows/docs-deploy.yml

@@ -0,0 +1,47 @@
+name: "Deploy (docs)"
+
+on:
+    # Run on every push to main that changes docs/
+    push:
+        branches: [main]
+        paths:
+            - "docs/**"
+            - ".github/workflows/docs-deploy.yml"
+    # Also allow manually running the workflow
+    workflow_dispatch:
+
+jobs:
+    deploy:
+        runs-on: ubuntu-latest
+
+        defaults:
+            run:
+                working-directory: docs
+
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
+
+            - name: Setup node and enable yarn caching
+              uses: actions/setup-node@v4
+              with:
+                  node-version: 20
+                  cache: "yarn"
+                  cache-dependency-path: "docs/yarn.lock"
+
+            - name: Install dependencies
+              run: yarn install
+
+            - name: Build production site
+              # Will create docs/.vitepress/dist
+              run: yarn build
+
+            - name: Publish
+              uses: cloudflare/pages-action@1
+              with:
+                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+                  projectName: ente
+                  branch: help
+                  directory: docs/docs/.vitepress/dist
+                  wranglerVersion: "3"

+ 37 - 0
.github/workflows/docs-verify-build.yml

@@ -0,0 +1,37 @@
+name: "Verify build (docs)"
+
+# Preflight build of docs. This allows us to ensure that yarn build is
+# succeeding before we merge the PR into main.
+
+on:
+    # Run on every push to a branch other than main that changes docs/
+    push:
+        branches-ignore: [main]
+        paths:
+            - "docs/**"
+            - ".github/workflows/docs-verify-build.yml"
+
+jobs:
+    verify-build:
+        runs-on: ubuntu-latest
+
+        defaults:
+            run:
+                working-directory: docs
+
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
+
+            - name: Setup node and enable yarn caching
+              uses: actions/setup-node@v4
+              with:
+                  node-version: 20
+                  cache: "yarn"
+                  cache-dependency-path: "docs/yarn.lock"
+
+            - name: Install dependencies
+              run: yarn install
+
+            - name: Build production site
+              run: yarn build

+ 5 - 4
.github/workflows/mobile-crowdin.yml

@@ -3,15 +3,16 @@ name: "Sync Crowdin translations (mobile)"
 on:
 on:
     push:
     push:
         paths:
         paths:
-            # Run action when mobiles's intl_en.arb is changed
+            # Run workflow when mobiles's intl_en.arb is changed
             - "mobile/lib/l10n/intl_en.arb"
             - "mobile/lib/l10n/intl_en.arb"
             # Or the workflow itself is changed
             # Or the workflow itself is changed
             - ".github/workflows/mobile-crowdin.yml"
             - ".github/workflows/mobile-crowdin.yml"
         branches: [main]
         branches: [main]
     schedule:
     schedule:
-        # Run every 24 hours - https://crontab.guru/#0_*/24_*_*_*
-        - cron: "0 */24 * * *"
-    workflow_dispatch: # Allow manually running the action
+        # See: [Note: Run workflow on specific days of the week]
+        - cron: "40 1 * * 2,5"
+    # Also allow manually running the workflow
+    workflow_dispatch:
 
 
 jobs:
 jobs:
     synchronize-with-crowdin:
     synchronize-with-crowdin:

+ 2 - 4
.github/workflows/mobile-lint.yml

@@ -1,11 +1,9 @@
 name: "Lint (mobile)"
 name: "Lint (mobile)"
 
 
 on:
 on:
-    # Run on every push (this also covers pull requests)
+    # Run on every push to a branch other than main that changes mobile/
     push:
     push:
-        # See: [Note: Specify branch when specifying a path filter]
-        branches: ["**"]
-        # Only run if something changes in these paths
+        branches-ignore: [main, f-droid]
         paths:
         paths:
             - "mobile/**"
             - "mobile/**"
             - ".github/workflows/mobile-lint.yml"
             - ".github/workflows/mobile-lint.yml"

+ 5 - 3
.github/workflows/mobile-release.yml

@@ -39,7 +39,9 @@ jobs:
                   encodedString: ${{ secrets.SIGNING_KEY_PHOTOS }}
                   encodedString: ${{ secrets.SIGNING_KEY_PHOTOS }}
 
 
             - name: Build independent APK
             - name: Build independent APK
-              run: flutter build apk --release --flavor independent && mv build/app/outputs/flutter-apk/app-independent-release.apk build/app/outputs/flutter-apk/ente.apk
+              run: |
+                flutter build apk --release --flavor independent
+                mv build/app/outputs/flutter-apk/app-independent-release.apk build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk
               env:
               env:
                   SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks"
                   SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks"
                   SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }}
                   SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }}
@@ -47,10 +49,10 @@ jobs:
                   SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }}
                   SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }}
 
 
             - name: Checksum
             - name: Checksum
-              run: sha256sum build/app/outputs/flutter-apk/ente.apk > build/app/outputs/flutter-apk/sha256sum
+              run: sha256sum build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk > build/app/outputs/flutter-apk/sha256sum
 
 
             - name: Create a draft GitHub release
             - name: Create a draft GitHub release
               uses: ncipollo/release-action@v1
               uses: ncipollo/release-action@v1
               with:
               with:
-                  artifacts: "mobile/build/app/outputs/flutter-apk/ente.apk,mobile/build/app/outputs/flutter-apk/sha256sum"
+                  artifacts: "mobile/build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk,mobile/build/app/outputs/flutter-apk/sha256sum"
                   draft: true
                   draft: true

+ 2 - 4
.github/workflows/server-lint.yml

@@ -1,11 +1,9 @@
 name: "Lint (server)"
 name: "Lint (server)"
 
 
 on:
 on:
-    # Run on every push (this also covers pull requests)
+    # Run on every push to a branch other than main that changes server/
     push:
     push:
-        # See: [Note: Specify branch when specifying a path filter]
-        branches: ["**"]
-        # Only run if something changes in these paths
+        branches-ignore: [main]
         paths:
         paths:
             - "server/**"
             - "server/**"
             - ".github/workflows/server-lint.yml"
             - ".github/workflows/server-lint.yml"

+ 11 - 4
.github/workflows/web-crowdin.yml

@@ -3,15 +3,22 @@ name: "Sync Crowdin translations (web)"
 on:
 on:
     push:
     push:
         paths:
         paths:
-            # Run action when web's en-US/translation.json is changed
+            # Run workflow when web's en-US/translation.json is changed
             - "web/apps/photos/public/locales/en-US/translation.json"
             - "web/apps/photos/public/locales/en-US/translation.json"
             # Or the workflow itself is changed
             # Or the workflow itself is changed
             - ".github/workflows/web-crowdin.yml"
             - ".github/workflows/web-crowdin.yml"
         branches: [main]
         branches: [main]
     schedule:
     schedule:
-        # Run every 24 hours - https://crontab.guru/#0_*/24_*_*_*
-        - cron: "0 */24 * * *"
-    workflow_dispatch: # Allow manually running the action
+        # [Note: Run workflow on specific days of the week]
+        #
+        # The last (5th) component of the cron syntax denotes the day of the
+        # week, with 0 == SUN and 6 == SAT. So, for example, to run on every TUE
+        # and FRI, this can be set to `2,5`.
+        #
+        # See also: [Note: Run workflow every 24 hours]
+        - cron: "20 1 * * 2,5"
+    # Also allow manually running the workflow
+    workflow_dispatch:
 
 
 jobs:
 jobs:
     synchronize-with-crowdin:
     synchronize-with-crowdin:

+ 43 - 0
.github/workflows/web-deploy-accounts.yml

@@ -0,0 +1,43 @@
+name: "Deploy (accounts)"
+
+on:
+    push:
+        # Run workflow on pushes to the deploy/accounts
+        branches: [deploy/accounts]
+
+jobs:
+    deploy:
+        runs-on: ubuntu-latest
+
+        defaults:
+            run:
+                working-directory: web
+
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
+              with:
+                  submodules: recursive
+
+            - name: Setup node and enable yarn caching
+              uses: actions/setup-node@v4
+              with:
+                  node-version: 20
+                  cache: "yarn"
+                  cache-dependency-path: "docs/yarn.lock"
+
+            - name: Install dependencies
+              run: yarn install
+
+            - name: Build accounts
+              run: yarn build:accounts
+
+            - name: Publish accounts
+              uses: cloudflare/pages-action@1
+              with:
+                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+                  projectName: ente
+                  branch: deploy/accounts
+                  directory: web/apps/accounts/out
+                  wranglerVersion: "3"

+ 43 - 0
.github/workflows/web-deploy-auth.yml

@@ -0,0 +1,43 @@
+name: "Deploy (auth)"
+
+on:
+    push:
+        # Run workflow on pushes to the deploy/auth
+        branches: [deploy/auth]
+
+jobs:
+    deploy:
+        runs-on: ubuntu-latest
+
+        defaults:
+            run:
+                working-directory: web
+
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
+              with:
+                  submodules: recursive
+
+            - name: Setup node and enable yarn caching
+              uses: actions/setup-node@v4
+              with:
+                  node-version: 20
+                  cache: "yarn"
+                  cache-dependency-path: "docs/yarn.lock"
+
+            - name: Install dependencies
+              run: yarn install
+
+            - name: Build auth
+              run: yarn build:auth
+
+            - name: Publish auth
+              uses: cloudflare/pages-action@1
+              with:
+                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+                  projectName: ente
+                  branch: deploy/auth
+                  directory: web/apps/auth/out
+                  wranglerVersion: "3"

+ 43 - 0
.github/workflows/web-deploy-cast.yml

@@ -0,0 +1,43 @@
+name: "Deploy (cast)"
+
+on:
+    push:
+        # Run workflow on pushes to the deploy/cast
+        branches: [deploy/cast]
+
+jobs:
+    deploy:
+        runs-on: ubuntu-latest
+
+        defaults:
+            run:
+                working-directory: web
+
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
+              with:
+                  submodules: recursive
+
+            - name: Setup node and enable yarn caching
+              uses: actions/setup-node@v4
+              with:
+                  node-version: 20
+                  cache: "yarn"
+                  cache-dependency-path: "docs/yarn.lock"
+
+            - name: Install dependencies
+              run: yarn install
+
+            - name: Build cast
+              run: yarn build:cast
+
+            - name: Publish cast
+              uses: cloudflare/pages-action@1
+              with:
+                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+                  projectName: ente
+                  branch: deploy/cast
+                  directory: web/apps/cast/out
+                  wranglerVersion: "3"

+ 43 - 0
.github/workflows/web-deploy-photos.yml

@@ -0,0 +1,43 @@
+name: "Deploy (photos)"
+
+on:
+    push:
+        # Run workflow on pushes to the deploy/photos
+        branches: [deploy/photos]
+
+jobs:
+    deploy:
+        runs-on: ubuntu-latest
+
+        defaults:
+            run:
+                working-directory: web
+
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
+              with:
+                  submodules: recursive
+
+            - name: Setup node and enable yarn caching
+              uses: actions/setup-node@v4
+              with:
+                  node-version: 20
+                  cache: "yarn"
+                  cache-dependency-path: "docs/yarn.lock"
+
+            - name: Install dependencies
+              run: yarn install
+
+            - name: Build photos
+              run: yarn build:photos
+
+            - name: Publish photos
+              uses: cloudflare/pages-action@1
+              with:
+                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+                  projectName: ente
+                  branch: deploy/photos
+                  directory: web/apps/photos/out
+                  wranglerVersion: "3"

+ 2 - 13
.github/workflows/web-lint.yml

@@ -1,20 +1,9 @@
 name: "Lint (web)"
 name: "Lint (web)"
 
 
 on:
 on:
-    # Run on every push (this also covers pull requests)
+    # Run on every push to a branch other than main that changes web/
     push:
     push:
-        # [Note: Specify branch when specifying a path filter]
-        #
-        # Path filters are ignored for tag pushes, which causes this workflow to
-        # always run when we push a tag. Defining an explicit branch solves the
-        # issue. From GitHub's docs:
-        #
-        # > if you define both branches/branches-ignore and paths/paths-ignore,
-        # > the workflow will only run when both filters are satisfied.
-        #
-        # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
-        branches: ["**"]
-        # Only run if something changes in these paths
+        branches-ignore: [main]
         paths:
         paths:
             - "web/**"
             - "web/**"
             - ".github/workflows/web-lint.yml"
             - ".github/workflows/web-lint.yml"

+ 94 - 0
.github/workflows/web-nightly.yml

@@ -0,0 +1,94 @@
+name: "Nightly (web)"
+
+on:
+    schedule:
+        # [Note: Run workflow every 24 hours]
+        #
+        # Run every 24 hours - First field is minute, second is hour of the day
+        # This runs 23:15 UTC everyday - 1 and 15 are just arbitrary offset to
+        # avoid scheduling it on the exact hour, as suggested by GitHub.
+        #
+        # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
+        # https://crontab.guru/
+        #
+        - cron: "15 23 * * *"
+    # Also allow manually running the workflow
+    workflow_dispatch:
+
+jobs:
+    deploy:
+        runs-on: ubuntu-latest
+
+        defaults:
+            run:
+                working-directory: web
+
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
+              with:
+                  submodules: recursive
+
+            - name: Setup node and enable yarn caching
+              uses: actions/setup-node@v4
+              with:
+                  node-version: 20
+                  cache: "yarn"
+                  cache-dependency-path: "docs/yarn.lock"
+
+            - name: Install dependencies
+              run: yarn install
+
+            - name: Build accounts
+              run: yarn build:accounts
+
+            - name: Publish accounts
+              uses: cloudflare/pages-action@1
+              with:
+                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+                  projectName: ente
+                  branch: n-accounts
+                  directory: web/apps/accounts/out
+                  wranglerVersion: "3"
+
+            - name: Build auth
+              run: yarn build:auth
+
+            - name: Publish auth
+              uses: cloudflare/pages-action@1
+              with:
+                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+                  projectName: ente
+                  branch: n-auth
+                  directory: web/apps/auth/out
+                  wranglerVersion: "3"
+
+            - name: Build cast
+              run: yarn build:cast
+
+            - name: Publish cast
+              uses: cloudflare/pages-action@1
+              with:
+                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+                  projectName: ente
+                  branch: n-cast
+                  directory: web/apps/cast/out
+                  wranglerVersion: "3"
+
+            - name: Build photos
+              run: yarn build:photos
+              env:
+                  NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT: https://albums.ente.sh
+
+            - name: Publish photos
+              uses: cloudflare/pages-action@1
+              with:
+                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+                  projectName: ente
+                  branch: n-photos
+                  directory: web/apps/photos/out
+                  wranglerVersion: "3"

+ 52 - 0
.github/workflows/web-preview.yml

@@ -0,0 +1,52 @@
+name: "Preview (web)"
+
+on:
+    workflow_dispatch:
+        inputs:
+            app:
+                description: "App to build and deploy"
+                type: choice
+                required: true
+                default: "photos"
+                options:
+                    - "accounts"
+                    - "auth"
+                    - "cast"
+                    - "photos"
+
+jobs:
+    deploy:
+        runs-on: ubuntu-latest
+
+        defaults:
+            run:
+                working-directory: web
+
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
+              with:
+                  submodules: recursive
+
+            - name: Setup node and enable yarn caching
+              uses: actions/setup-node@v4
+              with:
+                  node-version: 20
+                  cache: "yarn"
+                  cache-dependency-path: "docs/yarn.lock"
+
+            - name: Install dependencies
+              run: yarn install
+
+            - name: Build ${{ inputs.app }}
+              run: yarn build:${{ inputs.app }}
+
+            - name: Publish ${{ inputs.app }} to preview
+              uses: cloudflare/pages-action@1
+              with:
+                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+                  projectName: ente
+                  branch: preview
+                  directory: web/apps/${{ inputs.app }}/out
+                  wranglerVersion: "3"

+ 0 - 11
.gitmodules

@@ -9,20 +9,9 @@
 [submodule "auth/assets/simple-icons"]
 [submodule "auth/assets/simple-icons"]
 	path = auth/assets/simple-icons
 	path = auth/assets/simple-icons
 	url = https://github.com/simple-icons/simple-icons.git
 	url = https://github.com/simple-icons/simple-icons.git
-[submodule "desktop/thirdparty/next-electron-server"]
-	path = desktop/thirdparty/next-electron-server
-	url = https://github.com/ente-io/next-electron-server.git
-	branch = desktop
-[submodule "mobile/thirdparty/flutter"]
-	path = mobile/thirdparty/flutter
-	url = https://github.com/flutter/flutter.git
-	branch = stable
 [submodule "mobile/plugins/clip_ggml"]
 [submodule "mobile/plugins/clip_ggml"]
 	path = mobile/plugins/clip_ggml
 	path = mobile/plugins/clip_ggml
 	url = https://github.com/ente-io/clip-ggml.git
 	url = https://github.com/ente-io/clip-ggml.git
-[submodule "mobile/thirdparty/isar"]
-	path = mobile/thirdparty/isar
-	url = https://github.com/isar/isar
 [submodule "web/apps/photos/thirdparty/ffmpeg-wasm"]
 [submodule "web/apps/photos/thirdparty/ffmpeg-wasm"]
 	path = web/apps/photos/thirdparty/ffmpeg-wasm
 	path = web/apps/photos/thirdparty/ffmpeg-wasm
 	url = https://github.com/abhinavkgrd/ffmpeg.wasm.git
 	url = https://github.com/abhinavkgrd/ffmpeg.wasm.git

+ 2 - 2
CONTRIBUTING.md

@@ -50,13 +50,13 @@ Thank you for your support.
 
 
 ## Document
 ## Document
 
 
-_Coming soon!_
-
 The help guides and FAQs for users of Ente products are also open source, and
 The help guides and FAQs for users of Ente products are also open source, and
 can be edited in a wiki-esque manner by our community members. More than the
 can be edited in a wiki-esque manner by our community members. More than the
 quantity, we feel this helps improve the quality and approachability of the
 quantity, we feel this helps improve the quality and approachability of the
 documentation by bringing in more diverse viewpoints and familiarity levels.
 documentation by bringing in more diverse viewpoints and familiarity levels.
 
 
+See [docs/](docs/README.md) for how to edit these documents.
+
 ## Code contributions
 ## Code contributions
 
 
 If you'd like to contribute code, it is best to start small.
 If you'd like to contribute code, it is best to start small.

+ 1 - 1
README.md

@@ -70,7 +70,7 @@ existing users will be grandfathered in.
 [<img height="42" src=".github/assets/app-store-badge.svg">](https://apps.apple.com/app/id6444121398)
 [<img height="42" src=".github/assets/app-store-badge.svg">](https://apps.apple.com/app/id6444121398)
 [<img height="42" src=".github/assets/play-store-badge.png">](https://play.google.com/store/apps/details?id=io.ente.auth)
 [<img height="42" src=".github/assets/play-store-badge.png">](https://play.google.com/store/apps/details?id=io.ente.auth)
 [<img height="42" src=".github/assets/f-droid-badge.png">](https://f-droid.org/packages/io.ente.auth/)
 [<img height="42" src=".github/assets/f-droid-badge.png">](https://f-droid.org/packages/io.ente.auth/)
-[<img height="42" src=".github/assets/github-badge.png">](https://github.com/ente-io/ente/releases?q=tag%3Av2.0.34&expanded=true)
+[<img height="42" src=".github/assets/github-badge.png">](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
 [<img height="42" src=".github/assets/web-badge.svg">](https://auth.ente.io)
 [<img height="42" src=".github/assets/web-badge.svg">](https://auth.ente.io)
 
 
 </div>
 </div>

+ 1 - 1
SUPPORT.md

@@ -7,7 +7,7 @@ details as possible about whatever it is that you need help with, and we will
 get back to you as soon as possible.
 get back to you as soon as possible.
 
 
 In some cases, your query might already have been answered in our help
 In some cases, your query might already have been answered in our help
-documentation (_Coming soon!_).
+documentation at [help.ente.io](https://help.ente.io).
 
 
 Other ways to get in touch are:
 Other ways to get in touch are:
 
 

+ 3 - 0
auth/.gitignore

@@ -9,6 +9,9 @@
 .history
 .history
 .svn/
 .svn/
 
 
+# Editors
+.vscode/
+
 # IntelliJ related
 # IntelliJ related
 *.iml
 *.iml
 *.ipr
 *.ipr

+ 1 - 1
auth/README.md

@@ -12,7 +12,7 @@ multi-device sync.
 ### Android
 ### Android
 
 
 This repository's [GitHub
 This repository's [GitHub
-releases](https://github.com/ente-io/ente/releases/latest/download/ente-auth.apk)
+releases](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
 contains APKs, built straight from source. These builds keep themselves updated,
 contains APKs, built straight from source. These builds keep themselves updated,
 without relying on third party stores.
 without relying on third party stores.
 
 

+ 3 - 0
auth/analysis_options.yaml

@@ -27,6 +27,7 @@ linter:
     - use_rethrow_when_possible
     - use_rethrow_when_possible
     - directives_ordering
     - directives_ordering
     - always_use_package_imports
     - always_use_package_imports
+    - unawaited_futures
 
 
 analyzer:
 analyzer:
   errors:
   errors:
@@ -45,6 +46,8 @@ analyzer:
     prefer_const_declarations: warning
     prefer_const_declarations: warning
     prefer_const_constructors_in_immutables: ignore # too many warnings
     prefer_const_constructors_in_immutables: ignore # too many warnings
 
 
+    unawaited_futures: warning # convert to warning after fixing existing issues
+
     avoid_renaming_method_parameters: ignore # incorrect warnings for `equals` overrides
     avoid_renaming_method_parameters: ignore # incorrect warnings for `equals` overrides
 
 
   exclude:
   exclude:

+ 8 - 16
auth/assets/custom-icons/_data/custom-icons.json

@@ -37,8 +37,7 @@
     {
     {
       "title": "BorgBase",
       "title": "BorgBase",
       "altNames": ["borg"],
       "altNames": ["borg"],
-      "slug": "BorgBase",
-      "hex": "222C31"
+      "slug": "BorgBase"
     },
     },
     {
     {
       "title": "Brave Creators",
       "title": "Brave Creators",
@@ -109,8 +108,7 @@
     {
     {
       "title": "Gosuslugi",
       "title": "Gosuslugi",
       "altNames": ["Госуслуги"],
       "altNames": ["Госуслуги"],
-      "slug": "Gosuslugi",
-      "hex": "EE2F53"
+      "slug": "Gosuslugi"
     },
     },
     {
     {
       "title": "Healthchecks.io",
       "title": "Healthchecks.io",
@@ -127,13 +125,11 @@
     },
     },
     {
     {
       "title": "IVPN",
       "title": "IVPN",
-      "slug": "IVPN",
-      "hex": "FA3243"
+      "slug": "IVPN"
     },
     },
     {
     {
       "title": "IceDrive",
       "title": "IceDrive",
-      "slug": "Icedrive",
-      "hex": "1F4FD0"
+      "slug": "Icedrive"
     },
     },
     {
     {
       "title": "Jagex",
       "title": "Jagex",
@@ -154,8 +150,7 @@
       "title": "Kite"
       "title": "Kite"
     },
     },
     {
     {
-      "title": "Koofr",
-      "hex": "71BA05"
+      "title": "Koofr"
     },
     },
     {
     {
       "title": "Kraken",
       "title": "Kraken",
@@ -184,8 +179,7 @@
     {
     {
       "title": "Murena",
       "title": "Murena",
       "altNames": ["eCloud"],
       "altNames": ["eCloud"],
-      "slug": "ecloud",
-      "hex": "EC6A55"
+      "slug": "ecloud"
     },
     },
     {
     {
       "title": "Microsoft"
       "title": "Microsoft"
@@ -230,8 +224,7 @@
     },
     },
     {
     {
       "title": "pCloud",
       "title": "pCloud",
-      "slug": "pCloud",
-      "hex": "1EBCC5"
+      "slug": "pCloud"
     },
     },
     {
     {
       "title": "Peerberry",
       "title": "Peerberry",
@@ -371,8 +364,7 @@
     {
     {
       "title": "Yandex",
       "title": "Yandex",
       "altNames": ["Ya", "Яндекс"],
       "altNames": ["Ya", "Яндекс"],
-      "slug": "Yandex",
-      "hex": "FC3F1D"
+      "slug": "Yandex"
     }
     }
   ]
   ]
 }
 }

+ 14 - 2
auth/docs/release.md

@@ -1,7 +1,14 @@
 # Releases
 # Releases
 
 
-Create a PR to bump up the version in `pubspec.yaml`. Once that is merged, tag
-main, and push the tag.
+Create a PR to bump up the version in `pubspec.yaml`.
+
+> [!NOTE]
+>
+> Use [semver](https://semver.org/) for the tags, with `auth-` as a prefix.
+> Multiple beta releases for the same upcoming version can be done by adding
+> build metadata at the end, e.g. `auth-v1.2.3-beta+3`.
+
+Once that is merged, tag main, and push the tag.
 
 
 ```sh
 ```sh
 git tag auth-v1.2.3
 git tag auth-v1.2.3
@@ -16,6 +23,11 @@ This'll trigger a GitHub workflow that:
 * Creates a new release in the internal track on Play Store.
 * Creates a new release in the internal track on Play Store.
 
 
 Once the workflow completes, go to the draft GitHub release that was created.
 Once the workflow completes, go to the draft GitHub release that was created.
+
+> [!NOTE]
+>
+> Keep the title of the release same as the tag.
+
 Set "Previous tag" to the last release of auth and press "Generate release
 Set "Previous tag" to the last release of auth and press "Generate release
 notes". The generated release note will contain all PRs and new contributors
 notes". The generated release note will contain all PRs and new contributors
 from all the releases in the monorepo, so you'll need to filter them to keep
 from all the releases in the monorepo, so you'll need to filter them to keep

+ 1 - 1
auth/flutter

@@ -1 +1 @@
-Subproject commit ba393198430278b6595976de84fe170f553cc728
+Subproject commit 41456452f29d64e8deb623a3c927524bcf9f111b

+ 0 - 6
auth/ios/Podfile.lock

@@ -70,8 +70,6 @@ PODS:
   - move_to_background (0.0.1):
   - move_to_background (0.0.1):
     - Flutter
     - Flutter
   - MTBBarcodeScanner (5.0.11)
   - MTBBarcodeScanner (5.0.11)
-  - open_filex (0.0.2):
-    - Flutter
   - OrderedSet (5.0.0)
   - OrderedSet (5.0.0)
   - package_info_plus (0.4.5):
   - package_info_plus (0.4.5):
     - Flutter
     - Flutter
@@ -143,7 +141,6 @@ DEPENDENCIES:
   - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
   - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
   - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
   - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
   - move_to_background (from `.symlinks/plugins/move_to_background/ios`)
   - move_to_background (from `.symlinks/plugins/move_to_background/ios`)
-  - open_filex (from `.symlinks/plugins/open_filex/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
   - privacy_screen (from `.symlinks/plugins/privacy_screen/ios`)
   - privacy_screen (from `.symlinks/plugins/privacy_screen/ios`)
@@ -204,8 +201,6 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/local_auth_ios/ios"
     :path: ".symlinks/plugins/local_auth_ios/ios"
   move_to_background:
   move_to_background:
     :path: ".symlinks/plugins/move_to_background/ios"
     :path: ".symlinks/plugins/move_to_background/ios"
-  open_filex:
-    :path: ".symlinks/plugins/open_filex/ios"
   package_info_plus:
   package_info_plus:
     :path: ".symlinks/plugins/package_info_plus/ios"
     :path: ".symlinks/plugins/package_info_plus/ios"
   path_provider_foundation:
   path_provider_foundation:
@@ -251,7 +246,6 @@ SPEC CHECKSUMS:
   local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac
   local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac
   move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
   move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
   MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
   MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
-  open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
   OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
   OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
   package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
   package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
   path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
   path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c

+ 8 - 1
auth/lib/core/configuration.dart

@@ -6,6 +6,7 @@ import 'dart:typed_data';
 import 'package:bip39/bip39.dart' as bip39;
 import 'package:bip39/bip39.dart' as bip39;
 import 'package:ente_auth/core/constants.dart';
 import 'package:ente_auth/core/constants.dart';
 import 'package:ente_auth/core/event_bus.dart';
 import 'package:ente_auth/core/event_bus.dart';
+import 'package:ente_auth/events/endpoint_updated_event.dart';
 import 'package:ente_auth/events/signed_in_event.dart';
 import 'package:ente_auth/events/signed_in_event.dart';
 import 'package:ente_auth/events/signed_out_event.dart';
 import 'package:ente_auth/events/signed_out_event.dart';
 import 'package:ente_auth/models/key_attributes.dart';
 import 'package:ente_auth/models/key_attributes.dart';
@@ -42,6 +43,7 @@ class Configuration {
   static const userIDKey = "user_id";
   static const userIDKey = "user_id";
   static const hasMigratedSecureStorageKey = "has_migrated_secure_storage";
   static const hasMigratedSecureStorageKey = "has_migrated_secure_storage";
   static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode";
   static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode";
+  static const endPointKey = "endpoint";
   final List<String> onlineSecureKeys = [
   final List<String> onlineSecureKeys = [
     keyKey,
     keyKey,
     secretKeyKey,
     secretKeyKey,
@@ -318,7 +320,12 @@ class Configuration {
   }
   }
 
 
   String getHttpEndpoint() {
   String getHttpEndpoint() {
-    return endpoint;
+    return _preferences.getString(endPointKey) ?? endpoint;
+  }
+
+  Future<void> setHttpEndpoint(String endpoint) async {
+    await _preferences.setString(endPointKey, endpoint);
+    Bus.instance.fire(EndpointUpdatedEvent());
   }
   }
 
 
   String? getToken() {
   String? getToken() {

+ 3 - 3
auth/lib/core/logging/super_logging.dart

@@ -167,7 +167,7 @@ class SuperLogging {
       await setupLogDir();
       await setupLogDir();
     }
     }
     if (sentryIsEnabled) {
     if (sentryIsEnabled) {
-      setupSentry();
+      setupSentry().ignore();
     }
     }
 
 
     Logger.root.level = Level.ALL;
     Logger.root.level = Level.ALL;
@@ -250,7 +250,7 @@ class SuperLogging {
 
 
     // add error to sentry queue
     // add error to sentry queue
     if (sentryIsEnabled && rec.error != null) {
     if (sentryIsEnabled && rec.error != null) {
-      _sendErrorToSentry(rec.error!, null);
+      _sendErrorToSentry(rec.error!, null).ignore();
     }
     }
   }
   }
 
 
@@ -289,7 +289,7 @@ class SuperLogging {
     SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
     SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
     await for (final error in sentryQueueControl.stream.asBroadcastStream()) {
     await for (final error in sentryQueueControl.stream.asBroadcastStream()) {
       try {
       try {
-        Sentry.captureException(
+        await Sentry.captureException(
           error,
           error,
         );
         );
       } catch (e) {
       } catch (e) {

+ 24 - 16
auth/lib/core/network.dart

@@ -13,11 +13,6 @@ import 'package:uuid/uuid.dart';
 int kConnectTimeout = 15000;
 int kConnectTimeout = 15000;
 
 
 class Network {
 class Network {
-  // apiEndpoint points to the Ente server's API endpoint
-  static const apiEndpoint = String.fromEnvironment(
-    "endpoint",
-    defaultValue: kDefaultProductionEndpoint,
-  );
   late Dio _dio;
   late Dio _dio;
   late Dio _enteDio;
   late Dio _enteDio;
 
 
@@ -41,7 +36,7 @@ class Network {
         },
         },
       ),
       ),
     );
     );
-    _dio.interceptors.add(RequestIdInterceptor());
+
     _enteDio = Dio(
     _enteDio = Dio(
       BaseOptions(
       BaseOptions(
         baseUrl: apiEndpoint,
         baseUrl: apiEndpoint,
@@ -56,7 +51,13 @@ class Network {
         },
         },
       ),
       ),
     );
     );
-    _enteDio.interceptors.add(EnteRequestInterceptor(preferences, apiEndpoint));
+    _setupInterceptors(endpoint);
+
+    Bus.instance.on<EndpointUpdatedEvent>().listen((event) {
+      final endpoint = Configuration.instance.getHttpEndpoint();
+      _enteDio.options.baseUrl = endpoint;
+      _setupInterceptors(endpoint);
+    });
   }
   }
 
 
   Network._privateConstructor();
   Network._privateConstructor();
@@ -65,34 +66,41 @@ class Network {
 
 
   Dio getDio() => _dio;
   Dio getDio() => _dio;
   Dio get enteDio => _enteDio;
   Dio get enteDio => _enteDio;
+
+  void _setupInterceptors(String endpoint) {
+    _dio.interceptors.clear();
+    _dio.interceptors.add(RequestIdInterceptor());
+
+    _enteDio.interceptors.clear();
+    _enteDio.interceptors.add(EnteRequestInterceptor(endpoint));
+  }
 }
 }
 
 
 class RequestIdInterceptor extends Interceptor {
 class RequestIdInterceptor extends Interceptor {
   @override
   @override
   void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
   void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
-    // ignore: prefer_const_constructors
-    options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString());
+    options.headers
+        .putIfAbsent("x-request-id", () => const Uuid().v4().toString());
     return super.onRequest(options, handler);
     return super.onRequest(options, handler);
   }
   }
 }
 }
 
 
 class EnteRequestInterceptor extends Interceptor {
 class EnteRequestInterceptor extends Interceptor {
-  final SharedPreferences _preferences;
-  final String enteEndpoint;
+  final String endpoint;
 
 
-  EnteRequestInterceptor(this._preferences, this.enteEndpoint);
+  EnteRequestInterceptor(this.endpoint);
 
 
   @override
   @override
   void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
   void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
     if (kDebugMode) {
     if (kDebugMode) {
       assert(
       assert(
-        options.baseUrl == enteEndpoint,
+        options.baseUrl == endpoint,
         "interceptor should only be used for API endpoint",
         "interceptor should only be used for API endpoint",
       );
       );
     }
     }
-    // ignore: prefer_const_constructors
-    options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString());
-    final String? tokenValue = _preferences.getString(Configuration.tokenKey);
+    options.headers
+        .putIfAbsent("x-request-id", () => const Uuid().v4().toString());
+    final String? tokenValue = Configuration.instance.getToken();
     if (tokenValue != null) {
     if (tokenValue != null) {
       options.headers.putIfAbsent("X-Auth-Token", () => tokenValue);
       options.headers.putIfAbsent("X-Auth-Token", () => tokenValue);
     }
     }

+ 3 - 0
auth/lib/events/endpoint_updated_event.dart

@@ -0,0 +1,3 @@
+import 'package:ente_auth/events/event.dart';
+
+class EndpointUpdatedEvent extends Event {}

+ 15 - 49
auth/lib/gateway/authenticator.dart

@@ -1,43 +1,29 @@
 import 'package:dio/dio.dart';
 import 'package:dio/dio.dart';
-import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/core/errors.dart';
 import 'package:ente_auth/core/errors.dart';
+import 'package:ente_auth/core/network.dart';
 import 'package:ente_auth/models/authenticator/auth_entity.dart';
 import 'package:ente_auth/models/authenticator/auth_entity.dart';
 import 'package:ente_auth/models/authenticator/auth_key.dart';
 import 'package:ente_auth/models/authenticator/auth_key.dart';
 
 
 class AuthenticatorGateway {
 class AuthenticatorGateway {
-  final Dio _dio;
-  final Configuration _config;
-  late String _basedEndpoint;
+  late Dio _enteDio;
 
 
-  AuthenticatorGateway(this._dio, this._config) {
-    _basedEndpoint = "${_config.getHttpEndpoint()}/authenticator";
+  AuthenticatorGateway() {
+    _enteDio = Network.instance.enteDio;
   }
   }
 
 
   Future<void> createKey(String encKey, String header) async {
   Future<void> createKey(String encKey, String header) async {
-    await _dio.post(
-      "$_basedEndpoint/key",
+    await _enteDio.post(
+      "/authenticator/key",
       data: {
       data: {
         "encryptedKey": encKey,
         "encryptedKey": encKey,
         "header": header,
         "header": header,
       },
       },
-      options: Options(
-        headers: {
-          "X-Auth-Token": _config.getToken(),
-        },
-      ),
     );
     );
   }
   }
 
 
   Future<AuthKey> getKey() async {
   Future<AuthKey> getKey() async {
     try {
     try {
-      final response = await _dio.get(
-        "$_basedEndpoint/key",
-        options: Options(
-          headers: {
-            "X-Auth-Token": _config.getToken(),
-          },
-        ),
-      );
+      final response = await _enteDio.get("/authenticator/key");
       return AuthKey.fromMap(response.data);
       return AuthKey.fromMap(response.data);
     } on DioException catch (e) {
     } on DioException catch (e) {
       if (e.response != null && (e.response!.statusCode ?? 0) == 404) {
       if (e.response != null && (e.response!.statusCode ?? 0) == 404) {
@@ -51,17 +37,12 @@ class AuthenticatorGateway {
   }
   }
 
 
   Future<AuthEntity> createEntity(String encryptedData, String header) async {
   Future<AuthEntity> createEntity(String encryptedData, String header) async {
-    final response = await _dio.post(
-      "$_basedEndpoint/entity",
+    final response = await _enteDio.post(
+      "/authenticator/entity",
       data: {
       data: {
         "encryptedData": encryptedData,
         "encryptedData": encryptedData,
         "header": header,
         "header": header,
       },
       },
-      options: Options(
-        headers: {
-          "X-Auth-Token": _config.getToken(),
-        },
-      ),
     );
     );
     return AuthEntity.fromMap(response.data);
     return AuthEntity.fromMap(response.data);
   }
   }
@@ -71,50 +52,35 @@ class AuthenticatorGateway {
     String encryptedData,
     String encryptedData,
     String header,
     String header,
   ) async {
   ) async {
-    await _dio.put(
-      "$_basedEndpoint/entity",
+    await _enteDio.put(
+      "/authenticator/entity",
       data: {
       data: {
         "id": id,
         "id": id,
         "encryptedData": encryptedData,
         "encryptedData": encryptedData,
         "header": header,
         "header": header,
       },
       },
-      options: Options(
-        headers: {
-          "X-Auth-Token": _config.getToken(),
-        },
-      ),
     );
     );
   }
   }
 
 
   Future<void> deleteEntity(
   Future<void> deleteEntity(
     String id,
     String id,
   ) async {
   ) async {
-    await _dio.delete(
-      "$_basedEndpoint/entity",
+    await _enteDio.delete(
+      "/authenticator/entity",
       queryParameters: {
       queryParameters: {
         "id": id,
         "id": id,
       },
       },
-      options: Options(
-        headers: {
-          "X-Auth-Token": _config.getToken(),
-        },
-      ),
     );
     );
   }
   }
 
 
   Future<List<AuthEntity>> getDiff(int sinceTime, {int limit = 500}) async {
   Future<List<AuthEntity>> getDiff(int sinceTime, {int limit = 500}) async {
     try {
     try {
-      final response = await _dio.get(
-        "$_basedEndpoint/entity/diff",
+      final response = await _enteDio.get(
+        "/authenticator/entity/diff",
         queryParameters: {
         queryParameters: {
           "sinceTime": sinceTime,
           "sinceTime": sinceTime,
           "limit": limit,
           "limit": limit,
         },
         },
-        options: Options(
-          headers: {
-            "X-Auth-Token": _config.getToken(),
-          },
-        ),
       );
       );
       final List<AuthEntity> authEntities = <AuthEntity>[];
       final List<AuthEntity> authEntities = <AuthEntity>[];
       final diff = response.data["diff"] as List;
       final diff = response.data["diff"] as List;

+ 1 - 0
auth/lib/l10n/arb/app_bg.arb

@@ -0,0 +1 @@
+{}

+ 12 - 1
auth/lib/l10n/arb/app_de.arb

@@ -144,6 +144,7 @@
   "enterCodeHint": "Geben Sie den 6-stelligen Code \naus Ihrer Authentifikator-App ein.",
   "enterCodeHint": "Geben Sie den 6-stelligen Code \naus Ihrer Authentifikator-App ein.",
   "lostDeviceTitle": "Gerät verloren?",
   "lostDeviceTitle": "Gerät verloren?",
   "twoFactorAuthTitle": "Zwei-Faktor-Authentifizierung",
   "twoFactorAuthTitle": "Zwei-Faktor-Authentifizierung",
+  "passkeyAuthTitle": "Passkey Authentifizierung",
   "recoverAccount": "Konto wiederherstellen",
   "recoverAccount": "Konto wiederherstellen",
   "enterRecoveryKeyHint": "Geben Sie Ihren Wiederherstellungsschlüssel ein",
   "enterRecoveryKeyHint": "Geben Sie Ihren Wiederherstellungsschlüssel ein",
   "recover": "Wiederherstellen",
   "recover": "Wiederherstellen",
@@ -404,5 +405,15 @@
   "signOutOtherDevices": "Andere Geräte abmelden",
   "signOutOtherDevices": "Andere Geräte abmelden",
   "doNotSignOut": "Nicht abmelden",
   "doNotSignOut": "Nicht abmelden",
   "hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)",
   "hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)",
-  "hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!"
+  "hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!",
+  "waitingForBrowserRequest": "Warten auf Browseranfrage...",
+  "launchPasskeyUrlAgain": "Passwort-URL erneut starten",
+  "passkey": "Passkey",
+  "developerSettingsWarning": "Sind Sie sicher, dass Sie die Entwicklereinstellungen ändern möchten?",
+  "developerSettings": "Entwicklereinstellungen",
+  "serverEndpoint": "Server Endpunkt",
+  "invalidEndpoint": "Ungültiger Endpunkt",
+  "invalidEndpointMessage": "Der eingegebene Endpunkt ist ungültig. Bitte geben Sie einen gültigen Endpunkt ein und versuchen Sie es erneut.",
+  "endpointUpdatedMessage": "Endpunkt erfolgreich aktualisiert",
+  "customEndpoint": "Mit {endpoint} verbunden"
 }
 }

+ 11 - 3
auth/lib/l10n/arb/app_en.arb

@@ -144,7 +144,8 @@
   "enterCodeHint": "Enter the 6-digit code from\nyour authenticator app",
   "enterCodeHint": "Enter the 6-digit code from\nyour authenticator app",
   "lostDeviceTitle": "Lost device?",
   "lostDeviceTitle": "Lost device?",
   "twoFactorAuthTitle": "Two-factor authentication",
   "twoFactorAuthTitle": "Two-factor authentication",
-  "passkeyAuthTitle": "Passkey authentication",
+  "passkeyAuthTitle": "Passkey verification",
+  "verifyPasskey": "Verify passkey",
   "recoverAccount": "Recover account",
   "recoverAccount": "Recover account",
   "enterRecoveryKeyHint": "Enter your recovery key",
   "enterRecoveryKeyHint": "Enter your recovery key",
   "recover": "Recover",
   "recover": "Recover",
@@ -412,6 +413,13 @@
   "hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
   "hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
   "recoveryKeySaved": "Recovery key saved in Downloads folder!",
   "recoveryKeySaved": "Recovery key saved in Downloads folder!",
   "waitingForBrowserRequest": "Waiting for browser request...",
   "waitingForBrowserRequest": "Waiting for browser request...",
-  "launchPasskeyUrlAgain": "Launch passkey URL again",
-  "passkey": "Passkey"
+  "waitingForVerification": "Waiting for verification...",
+  "passkey": "Passkey",
+  "developerSettingsWarning":"Are you sure that you want to modify Developer settings?",
+  "developerSettings": "Developer settings",
+  "serverEndpoint": "Server endpoint",
+  "invalidEndpoint": "Invalid endpoint",
+  "invalidEndpointMessage": "Sorry, the endpoint you entered is invalid. Please enter a valid endpoint and try again.",
+  "endpointUpdatedMessage": "Endpoint updated successfully",
+  "customEndpoint": "Connected to {endpoint}"
 }
 }

+ 12 - 1
auth/lib/l10n/arb/app_ja.arb

@@ -144,6 +144,7 @@
   "enterCodeHint": "認証アプリに表示された 6 桁のコードを入力してください",
   "enterCodeHint": "認証アプリに表示された 6 桁のコードを入力してください",
   "lostDeviceTitle": "デバイスを紛失しましたか?",
   "lostDeviceTitle": "デバイスを紛失しましたか?",
   "twoFactorAuthTitle": "2 要素認証",
   "twoFactorAuthTitle": "2 要素認証",
+  "passkeyAuthTitle": "パスキー認証",
   "recoverAccount": "アカウントを回復",
   "recoverAccount": "アカウントを回復",
   "enterRecoveryKeyHint": "回復キーを入力",
   "enterRecoveryKeyHint": "回復キーを入力",
   "recover": "回復",
   "recover": "回復",
@@ -404,5 +405,15 @@
   "signOutOtherDevices": "他のデバイスからサインアウトする",
   "signOutOtherDevices": "他のデバイスからサインアウトする",
   "doNotSignOut": "サインアウトしない",
   "doNotSignOut": "サインアウトしない",
   "hearUsWhereTitle": "Ente についてどのようにお聞きになりましたか?(任意)",
   "hearUsWhereTitle": "Ente についてどのようにお聞きになりましたか?(任意)",
-  "hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!"
+  "hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!",
+  "waitingForBrowserRequest": "ブラウザのリクエストを待っています...",
+  "launchPasskeyUrlAgain": "パスキーのURLを再度起動する",
+  "passkey": "パスキー",
+  "developerSettingsWarning": "開発者向け設定を変更してもよろしいですか?",
+  "developerSettings": "開発者向け設定",
+  "serverEndpoint": "サーバーエンドポイント",
+  "invalidEndpoint": "無効なエンドポイントです",
+  "invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。",
+  "endpointUpdatedMessage": "エンドポイントの更新に成功しました",
+  "customEndpoint": "{endpoint} に接続しました"
 }
 }

+ 12 - 1
auth/lib/l10n/arb/app_pt.arb

@@ -144,6 +144,7 @@
   "enterCodeHint": "Digite o código de 6 dígitos de\nseu aplicativo autenticador",
   "enterCodeHint": "Digite o código de 6 dígitos de\nseu aplicativo autenticador",
   "lostDeviceTitle": "Perdeu seu dispositivo?",
   "lostDeviceTitle": "Perdeu seu dispositivo?",
   "twoFactorAuthTitle": "Autenticação de dois fatores",
   "twoFactorAuthTitle": "Autenticação de dois fatores",
+  "passkeyAuthTitle": "Autenticação via Chave de acesso",
   "recoverAccount": "Recuperar conta",
   "recoverAccount": "Recuperar conta",
   "enterRecoveryKeyHint": "Digite sua chave de recuperação",
   "enterRecoveryKeyHint": "Digite sua chave de recuperação",
   "recover": "Recuperar",
   "recover": "Recuperar",
@@ -404,5 +405,15 @@
   "signOutOtherDevices": "Terminar sessão em outros dispositivos",
   "signOutOtherDevices": "Terminar sessão em outros dispositivos",
   "doNotSignOut": "Não encerrar sessão",
   "doNotSignOut": "Não encerrar sessão",
   "hearUsWhereTitle": "Como você ouviu sobre o Ente? (opcional)",
   "hearUsWhereTitle": "Como você ouviu sobre o Ente? (opcional)",
-  "hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!"
+  "hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!",
+  "waitingForBrowserRequest": "Aguardando solicitação do navegador...",
+  "launchPasskeyUrlAgain": "Iniciar a URL de chave de acesso novamente",
+  "passkey": "Chave de acesso",
+  "developerSettingsWarning": "Tem certeza de que deseja modificar as configurações de Desenvolvedor?",
+  "developerSettings": "Configurações de desenvolvedor",
+  "serverEndpoint": "Endpoint do servidor",
+  "invalidEndpoint": "Endpoint inválido",
+  "invalidEndpointMessage": "Desculpe, o endpoint que você inseriu é inválido. Por favor, insira um endpoint válido e tente novamente.",
+  "endpointUpdatedMessage": "Endpoint atualizado com sucesso",
+  "customEndpoint": "Conectado a {endpoint}"
 }
 }

+ 73 - 1
auth/lib/l10n/arb/app_sv.arb

@@ -59,6 +59,12 @@
   "recreatePassword": "Återskapa lösenord",
   "recreatePassword": "Återskapa lösenord",
   "useRecoveryKey": "Använd återställningsnyckel",
   "useRecoveryKey": "Använd återställningsnyckel",
   "incorrectPasswordTitle": "Felaktigt lösenord",
   "incorrectPasswordTitle": "Felaktigt lösenord",
+  "welcomeBack": "Välkommen tillbaka!",
+  "changePassword": "Ändra lösenord",
+  "cancel": "Avbryt",
+  "yes": "Ja",
+  "no": "Nej",
+  "settings": "Inställningar",
   "pleaseTryAgain": "Försök igen",
   "pleaseTryAgain": "Försök igen",
   "existingUser": "Befintlig användare",
   "existingUser": "Befintlig användare",
   "delete": "Radera",
   "delete": "Radera",
@@ -68,9 +74,23 @@
   "suggestFeatures": "Föreslå funktionalitet",
   "suggestFeatures": "Föreslå funktionalitet",
   "faq": "FAQ",
   "faq": "FAQ",
   "faq_q_1": "Hur säkert är ente Auth?",
   "faq_q_1": "Hur säkert är ente Auth?",
+  "scan": "Skanna",
+  "twoFactorAuthTitle": "Tvåfaktorsautentisering",
+  "enterRecoveryKeyHint": "Ange din återställningsnyckel",
+  "noRecoveryKeyTitle": "Ingen återställningsnyckel?",
+  "enterEmailHint": "Ange din e-postadress",
+  "invalidEmailTitle": "Ogiltig e-postadress",
+  "invalidEmailMessage": "Ange en giltig e-postadress.",
+  "deleteAccount": "Radera konto",
+  "yesSendFeedbackAction": "Ja, skicka feedback",
+  "noDeleteAccountAction": "Nej, radera konto",
+  "createNewAccount": "Skapa nytt konto",
   "weakStrength": "Svag",
   "weakStrength": "Svag",
   "strongStrength": "Stark",
   "strongStrength": "Stark",
   "moderateStrength": "Måttligt",
   "moderateStrength": "Måttligt",
+  "confirmPassword": "Bekräfta lösenord",
+  "close": "Stäng",
+  "language": "Språk",
   "searchHint": "Sök...",
   "searchHint": "Sök...",
   "search": "Sök",
   "search": "Sök",
   "sorryUnableToGenCode": "Tyvärr, det gick inte att generera en kod för {issuerName}",
   "sorryUnableToGenCode": "Tyvärr, det gick inte att generera en kod för {issuerName}",
@@ -83,5 +103,57 @@
   "copiedNextToClipboard": "Kopierade nästa kod till urklipp",
   "copiedNextToClipboard": "Kopierade nästa kod till urklipp",
   "error": "Fel",
   "error": "Fel",
   "recoveryKeyCopiedToClipboard": "Återställningsnyckel kopierad till urklipp",
   "recoveryKeyCopiedToClipboard": "Återställningsnyckel kopierad till urklipp",
-  "recoveryKeyOnForgotPassword": "Om du glömmer ditt lösenord är det enda sättet du kan återställa dina data med denna nyckel."
+  "recoveryKeyOnForgotPassword": "Om du glömmer ditt lösenord är det enda sättet du kan återställa dina data med denna nyckel.",
+  "saveKey": "Spara nyckel",
+  "back": "Tillbaka",
+  "createAccount": "Skapa konto",
+  "password": "Lösenord",
+  "privacyPolicyTitle": "Integritetspolicy",
+  "termsOfServicesTitle": "Villkor",
+  "encryption": "Kryptering",
+  "changePasswordTitle": "Ändra lösenord",
+  "resetPasswordTitle": "Återställ lösenord",
+  "encryptionKeys": "Krypteringsnycklar",
+  "continueLabel": "Fortsätt",
+  "logInLabel": "Logga in",
+  "logout": "Logga ut",
+  "areYouSureYouWantToLogout": "Är du säker på att du vill logga ut?",
+  "yesLogout": "Ja, logga ut",
+  "invalidKey": "Ogiltig nyckel",
+  "tryAgain": "Försök igen",
+  "viewRecoveryKey": "Visa återställningsnyckel",
+  "confirmRecoveryKey": "Bekräfta återställningsnyckel",
+  "confirmYourRecoveryKey": "Bekräfta din återställningsnyckel",
+  "confirm": "Bekräfta",
+  "copyEmailAddress": "Kopiera e-postadress",
+  "exportLogs": "Exportera loggar",
+  "enterYourRecoveryKey": "Ange din återställningsnyckel",
+  "about": "Om",
+  "terms": "Villkor",
+  "warning": "Varning",
+  "importSuccessDesc": "Du har importerat {count} koder!",
+  "@importSuccessDesc": {
+    "placeholders": {
+      "count": {
+        "description": "The number of codes imported",
+        "type": "int",
+        "example": "1"
+      }
+    }
+  },
+  "pendingSyncs": "Varning",
+  "activeSessions": "Aktiva sessioner",
+  "enterPassword": "Ange lösenord",
+  "export": "Exportera",
+  "singIn": "Logga in",
+  "androidCancelButton": "Avbryt",
+  "@androidCancelButton": {
+    "description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters."
+  },
+  "iOSOkButton": "OK",
+  "@iOSOkButton": {
+    "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
+  },
+  "noInternetConnection": "Ingen internetanslutning",
+  "pleaseCheckYourInternetConnectionAndTryAgain": "Kontrollera din internetanslutning och försök igen."
 }
 }

+ 12 - 1
auth/lib/l10n/arb/app_zh.arb

@@ -144,6 +144,7 @@
   "enterCodeHint": "从你的身份验证器应用中\n输入6位数字代码",
   "enterCodeHint": "从你的身份验证器应用中\n输入6位数字代码",
   "lostDeviceTitle": "丢失了设备吗?",
   "lostDeviceTitle": "丢失了设备吗?",
   "twoFactorAuthTitle": "双因素认证",
   "twoFactorAuthTitle": "双因素认证",
+  "passkeyAuthTitle": "通行密钥认证",
   "recoverAccount": "恢复账户",
   "recoverAccount": "恢复账户",
   "enterRecoveryKeyHint": "输入您的恢复密钥",
   "enterRecoveryKeyHint": "输入您的恢复密钥",
   "recover": "恢复",
   "recover": "恢复",
@@ -404,5 +405,15 @@
   "signOutOtherDevices": "登出其他设备",
   "signOutOtherDevices": "登出其他设备",
   "doNotSignOut": "不要退登",
   "doNotSignOut": "不要退登",
   "hearUsWhereTitle": "您是如何知道Ente的? (可选的)",
   "hearUsWhereTitle": "您是如何知道Ente的? (可选的)",
-  "hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!"
+  "hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!",
+  "waitingForBrowserRequest": "正在等待浏览器请求...",
+  "launchPasskeyUrlAgain": "再次启动 通行密钥 URL",
+  "passkey": "通行密钥",
+  "developerSettingsWarning": "您确定要修改开发者设置吗?",
+  "developerSettings": "开发者设置",
+  "serverEndpoint": "服务器端点",
+  "invalidEndpoint": "端点无效",
+  "invalidEndpointMessage": "抱歉,您输入的端点无效。请输入有效的端点,然后重试。",
+  "endpointUpdatedMessage": "端点更新成功",
+  "customEndpoint": "已连接至 {endpoint}"
 }
 }

+ 2 - 1
auth/lib/main.dart

@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'dart:io';
 import 'dart:io';
 
 
 import 'package:adaptive_theme/adaptive_theme.dart';
 import 'package:adaptive_theme/adaptive_theme.dart';
@@ -59,7 +60,7 @@ Future<void> _runInForeground() async {
     _logger.info("Starting app in foreground");
     _logger.info("Starting app in foreground");
     await _init(false, via: 'mainMethod');
     await _init(false, via: 'mainMethod');
     final Locale locale = await getLocale();
     final Locale locale = await getLocale();
-    UpdateService.instance.showUpdateNotification();
+    unawaited(UpdateService.instance.showUpdateNotification());
     runApp(
     runApp(
       AppLock(
       AppLock(
         builder: (args) => App(locale: locale),
         builder: (args) => App(locale: locale),

+ 13 - 0
auth/lib/models/account/two_factor.dart

@@ -0,0 +1,13 @@
+enum TwoFactorType { totp, passkey }
+
+// ToString for TwoFactorType
+String twoFactorTypeToString(TwoFactorType type) {
+  switch (type) {
+    case TwoFactorType.totp:
+      return "totp";
+    case TwoFactorType.passkey:
+      return "passkey";
+    default:
+      return type.name;
+  }
+}

+ 141 - 107
auth/lib/onboarding/view/onboarding_page.dart

@@ -18,6 +18,8 @@ import 'package:ente_auth/ui/common/gradient_button.dart';
 import 'package:ente_auth/ui/components/buttons/button_widget.dart';
 import 'package:ente_auth/ui/components/buttons/button_widget.dart';
 import 'package:ente_auth/ui/components/models/button_result.dart';
 import 'package:ente_auth/ui/components/models/button_result.dart';
 import 'package:ente_auth/ui/home_page.dart';
 import 'package:ente_auth/ui/home_page.dart';
+import 'package:ente_auth/ui/settings/developer_settings_page.dart';
+import 'package:ente_auth/ui/settings/developer_settings_widget.dart';
 import 'package:ente_auth/ui/settings/language_picker.dart';
 import 'package:ente_auth/ui/settings/language_picker.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/navigation_util.dart';
 import 'package:ente_auth/utils/navigation_util.dart';
@@ -34,8 +36,12 @@ class OnboardingPage extends StatefulWidget {
 }
 }
 
 
 class _OnboardingPageState extends State<OnboardingPage> {
 class _OnboardingPageState extends State<OnboardingPage> {
+  static const kDeveloperModeTapCountThreshold = 7;
+
   late StreamSubscription<TriggerLogoutEvent> _triggerLogoutEvent;
   late StreamSubscription<TriggerLogoutEvent> _triggerLogoutEvent;
 
 
+  int _developerModeTapCount = 0;
+
   @override
   @override
   void initState() {
   void initState() {
     _triggerLogoutEvent =
     _triggerLogoutEvent =
@@ -57,125 +63,152 @@ class _OnboardingPageState extends State<OnboardingPage> {
     final l10n = context.l10n;
     final l10n = context.l10n;
     return Scaffold(
     return Scaffold(
       body: SafeArea(
       body: SafeArea(
-        child: SingleChildScrollView(
-          child: Center(
-            child: ConstrainedBox(
-              constraints:
-                  const BoxConstraints.tightFor(height: 800, width: 450),
-              child: Padding(
-                padding: const EdgeInsets.symmetric(
-                  vertical: 40.0,
-                  horizontal: 40,
-                ),
-                child: Column(
-                  children: [
-                    Column(
-                      children: [
-                        kDebugMode
-                            ? GestureDetector(
-                                child: const Align(
-                                  alignment: Alignment.topRight,
-                                  child: Text("Lang"),
+        child: GestureDetector(
+          onTap: () async {
+            _developerModeTapCount++;
+            if (_developerModeTapCount >= kDeveloperModeTapCountThreshold) {
+              _developerModeTapCount = 0;
+              final result = await showChoiceDialog(
+                context,
+                title: l10n.developerSettings,
+                firstButtonLabel: l10n.yes,
+                body: l10n.developerSettingsWarning,
+                isDismissible: false,
+              );
+              if (result?.action == ButtonAction.first) {
+                await Navigator.of(context).push(
+                  MaterialPageRoute(
+                    builder: (BuildContext context) {
+                      return const DeveloperSettingsPage();
+                    },
+                  ),
+                );
+                setState(() {});
+              }
+            }
+          },
+          child: SingleChildScrollView(
+            child: Center(
+              child: ConstrainedBox(
+                constraints:
+                    const BoxConstraints.tightFor(height: 800, width: 450),
+                child: Padding(
+                  padding: const EdgeInsets.symmetric(
+                    vertical: 40.0,
+                    horizontal: 40,
+                  ),
+                  child: Column(
+                    children: [
+                      Column(
+                        children: [
+                          kDebugMode
+                              ? GestureDetector(
+                                  child: const Align(
+                                    alignment: Alignment.topRight,
+                                    child: Text("Lang"),
+                                  ),
+                                  onTap: () async {
+                                    final locale = await getLocale();
+                                    // ignore: unawaited_futures
+                                    routeToPage(
+                                      context,
+                                      LanguageSelectorPage(
+                                        appSupportedLocales,
+                                        (locale) async {
+                                          await setLocale(locale);
+                                          App.setLocale(context, locale);
+                                        },
+                                        locale,
+                                      ),
+                                    ).then((value) {
+                                      setState(() {});
+                                    });
+                                  },
+                                )
+                              : const SizedBox(),
+                          Image.asset(
+                            "assets/sheild-front-gradient.png",
+                            width: 200,
+                            height: 200,
+                          ),
+                          const SizedBox(height: 12),
+                          const Text(
+                            "ente",
+                            style: TextStyle(
+                              fontWeight: FontWeight.bold,
+                              fontFamily: 'Montserrat',
+                              fontSize: 42,
+                            ),
+                          ),
+                          const SizedBox(height: 4),
+                          Text(
+                            "Authenticator",
+                            style: Theme.of(context).textTheme.headlineMedium,
+                          ),
+                          const SizedBox(height: 32),
+                          Text(
+                            l10n.onBoardingBody,
+                            textAlign: TextAlign.center,
+                            style: Theme.of(context)
+                                .textTheme
+                                .titleLarge!
+                                .copyWith(
+                                  color: Colors.white38,
                                 ),
                                 ),
-                                onTap: () async {
-                                  final locale = await getLocale();
-                                  routeToPage(
-                                    context,
-                                    LanguageSelectorPage(
-                                      appSupportedLocales,
-                                      (locale) async {
-                                        await setLocale(locale);
-                                        App.setLocale(context, locale);
-                                      },
-                                      locale,
-                                    ),
-                                  ).then((value) {
-                                    setState(() {});
-                                  });
-                                },
-                              )
-                            : const SizedBox(),
-                        Image.asset(
-                          "assets/sheild-front-gradient.png",
-                          width: 200,
-                          height: 200,
-                        ),
-                        const SizedBox(height: 12),
-                        const Text(
-                          "ente",
-                          style: TextStyle(
-                            fontWeight: FontWeight.bold,
-                            fontFamily: 'Montserrat',
-                            fontSize: 42,
                           ),
                           ),
+                        ],
+                      ),
+                      const SizedBox(height: 100),
+                      Container(
+                        width: double.infinity,
+                        padding: const EdgeInsets.symmetric(horizontal: 20),
+                        child: GradientButton(
+                          onTap: _navigateToSignUpPage,
+                          text: l10n.newUser,
                         ),
                         ),
-                        const SizedBox(height: 4),
-                        Text(
-                          "Authenticator",
-                          style: Theme.of(context).textTheme.headlineMedium,
-                        ),
-                        const SizedBox(height: 32),
-                        Text(
-                          l10n.onBoardingBody,
-                          textAlign: TextAlign.center,
-                          style:
-                              Theme.of(context).textTheme.titleLarge!.copyWith(
-                                    color: Colors.white38,
-                                    // color: Theme.of(context)
-                                    //                            .colorScheme
-                                    //                            .mutedTextColor,
-                                  ),
-                        ),
-                      ],
-                    ),
-                    const SizedBox(height: 100),
-                    Container(
-                      width: double.infinity,
-                      padding: const EdgeInsets.symmetric(horizontal: 20),
-                      child: GradientButton(
-                        onTap: _navigateToSignUpPage,
-                        text: l10n.newUser,
                       ),
                       ),
-                    ),
-                    const SizedBox(height: 24),
-                    Container(
-                      height: 56,
-                      width: double.infinity,
-                      padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
-                      child: Hero(
-                        tag: "log_in",
-                        child: ElevatedButton(
-                          style: Theme.of(context)
-                              .colorScheme
-                              .optionalActionButtonStyle,
-                          onPressed: _navigateToSignInPage,
-                          child: Text(
-                            l10n.existingUser,
-                            style: const TextStyle(
-                              color: Colors.black, // same for both themes
+                      const SizedBox(height: 24),
+                      Container(
+                        height: 56,
+                        width: double.infinity,
+                        padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
+                        child: Hero(
+                          tag: "log_in",
+                          child: ElevatedButton(
+                            style: Theme.of(context)
+                                .colorScheme
+                                .optionalActionButtonStyle,
+                            onPressed: _navigateToSignInPage,
+                            child: Text(
+                              l10n.existingUser,
+                              style: const TextStyle(
+                                color: Colors.black, // same for both themes
+                              ),
                             ),
                             ),
                           ),
                           ),
                         ),
                         ),
                       ),
                       ),
-                    ),
-                    const SizedBox(height: 4),
-                    Container(
-                      width: double.infinity,
-                      padding: const EdgeInsets.only(top: 20, bottom: 20),
-                      child: GestureDetector(
-                        onTap: _optForOfflineMode,
-                        child: Center(
-                          child: Text(
-                            l10n.useOffline,
-                            style: body.copyWith(
-                              color:
-                                  Theme.of(context).colorScheme.mutedTextColor,
+                      const SizedBox(height: 4),
+                      Container(
+                        width: double.infinity,
+                        padding: const EdgeInsets.only(top: 20, bottom: 20),
+                        child: GestureDetector(
+                          onTap: _optForOfflineMode,
+                          child: Center(
+                            child: Text(
+                              l10n.useOffline,
+                              style: body.copyWith(
+                                color: Theme.of(context)
+                                    .colorScheme
+                                    .mutedTextColor,
+                              ),
                             ),
                             ),
                           ),
                           ),
                         ),
                         ),
                       ),
                       ),
-                    ),
-                  ],
+                      const DeveloperSettingsWidget(),
+                    ],
+                  ),
                 ),
                 ),
               ),
               ),
             ),
             ),
@@ -210,6 +243,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
     }
     }
     if (hasOptedBefore || result?.action == ButtonAction.first) {
     if (hasOptedBefore || result?.action == ButtonAction.first) {
       await Configuration.instance.optForOfflineMode();
       await Configuration.instance.optForOfflineMode();
+      // ignore: unawaited_futures
       Navigator.of(context).push(
       Navigator.of(context).push(
         MaterialPageRoute(
         MaterialPageRoute(
           builder: (BuildContext context) {
           builder: (BuildContext context) {

+ 3 - 4
auth/lib/services/authenticator_service.dart

@@ -5,7 +5,6 @@ import 'dart:math';
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/core/errors.dart';
 import 'package:ente_auth/core/errors.dart';
 import 'package:ente_auth/core/event_bus.dart';
 import 'package:ente_auth/core/event_bus.dart';
-import 'package:ente_auth/core/network.dart';
 import 'package:ente_auth/events/codes_updated_event.dart';
 import 'package:ente_auth/events/codes_updated_event.dart';
 import 'package:ente_auth/events/signed_in_event.dart';
 import 'package:ente_auth/events/signed_in_event.dart';
 import 'package:ente_auth/events/trigger_logout_event.dart';
 import 'package:ente_auth/events/trigger_logout_event.dart';
@@ -56,7 +55,7 @@ class AuthenticatorService {
     _prefs = await SharedPreferences.getInstance();
     _prefs = await SharedPreferences.getInstance();
     _db = AuthenticatorDB.instance;
     _db = AuthenticatorDB.instance;
     _offlineDb = OfflineAuthenticatorDB.instance;
     _offlineDb = OfflineAuthenticatorDB.instance;
-    _gateway = AuthenticatorGateway(Network.instance.getDio(), _config);
+    _gateway = AuthenticatorGateway();
     if (Configuration.instance.hasConfiguredAccount()) {
     if (Configuration.instance.hasConfiguredAccount()) {
       unawaited(onlineSync());
       unawaited(onlineSync());
     }
     }
@@ -210,8 +209,8 @@ class AuthenticatorService {
     if (deletedIDs.isNotEmpty) {
     if (deletedIDs.isNotEmpty) {
       await _db.deleteByIDs(ids: deletedIDs);
       await _db.deleteByIDs(ids: deletedIDs);
     }
     }
-    _prefs.setInt(_lastEntitySyncTime, maxSyncTime);
-    _logger.info("Setting synctime to $maxSyncTime");
+    await _prefs.setInt(_lastEntitySyncTime, maxSyncTime);
+    _logger.info("Setting synctime to " + maxSyncTime.toString());
     if (result.length == fetchLimit) {
     if (result.length == fetchLimit) {
       _logger.info("Diff limit reached, pulling again");
       _logger.info("Diff limit reached, pulling again");
       await _remoteToLocalSync();
       await _remoteToLocalSync();

+ 1 - 0
auth/lib/services/local_authentication_service.dart

@@ -60,6 +60,7 @@ class LocalAuthenticationService {
             .setEnabled(Configuration.instance.shouldShowLockScreen());
             .setEnabled(Configuration.instance.shouldShowLockScreen());
       }
       }
     } else {
     } else {
+      // ignore: unawaited_futures
       showErrorDialog(
       showErrorDialog(
         context,
         context,
         errorDialogTitle,
         errorDialogTitle,

+ 22 - 0
auth/lib/services/passkey_service.dart

@@ -17,6 +17,28 @@ class PasskeyService {
     return response.data!["accountsToken"] as String;
     return response.data!["accountsToken"] as String;
   }
   }
 
 
+  Future<bool> isPasskeyRecoveryEnabled() async {
+    final response = await _enteDio.get(
+      "/users/two-factor/recovery-status",
+    );
+    return response.data!["isPasskeyRecoveryEnabled"] as bool;
+  }
+
+  Future<void> configurePasskeyRecovery(
+    String secret,
+    String userEncryptedSecret,
+    String userSecretNonce,
+  ) async {
+    await _enteDio.post(
+      "/users/two-factor/passkeys/configure-recovery",
+      data: {
+        "secret": secret,
+        "userSecretCipher": userEncryptedSecret,
+        "userSecretNonce": userSecretNonce,
+      },
+    );
+  }
+
   Future<void> openPasskeyPage(BuildContext context) async {
   Future<void> openPasskeyPage(BuildContext context) async {
     try {
     try {
       final jwtToken = await getJwtToken();
       final jwtToken = await getJwtToken();

+ 6 - 3
auth/lib/services/update_service.dart

@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'dart:io';
 import 'dart:io';
 
 
 import 'package:ente_auth/core/constants.dart';
 import 'package:ente_auth/core/constants.dart';
@@ -71,9 +72,11 @@ class UpdateService {
     if (shouldUpdate &&
     if (shouldUpdate &&
         hasBeen3DaysSinceLastNotification &&
         hasBeen3DaysSinceLastNotification &&
         _latestVersion!.shouldNotify!) {
         _latestVersion!.shouldNotify!) {
-      NotificationService.instance.showNotification(
-        "Update available",
-        "Click to install our best version yet",
+      unawaited(
+        NotificationService.instance.showNotification(
+          "Update available",
+          "Click to install our best version yet",
+        ),
       );
       );
       await _prefs.setInt(kUpdateAvailableShownTimeKey, now);
       await _prefs.setInt(kUpdateAvailableShownTimeKey, now);
     } else {
     } else {

+ 39 - 5
auth/lib/services/user_service.dart

@@ -11,6 +11,7 @@ import 'package:ente_auth/core/event_bus.dart';
 import 'package:ente_auth/core/network.dart';
 import 'package:ente_auth/core/network.dart';
 import 'package:ente_auth/events/user_details_changed_event.dart';
 import 'package:ente_auth/events/user_details_changed_event.dart';
 import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/l10n/l10n.dart';
+import 'package:ente_auth/models/account/two_factor.dart';
 import 'package:ente_auth/models/api/user/srp.dart';
 import 'package:ente_auth/models/api/user/srp.dart';
 import 'package:ente_auth/models/delete_account.dart';
 import 'package:ente_auth/models/delete_account.dart';
 import 'package:ente_auth/models/key_attributes.dart';
 import 'package:ente_auth/models/key_attributes.dart';
@@ -147,18 +148,18 @@ class UserService {
       final userDetails = UserDetails.fromMap(response.data);
       final userDetails = UserDetails.fromMap(response.data);
       if (shouldCache) {
       if (shouldCache) {
         if (userDetails.profileData != null) {
         if (userDetails.profileData != null) {
-          _preferences.setBool(
+          await _preferences.setBool(
             kIsEmailMFAEnabled,
             kIsEmailMFAEnabled,
             userDetails.profileData!.isEmailMFAEnabled,
             userDetails.profileData!.isEmailMFAEnabled,
           );
           );
-          _preferences.setBool(
+          await _preferences.setBool(
             kCanDisableEmailMFA,
             kCanDisableEmailMFA,
             userDetails.profileData!.canDisableEmailMFA,
             userDetails.profileData!.canDisableEmailMFA,
           );
           );
         }
         }
         // handle email change from different client
         // handle email change from different client
         if (userDetails.email != _config.getEmail()) {
         if (userDetails.email != _config.getEmail()) {
-          setEmail(userDetails.email);
+          await setEmail(userDetails.email);
         }
         }
       }
       }
       return userDetails;
       return userDetails;
@@ -282,6 +283,7 @@ class UserService {
       throw Exception("unexpected response during passkey verification");
       throw Exception("unexpected response during passkey verification");
     }
     }
 
 
+    // ignore: unawaited_futures
     Navigator.of(context).pushAndRemoveUntil(
     Navigator.of(context).pushAndRemoveUntil(
       MaterialPageRoute(
       MaterialPageRoute(
         builder: (BuildContext context) {
         builder: (BuildContext context) {
@@ -331,6 +333,7 @@ class UserService {
             );
             );
           }
           }
         }
         }
+        // ignore: unawaited_futures
         Navigator.of(context).pushAndRemoveUntil(
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
           MaterialPageRoute(
             builder: (BuildContext context) {
             builder: (BuildContext context) {
@@ -354,6 +357,7 @@ class UserService {
         );
         );
         Navigator.of(context).pop();
         Navigator.of(context).pop();
       } else {
       } else {
+        // ignore: unawaited_futures
         showErrorDialog(
         showErrorDialog(
           context,
           context,
           context.l10n.incorrectCode,
           context.l10n.incorrectCode,
@@ -363,6 +367,7 @@ class UserService {
     } catch (e) {
     } catch (e) {
       await dialog.hide();
       await dialog.hide();
       _logger.severe(e);
       _logger.severe(e);
+      // ignore: unawaited_futures
       showErrorDialog(
       showErrorDialog(
         context,
         context,
         context.l10n.oops,
         context.l10n.oops,
@@ -399,6 +404,7 @@ class UserService {
         Bus.instance.fire(UserDetailsChangedEvent());
         Bus.instance.fire(UserDetailsChangedEvent());
         return;
         return;
       }
       }
+      // ignore: unawaited_futures
       showErrorDialog(
       showErrorDialog(
         context,
         context,
         context.l10n.oops,
         context.l10n.oops,
@@ -407,12 +413,14 @@ class UserService {
     } on DioException catch (e) {
     } on DioException catch (e) {
       await dialog.hide();
       await dialog.hide();
       if (e.response != null && e.response!.statusCode == 403) {
       if (e.response != null && e.response!.statusCode == 403) {
+        // ignore: unawaited_futures
         showErrorDialog(
         showErrorDialog(
           context,
           context,
           context.l10n.oops,
           context.l10n.oops,
           context.l10n.thisEmailIsAlreadyInUse,
           context.l10n.thisEmailIsAlreadyInUse,
         );
         );
       } else {
       } else {
+        // ignore: unawaited_futures
         showErrorDialog(
         showErrorDialog(
           context,
           context,
           context.l10n.incorrectCode,
           context.l10n.incorrectCode,
@@ -422,6 +430,7 @@ class UserService {
     } catch (e) {
     } catch (e) {
       await dialog.hide();
       await dialog.hide();
       _logger.severe(e);
       _logger.severe(e);
+      // ignore: unawaited_futures
       showErrorDialog(
       showErrorDialog(
         context,
         context,
         context.l10n.oops,
         context.l10n.oops,
@@ -632,6 +641,7 @@ class UserService {
         }
         }
       }
       }
       await dialog.hide();
       await dialog.hide();
+      // ignore: unawaited_futures
       Navigator.of(context).pushAndRemoveUntil(
       Navigator.of(context).pushAndRemoveUntil(
         MaterialPageRoute(
         MaterialPageRoute(
           builder: (BuildContext context) {
           builder: (BuildContext context) {
@@ -709,6 +719,7 @@ class UserService {
       if (response.statusCode == 200) {
       if (response.statusCode == 200) {
         showShortToast(context, context.l10n.authenticationSuccessful);
         showShortToast(context, context.l10n.authenticationSuccessful);
         await _saveConfiguration(response);
         await _saveConfiguration(response);
+        // ignore: unawaited_futures
         Navigator.of(context).pushAndRemoveUntil(
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
           MaterialPageRoute(
             builder: (BuildContext context) {
             builder: (BuildContext context) {
@@ -723,6 +734,7 @@ class UserService {
       _logger.severe(e);
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {
       if (e.response != null && e.response!.statusCode == 404) {
         showToast(context, "Session expired");
         showToast(context, "Session expired");
+        // ignore: unawaited_futures
         Navigator.of(context).pushAndRemoveUntil(
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
           MaterialPageRoute(
             builder: (BuildContext context) {
             builder: (BuildContext context) {
@@ -732,6 +744,7 @@ class UserService {
           (route) => route.isFirst,
           (route) => route.isFirst,
         );
         );
       } else {
       } else {
+        // ignore: unawaited_futures
         showErrorDialog(
         showErrorDialog(
           context,
           context,
           context.l10n.incorrectCode,
           context.l10n.incorrectCode,
@@ -741,6 +754,7 @@ class UserService {
     } catch (e) {
     } catch (e) {
       await dialog.hide();
       await dialog.hide();
       _logger.severe(e);
       _logger.severe(e);
+      // ignore: unawaited_futures
       showErrorDialog(
       showErrorDialog(
         context,
         context,
         context.l10n.oops,
         context.l10n.oops,
@@ -749,7 +763,11 @@ class UserService {
     }
     }
   }
   }
 
 
-  Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
+  Future<void> recoverTwoFactor(
+    BuildContext context,
+    String sessionID,
+    TwoFactorType type,
+  ) async {
     final dialog = createProgressDialog(context, context.l10n.pleaseWait);
     final dialog = createProgressDialog(context, context.l10n.pleaseWait);
     await dialog.show();
     await dialog.show();
     try {
     try {
@@ -757,13 +775,16 @@ class UserService {
         "${_config.getHttpEndpoint()}/users/two-factor/recover",
         "${_config.getHttpEndpoint()}/users/two-factor/recover",
         queryParameters: {
         queryParameters: {
           "sessionID": sessionID,
           "sessionID": sessionID,
+          "twoFactorType": twoFactorTypeToString(type),
         },
         },
       );
       );
       if (response.statusCode == 200) {
       if (response.statusCode == 200) {
+        // ignore: unawaited_futures
         Navigator.of(context).pushAndRemoveUntil(
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
           MaterialPageRoute(
             builder: (BuildContext context) {
             builder: (BuildContext context) {
               return TwoFactorRecoveryPage(
               return TwoFactorRecoveryPage(
+                type,
                 sessionID,
                 sessionID,
                 response.data["encryptedSecret"],
                 response.data["encryptedSecret"],
                 response.data["secretDecryptionNonce"],
                 response.data["secretDecryptionNonce"],
@@ -774,9 +795,11 @@ class UserService {
         );
         );
       }
       }
     } on DioException catch (e) {
     } on DioException catch (e) {
+      await dialog.hide();
       _logger.severe(e);
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {
       if (e.response != null && e.response!.statusCode == 404) {
         showToast(context, context.l10n.sessionExpired);
         showToast(context, context.l10n.sessionExpired);
+        // ignore: unawaited_futures
         Navigator.of(context).pushAndRemoveUntil(
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
           MaterialPageRoute(
             builder: (BuildContext context) {
             builder: (BuildContext context) {
@@ -786,6 +809,7 @@ class UserService {
           (route) => route.isFirst,
           (route) => route.isFirst,
         );
         );
       } else {
       } else {
+        // ignore: unawaited_futures
         showErrorDialog(
         showErrorDialog(
           context,
           context,
           context.l10n.oops,
           context.l10n.oops,
@@ -793,7 +817,9 @@ class UserService {
         );
         );
       }
       }
     } catch (e) {
     } catch (e) {
+      await dialog.hide();
       _logger.severe(e);
       _logger.severe(e);
+      // ignore: unawaited_futures
       showErrorDialog(
       showErrorDialog(
         context,
         context,
         context.l10n.oops,
         context.l10n.oops,
@@ -806,6 +832,7 @@ class UserService {
 
 
   Future<void> removeTwoFactor(
   Future<void> removeTwoFactor(
     BuildContext context,
     BuildContext context,
+    TwoFactorType type,
     String sessionID,
     String sessionID,
     String recoveryKey,
     String recoveryKey,
     String encryptedSecret,
     String encryptedSecret,
@@ -845,6 +872,7 @@ class UserService {
         data: {
         data: {
           "sessionID": sessionID,
           "sessionID": sessionID,
           "secret": secret,
           "secret": secret,
+          "twoFactorType": twoFactorTypeToString(type),
         },
         },
       );
       );
       if (response.statusCode == 200) {
       if (response.statusCode == 200) {
@@ -853,6 +881,7 @@ class UserService {
           context.l10n.twofactorAuthenticationSuccessfullyReset,
           context.l10n.twofactorAuthenticationSuccessfullyReset,
         );
         );
         await _saveConfiguration(response);
         await _saveConfiguration(response);
+        // ignore: unawaited_futures
         Navigator.of(context).pushAndRemoveUntil(
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
           MaterialPageRoute(
             builder: (BuildContext context) {
             builder: (BuildContext context) {
@@ -863,9 +892,11 @@ class UserService {
         );
         );
       }
       }
     } on DioException catch (e) {
     } on DioException catch (e) {
+      await dialog.hide();
       _logger.severe(e);
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {
       if (e.response != null && e.response!.statusCode == 404) {
         showToast(context, "Session expired");
         showToast(context, "Session expired");
+        // ignore: unawaited_futures
         Navigator.of(context).pushAndRemoveUntil(
         Navigator.of(context).pushAndRemoveUntil(
           MaterialPageRoute(
           MaterialPageRoute(
             builder: (BuildContext context) {
             builder: (BuildContext context) {
@@ -875,6 +906,7 @@ class UserService {
           (route) => route.isFirst,
           (route) => route.isFirst,
         );
         );
       } else {
       } else {
+        // ignore: unawaited_futures
         showErrorDialog(
         showErrorDialog(
           context,
           context,
           context.l10n.oops,
           context.l10n.oops,
@@ -882,7 +914,9 @@ class UserService {
         );
         );
       }
       }
     } catch (e) {
     } catch (e) {
+      await dialog.hide();
       _logger.severe(e);
       _logger.severe(e);
+      // ignore: unawaited_futures
       showErrorDialog(
       showErrorDialog(
         context,
         context,
         context.l10n.oops,
         context.l10n.oops,
@@ -925,7 +959,7 @@ class UserService {
           "isEnabled": isEnabled,
           "isEnabled": isEnabled,
         },
         },
       );
       );
-      _preferences.setBool(kIsEmailMFAEnabled, isEnabled);
+      await _preferences.setBool(kIsEmailMFAEnabled, isEnabled);
     } catch (e) {
     } catch (e) {
       _logger.severe("Failed to update email mfa", e);
       _logger.severe("Failed to update email mfa", e);
       rethrow;
       rethrow;

+ 0 - 4
auth/lib/store/user_store.dart

@@ -7,10 +7,6 @@ class UserStore {
   late SharedPreferences _preferences;
   late SharedPreferences _preferences;
 
 
   static final UserStore instance = UserStore._privateConstructor();
   static final UserStore instance = UserStore._privateConstructor();
-  static const endpoint = String.fromEnvironment(
-    "endpoint",
-    defaultValue: "https://api.ente.io",
-  );
 
 
   Future<void> init() async {
   Future<void> init() async {
     _preferences = await SharedPreferences.getInstance();
     _preferences = await SharedPreferences.getInstance();

+ 1 - 1
auth/lib/ui/account/delete_account_page.dart

@@ -240,7 +240,7 @@ class DeleteAccountPage extends StatelessWidget {
         ),
         ),
       ],
       ],
     );
     );
-
+    // ignore: unawaited_futures
     showDialog(
     showDialog(
       context: context,
       context: context,
       builder: (BuildContext context) {
       builder: (BuildContext context) {

+ 1 - 0
auth/lib/ui/account/logout_dialog.dart

@@ -23,6 +23,7 @@ Future<void> autoLogoutAlert(BuildContext context) async {
           int pendingSyncCount =
           int pendingSyncCount =
               await AuthenticatorDB.instance.getNeedSyncCount();
               await AuthenticatorDB.instance.getNeedSyncCount();
           if (pendingSyncCount > 0) {
           if (pendingSyncCount > 0) {
+            // ignore: unawaited_futures
             showChoiceActionSheet(
             showChoiceActionSheet(
               context,
               context,
               title: l10n.pendingSyncs,
               title: l10n.pendingSyncs,

+ 6 - 1
auth/lib/ui/account/password_entry_page.dart

@@ -394,6 +394,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
     } catch (e, s) {
     } catch (e, s) {
       _logger.severe(e, s);
       _logger.severe(e, s);
       await dialog.hide();
       await dialog.hide();
+      // ignore: unawaited_futures
       showGenericErrorDialog(context: context);
       showGenericErrorDialog(context: context);
     }
     }
   }
   }
@@ -441,6 +442,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
           await UserService.instance.setAttributes(result);
           await UserService.instance.setAttributes(result);
           await dialog.hide();
           await dialog.hide();
           Configuration.instance.resetVolatilePassword();
           Configuration.instance.resetVolatilePassword();
+          // ignore: unawaited_futures
           Navigator.of(context).pushAndRemoveUntil(
           Navigator.of(context).pushAndRemoveUntil(
             MaterialPageRoute(
             MaterialPageRoute(
               builder: (BuildContext context) {
               builder: (BuildContext context) {
@@ -452,10 +454,11 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
         } catch (e, s) {
         } catch (e, s) {
           _logger.severe(e, s);
           _logger.severe(e, s);
           await dialog.hide();
           await dialog.hide();
+          // ignore: unawaited_futures
           showGenericErrorDialog(context: context);
           showGenericErrorDialog(context: context);
         }
         }
       }
       }
-
+      // ignore: unawaited_futures
       routeToPage(
       routeToPage(
         context,
         context,
         RecoveryKeyPage(
         RecoveryKeyPage(
@@ -471,12 +474,14 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
       _logger.severe(e);
       _logger.severe(e);
       await dialog.hide();
       await dialog.hide();
       if (e is UnsupportedError) {
       if (e is UnsupportedError) {
+        // ignore: unawaited_futures
         showErrorDialog(
         showErrorDialog(
           context,
           context,
           context.l10n.insecureDevice,
           context.l10n.insecureDevice,
           context.l10n.sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease,
           context.l10n.sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease,
         );
         );
       } else {
       } else {
+        // ignore: unawaited_futures
         showGenericErrorDialog(context: context);
         showGenericErrorDialog(context: context);
       }
       }
     }
     }

+ 1 - 0
auth/lib/ui/account/password_reentry_page.dart

@@ -116,6 +116,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
         firstButtonLabel: context.l10n.useRecoveryKey,
         firstButtonLabel: context.l10n.useRecoveryKey,
       );
       );
       if (dialogChoice!.action == ButtonAction.first) {
       if (dialogChoice!.action == ButtonAction.first) {
+        // ignore: unawaited_futures
         Navigator.of(context).push(
         Navigator.of(context).push(
           MaterialPageRoute(
           MaterialPageRoute(
             builder: (BuildContext context) {
             builder: (BuildContext context) {

+ 5 - 4
auth/lib/ui/account/request_pwd_verification_page.dart

@@ -80,7 +80,7 @@ class _RequestPasswordVerificationPageState
         onPressedFunction: () async {
         onPressedFunction: () async {
           FocusScope.of(context).unfocus();
           FocusScope.of(context).unfocus();
           final dialog = createProgressDialog(context, context.l10n.pleaseWait);
           final dialog = createProgressDialog(context, context.l10n.pleaseWait);
-          dialog.show();
+          await dialog.show();
           try {
           try {
             final attributes = Configuration.instance.getKeyAttributes()!;
             final attributes = Configuration.instance.getKeyAttributes()!;
             final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey(
             final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey(
@@ -94,17 +94,18 @@ class _RequestPasswordVerificationPageState
               keyEncryptionKey,
               keyEncryptionKey,
               CryptoUtil.base642bin(attributes.keyDecryptionNonce),
               CryptoUtil.base642bin(attributes.keyDecryptionNonce),
             );
             );
-            dialog.show();
+            await dialog.show();
             // pop
             // pop
             await widget.onPasswordVerified(keyEncryptionKey);
             await widget.onPasswordVerified(keyEncryptionKey);
-            dialog.hide();
+            await dialog.hide();
             Navigator.of(context).pop(true);
             Navigator.of(context).pop(true);
           } catch (e, s) {
           } catch (e, s) {
             _logger.severe("Error while verifying password", e, s);
             _logger.severe("Error while verifying password", e, s);
-            dialog.hide();
+            await dialog.hide();
             if (widget.onPasswordError != null) {
             if (widget.onPasswordError != null) {
               widget.onPasswordError!();
               widget.onPasswordError!();
             } else {
             } else {
+              // ignore: unawaited_futures
               showErrorDialog(
               showErrorDialog(
                 context,
                 context,
                 context.l10n.incorrectPasswordTitle,
                 context.l10n.incorrectPasswordTitle,

+ 2 - 1
auth/lib/ui/account/sessions_page.dart

@@ -121,6 +121,7 @@ class _SessionsPageState extends State<SessionsPage> {
     } catch (e) {
     } catch (e) {
       await dialog.hide();
       await dialog.hide();
       _logger.severe('failed to terminate');
       _logger.severe('failed to terminate');
+      // ignore: unawaited_futures
       showErrorDialog(
       showErrorDialog(
         context,
         context,
         context.l10n.oops,
         context.l10n.oops,
@@ -184,7 +185,7 @@ class _SessionsPageState extends State<SessionsPage> {
             if (isLoggingOutFromThisDevice) {
             if (isLoggingOutFromThisDevice) {
               await UserService.instance.logout(context);
               await UserService.instance.logout(context);
             } else {
             } else {
-              _terminateSession(session);
+              await _terminateSession(session);
             }
             }
           },
           },
         ),
         ),

+ 1 - 0
auth/lib/ui/account/verify_recovery_page.dart

@@ -106,6 +106,7 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
           ),
           ),
         );
         );
       } catch (e) {
       } catch (e) {
+        // ignore: unawaited_futures
         showGenericErrorDialog(context: context);
         showGenericErrorDialog(context: context);
         return;
         return;
       }
       }

+ 2 - 1
auth/lib/ui/code_widget.dart

@@ -357,6 +357,7 @@ class _CodeWidgetState extends State<CodeWidget> {
     await FlutterClipboard.copy(content);
     await FlutterClipboard.copy(content);
     showToast(context, confirmationMessage);
     showToast(context, confirmationMessage);
     if (Platform.isAndroid && shouldMinimizeOnCopy) {
     if (Platform.isAndroid && shouldMinimizeOnCopy) {
+      // ignore: unawaited_futures
       MoveToBackground.moveTaskToBack();
       MoveToBackground.moveTaskToBack();
     }
     }
   }
   }
@@ -387,7 +388,7 @@ class _CodeWidgetState extends State<CodeWidget> {
       ),
       ),
     );
     );
     if (code != null) {
     if (code != null) {
-      CodeStore.instance.addCode(code);
+      await CodeStore.instance.addCode(code);
     }
     }
   }
   }
 
 

+ 1 - 0
auth/lib/ui/common/progress_dialog.dart

@@ -146,6 +146,7 @@ class ProgressDialog {
     try {
     try {
       if (!_isShowing) {
       if (!_isShowing) {
         _dialog = _Body();
         _dialog = _Body();
+        // ignore: unawaited_futures
         showDialog<dynamic>(
         showDialog<dynamic>(
           context: _context!,
           context: _context!,
           barrierDismissible: _barrierDismissible,
           barrierDismissible: _barrierDismissible,

+ 2 - 2
auth/lib/ui/home_page.dart

@@ -125,7 +125,7 @@ class _HomePageState extends State<HomePage> {
       ),
       ),
     );
     );
     if (code != null) {
     if (code != null) {
-      CodeStore.instance.addCode(code);
+      await CodeStore.instance.addCode(code);
       // Focus the new code by searching
       // Focus the new code by searching
       if (_codes.length > 2) {
       if (_codes.length > 2) {
         _focusNewCode(code);
         _focusNewCode(code);
@@ -142,7 +142,7 @@ class _HomePageState extends State<HomePage> {
       ),
       ),
     );
     );
     if (code != null) {
     if (code != null) {
-      CodeStore.instance.addCode(code);
+      await CodeStore.instance.addCode(code);
     }
     }
   }
   }
 
 

+ 64 - 28
auth/lib/ui/passkey_page.dart

@@ -2,9 +2,12 @@ import 'dart:convert';
 
 
 import 'package:app_links/app_links.dart';
 import 'package:app_links/app_links.dart';
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/core/configuration.dart';
-import 'package:ente_auth/ente_theme_data.dart';
 import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/l10n/l10n.dart';
+import 'package:ente_auth/models/account/two_factor.dart';
 import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/services/user_service.dart';
+import 'package:ente_auth/ui/components/buttons/button_widget.dart';
+import 'package:ente_auth/ui/components/models/button_type.dart';
+import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:url_launcher/url_launcher_string.dart';
 import 'package:url_launcher/url_launcher_string.dart';
@@ -49,14 +52,27 @@ class _PasskeyPageState extends State<PasskeyPage> {
     if (!context.mounted ||
     if (!context.mounted ||
         Configuration.instance.hasConfiguredAccount() ||
         Configuration.instance.hasConfiguredAccount() ||
         link == null) {
         link == null) {
+      _logger.warning(
+        'ignored deeplink: contextMounted ${context.mounted} hasConfiguredAccount ${Configuration.instance.hasConfiguredAccount()}',
+      );
       return;
       return;
     }
     }
-    if (mounted && link.toLowerCase().startsWith("enteauth://passkey")) {
-      final uri = Uri.parse(link).queryParameters['response'];
-      // response to json
-      final res = utf8.decode(base64.decode(uri!));
-      final json = jsonDecode(res) as Map<String, dynamic>;
-      await UserService.instance.onPassKeyVerified(context, json);
+    try {
+      if (mounted && link.toLowerCase().startsWith("enteauth://passkey")) {
+        final String? uri = Uri.parse(link).queryParameters['response'];
+        String base64String = uri!.toString();
+        while (base64String.length % 4 != 0) {
+          base64String += '=';
+        }
+        final res = utf8.decode(base64.decode(base64String));
+        final json = jsonDecode(res) as Map<String, dynamic>;
+        await UserService.instance.onPassKeyVerified(context, json);
+      } else {
+        _logger.info('ignored deeplink: $link mounted $mounted');
+      }
+    } catch (e, s) {
+      _logger.severe('passKey: failed to handle deeplink', e, s);
+      showGenericErrorDialog(context: context).ignore();
     }
     }
   }
   }
 
 
@@ -86,30 +102,50 @@ class _PasskeyPageState extends State<PasskeyPage> {
   }
   }
 
 
   Widget _getBody() {
   Widget _getBody() {
-    final l10n = context.l10n;
-
     return Center(
     return Center(
-      child: Column(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          Text(
-            l10n.waitingForBrowserRequest,
-            style: const TextStyle(
-              height: 1.4,
-              fontSize: 16,
+      child: Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 32),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Text(
+              context.l10n.waitingForVerification,
+              style: const TextStyle(
+                height: 1.4,
+                fontSize: 16,
+              ),
             ),
             ),
-          ),
-          const SizedBox(height: 16),
-          Container(
-            width: double.infinity,
-            padding: const EdgeInsets.symmetric(horizontal: 32),
-            child: ElevatedButton(
-              style: Theme.of(context).colorScheme.optionalActionButtonStyle,
-              onPressed: launchPasskey,
-              child: Text(l10n.launchPasskeyUrlAgain),
+            const SizedBox(height: 16),
+            ButtonWidget(
+              buttonType: ButtonType.primary,
+              labelText: context.l10n.verifyPasskey,
+              onTap: () => launchPasskey(),
             ),
             ),
-          ),
-        ],
+            const Padding(padding: EdgeInsets.all(30)),
+            GestureDetector(
+              behavior: HitTestBehavior.opaque,
+              onTap: () {
+                UserService.instance.recoverTwoFactor(
+                  context,
+                  widget.sessionID,
+                  TwoFactorType.passkey,
+                );
+              },
+              child: Container(
+                padding: const EdgeInsets.all(10),
+                child: Center(
+                  child: Text(
+                    context.l10n.recoverAccount,
+                    style: const TextStyle(
+                      decoration: TextDecoration.underline,
+                      fontSize: 12,
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          ],
+        ),
       ),
       ),
     );
     );
   }
   }

+ 3 - 1
auth/lib/ui/settings/about_section_widget.dart

@@ -36,7 +36,8 @@ class AboutSectionWidget extends StatelessWidget {
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
-            launchUrl(Uri.parse("https://github.com/ente-io/auth"));
+            // ignore: unawaited_futures
+            launchUrl(Uri.parse("https://github.com/ente-io/ente"));
           },
           },
         ),
         ),
         sectionOptionSpacing,
         sectionOptionSpacing,
@@ -68,6 +69,7 @@ class AboutSectionWidget extends StatelessWidget {
                           await UpdateService.instance.shouldUpdate();
                           await UpdateService.instance.shouldUpdate();
                       await dialog.hide();
                       await dialog.hide();
                       if (shouldUpdate) {
                       if (shouldUpdate) {
+                        // ignore: unawaited_futures
                         showDialog(
                         showDialog(
                           context: context,
                           context: context,
                           builder: (BuildContext context) {
                           builder: (BuildContext context) {

+ 5 - 0
auth/lib/ui/settings/account_section_widget.dart

@@ -50,6 +50,7 @@ class AccountSectionWidget extends StatelessWidget {
           );
           );
           await PlatformUtil.refocusWindows();
           await PlatformUtil.refocusWindows();
           if (hasAuthenticated) {
           if (hasAuthenticated) {
+            // ignore: unawaited_futures
             showDialog(
             showDialog(
               context: context,
               context: context,
               builder: (BuildContext context) {
               builder: (BuildContext context) {
@@ -76,6 +77,7 @@ class AccountSectionWidget extends StatelessWidget {
             l10n.authToChangeYourPassword,
             l10n.authToChangeYourPassword,
           );
           );
           if (hasAuthenticated) {
           if (hasAuthenticated) {
+            // ignore: unawaited_futures
             Navigator.of(context).push(
             Navigator.of(context).push(
               MaterialPageRoute(
               MaterialPageRoute(
                 builder: (BuildContext context) {
                 builder: (BuildContext context) {
@@ -108,9 +110,11 @@ class AccountSectionWidget extends StatelessWidget {
               recoveryKey =
               recoveryKey =
                   CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey());
                   CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey());
             } catch (e) {
             } catch (e) {
+              // ignore: unawaited_futures
               showGenericErrorDialog(context: context);
               showGenericErrorDialog(context: context);
               return;
               return;
             }
             }
+            // ignore: unawaited_futures
             routeToPage(
             routeToPage(
               context,
               context,
               RecoveryKeyPage(
               RecoveryKeyPage(
@@ -144,6 +148,7 @@ class AccountSectionWidget extends StatelessWidget {
         trailingIcon: Icons.chevron_right_outlined,
         trailingIcon: Icons.chevron_right_outlined,
         trailingIconIsMuted: true,
         trailingIconIsMuted: true,
         onTap: () async {
         onTap: () async {
+          // ignore: unawaited_futures
           routeToPage(context, const DeleteAccountPage());
           routeToPage(context, const DeleteAccountPage());
         },
         },
       ),
       ),

+ 0 - 117
auth/lib/ui/settings/app_update_dialog.dart

@@ -1,15 +1,8 @@
-import 'dart:io';
-
-import 'package:ente_auth/core/configuration.dart';
-import 'package:ente_auth/core/network.dart';
-import 'package:ente_auth/ente_theme_data.dart';
 import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/services/update_service.dart';
 import 'package:ente_auth/services/update_service.dart';
 import 'package:ente_auth/theme/ente_theme.dart';
 import 'package:ente_auth/theme/ente_theme.dart';
 import 'package:ente_auth/utils/platform_util.dart';
 import 'package:ente_auth/utils/platform_util.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:logging/logging.dart';
-import 'package:open_filex/open_filex.dart';
 import 'package:url_launcher/url_launcher_string.dart';
 import 'package:url_launcher/url_launcher_string.dart';
 
 
 class AppUpdateDialog extends StatefulWidget {
 class AppUpdateDialog extends StatefulWidget {
@@ -117,113 +110,3 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
     );
     );
   }
   }
 }
 }
-
-class ApkDownloaderDialog extends StatefulWidget {
-  final LatestVersionInfo? versionInfo;
-
-  const ApkDownloaderDialog(this.versionInfo, {super.key});
-
-  @override
-  State<ApkDownloaderDialog> createState() => _ApkDownloaderDialogState();
-}
-
-class _ApkDownloaderDialogState extends State<ApkDownloaderDialog> {
-  String? _saveUrl;
-  double? _downloadProgress;
-
-  @override
-  void initState() {
-    super.initState();
-    _saveUrl =
-        "${Configuration.instance.getTempDirectory()}ente-${widget.versionInfo!.name!}.apk";
-    _downloadApk();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return PopScope(
-      canPop: false,
-      child: AlertDialog(
-        title: const Text(
-          "Downloading...",
-          style: TextStyle(
-            fontSize: 16,
-          ),
-          textAlign: TextAlign.center,
-        ),
-        content: LinearProgressIndicator(
-          value: _downloadProgress,
-          valueColor: AlwaysStoppedAnimation<Color>(
-            Theme.of(context).colorScheme.alternativeColor,
-          ),
-        ),
-      ),
-    );
-  }
-
-  Future<void> _downloadApk() async {
-    try {
-      if (!File(_saveUrl!).existsSync()) {
-        await Network.instance.getDio().download(
-          widget.versionInfo!.url!,
-          _saveUrl,
-          onReceiveProgress: (count, _) {
-            setState(() {
-              _downloadProgress = count / widget.versionInfo!.size!;
-            });
-          },
-        );
-      }
-      Navigator.of(context, rootNavigator: true).pop('dialog');
-      OpenFilex.open(_saveUrl);
-    } catch (e) {
-      Logger("ApkDownloader").severe(e);
-      final AlertDialog alert = AlertDialog(
-        title: const Text("Sorry"),
-        content: const Text("The download could not be completed"),
-        actions: [
-          TextButton(
-            child: const Text(
-              "Ignore",
-              style: TextStyle(
-                color: Colors.white,
-              ),
-            ),
-            onPressed: () {
-              Navigator.of(context, rootNavigator: true).pop('dialog');
-              Navigator.of(context, rootNavigator: true).pop('dialog');
-            },
-          ),
-          TextButton(
-            child: Text(
-              "Retry",
-              style: TextStyle(
-                color: Theme.of(context).colorScheme.alternativeColor,
-              ),
-            ),
-            onPressed: () {
-              Navigator.of(context, rootNavigator: true).pop('dialog');
-              Navigator.of(context, rootNavigator: true).pop('dialog');
-              showDialog(
-                context: context,
-                builder: (BuildContext context) {
-                  return ApkDownloaderDialog(widget.versionInfo);
-                },
-                barrierDismissible: false,
-              );
-            },
-          ),
-        ],
-      );
-
-      showDialog(
-        context: context,
-        builder: (BuildContext context) {
-          return alert;
-        },
-        barrierColor: Colors.black87,
-      );
-      return;
-    }
-  }
-}

+ 1 - 0
auth/lib/ui/settings/danger_section_widget.dart

@@ -46,6 +46,7 @@ class DangerSectionWidget extends StatelessWidget {
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
+            // ignore: unawaited_futures
             routeToPage(context, const DeleteAccountPage());
             routeToPage(context, const DeleteAccountPage());
           },
           },
         ),
         ),

+ 1 - 0
auth/lib/ui/settings/data/import/google_auth_import.dart

@@ -58,6 +58,7 @@ Future<void> showGoogleAuthInstruction(BuildContext context) async {
         await CodeStore.instance.addCode(code, shouldSync: false);
         await CodeStore.instance.addCode(code, shouldSync: false);
       }
       }
       unawaited(AuthenticatorService.instance.onlineSync());
       unawaited(AuthenticatorService.instance.onlineSync());
+      // ignore: unawaited_futures
       importSuccessDialog(context, codes.length);
       importSuccessDialog(context, codes.length);
     }
     }
   }
   }

+ 8 - 8
auth/lib/ui/settings/data/import/import_service.dart

@@ -19,29 +19,29 @@ class ImportService {
   Future<void> initiateImport(BuildContext context, ImportType type) async {
   Future<void> initiateImport(BuildContext context, ImportType type) async {
     switch (type) {
     switch (type) {
       case ImportType.plainText:
       case ImportType.plainText:
-        showImportInstructionDialog(context);
+        await showImportInstructionDialog(context);
         break;
         break;
       case ImportType.encrypted:
       case ImportType.encrypted:
-        showEncryptedImportInstruction(context);
+        await showEncryptedImportInstruction(context);
         break;
         break;
       case ImportType.ravio:
       case ImportType.ravio:
-        showRaivoImportInstruction(context);
+        await showRaivoImportInstruction(context);
         break;
         break;
       case ImportType.googleAuthenticator:
       case ImportType.googleAuthenticator:
-        showGoogleAuthInstruction(context);
+        await showGoogleAuthInstruction(context);
         // showToast(context, 'coming soon');
         // showToast(context, 'coming soon');
         break;
         break;
       case ImportType.aegis:
       case ImportType.aegis:
-        showAegisImportInstruction(context);
+        await showAegisImportInstruction(context);
         break;
         break;
       case ImportType.twoFas:
       case ImportType.twoFas:
-        show2FasImportInstruction(context);
+        await show2FasImportInstruction(context);
         break;
         break;
       case ImportType.bitwarden:
       case ImportType.bitwarden:
-        showBitwardenImportInstruction(context);
+        await showBitwardenImportInstruction(context);
         break;
         break;
       case ImportType.lastpass:
       case ImportType.lastpass:
-        showLastpassImportInstruction(context);
+        await showLastpassImportInstruction(context);
         break;
         break;
     }
     }
   }
   }

+ 1 - 1
auth/lib/ui/settings/data/import_page.dart

@@ -105,7 +105,7 @@ class ImportCodePage extends StatelessWidget {
                               index != importOptions.length - 1,
                               index != importOptions.length - 1,
                           isTopBorderRadiusRemoved: index != 0,
                           isTopBorderRadiusRemoved: index != 0,
                           onTap: () async {
                           onTap: () async {
-                            ImportService().initiateImport(context, type);
+                            await ImportService().initiateImport(context, type);
                             // routeToPage(context, ImportCodePage());
                             // routeToPage(context, ImportCodePage());
                             // _showImportInstructionDialog(context);
                             // _showImportInstructionDialog(context);
                           },
                           },

+ 90 - 0
auth/lib/ui/settings/developer_settings_page.dart

@@ -0,0 +1,90 @@
+import 'package:dio/dio.dart';
+import 'package:ente_auth/core/configuration.dart';
+import 'package:ente_auth/l10n/l10n.dart';
+import 'package:ente_auth/ui/common/gradient_button.dart';
+import 'package:ente_auth/utils/dialog_util.dart';
+import 'package:ente_auth/utils/toast_util.dart';
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+
+class DeveloperSettingsPage extends StatefulWidget {
+  const DeveloperSettingsPage({super.key});
+
+  @override
+  _DeveloperSettingsPageState createState() => _DeveloperSettingsPageState();
+}
+
+class _DeveloperSettingsPageState extends State<DeveloperSettingsPage> {
+  final _logger = Logger('DeveloperSettingsPage');
+  final _urlController = TextEditingController();
+
+  @override
+  void dispose() {
+    _urlController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    _logger.info(
+      "Current endpoint is: " + Configuration.instance.getHttpEndpoint(),
+    );
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(context.l10n.developerSettings),
+      ),
+      body: Padding(
+        padding: const EdgeInsets.all(16.0),
+        child: Column(
+          children: [
+            TextField(
+              controller: _urlController,
+              decoration: InputDecoration(
+                labelText: context.l10n.serverEndpoint,
+                hintText: Configuration.instance.getHttpEndpoint(),
+              ),
+              autofocus: true,
+            ),
+            const SizedBox(height: 40),
+            GradientButton(
+              onTap: () async {
+                String url = _urlController.text;
+                _logger.info("Entered endpoint: " + url);
+                try {
+                  final uri = Uri.parse(url);
+                  if ((uri.scheme == "http" || uri.scheme == "https")) {
+                    await _ping(url);
+                    await Configuration.instance.setHttpEndpoint(url);
+                    showToast(context, context.l10n.endpointUpdatedMessage);
+                    Navigator.of(context).pop();
+                  } else {
+                    throw const FormatException();
+                  }
+                } catch (e) {
+                  // ignore: unawaited_futures
+                  showErrorDialog(
+                    context,
+                    context.l10n.invalidEndpoint,
+                    context.l10n.invalidEndpointMessage,
+                  );
+                }
+              },
+              text: context.l10n.saveAction,
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Future<void> _ping(String endpoint) async {
+    try {
+      final response = await Dio().get(endpoint + '/ping');
+      if (response.data['message'] != 'pong') {
+        throw Exception('Invalid response');
+      }
+    } catch (e) {
+      throw Exception('Error occurred: $e');
+    }
+  }
+}

+ 27 - 0
auth/lib/ui/settings/developer_settings_widget.dart

@@ -0,0 +1,27 @@
+import 'package:ente_auth/core/configuration.dart';
+import 'package:ente_auth/core/constants.dart';
+import 'package:ente_auth/l10n/l10n.dart';
+import 'package:flutter/material.dart';
+
+class DeveloperSettingsWidget extends StatelessWidget {
+  const DeveloperSettingsWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final endpoint = Configuration.instance.getHttpEndpoint();
+    if (endpoint != kDefaultProductionEndpoint) {
+      final endpointURI = Uri.parse(endpoint);
+      return Padding(
+        padding: const EdgeInsets.only(bottom: 20),
+        child: Text(
+          context.l10n.customEndpoint(
+            endpointURI.host + ":" + endpointURI.port.toString(),
+          ),
+          style: Theme.of(context).textTheme.bodySmall,
+        ),
+      );
+    } else {
+      return const SizedBox.shrink();
+    }
+  }
+}

+ 1 - 0
auth/lib/ui/settings/general_section_widget.dart

@@ -48,6 +48,7 @@ class _AdvancedSectionWidgetState extends State<AdvancedSectionWidget> {
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
             final locale = await getLocale();
             final locale = await getLocale();
+            // ignore: unawaited_futures
             routeToPage(
             routeToPage(
               context,
               context,
               LanguageSelectorPage(
               LanguageSelectorPage(

+ 29 - 1
auth/lib/ui/settings/security_section_widget.dart

@@ -22,6 +22,7 @@ import 'package:ente_auth/utils/platform_util.dart';
 import 'package:ente_auth/utils/toast_util.dart';
 import 'package:ente_auth/utils/toast_util.dart';
 import 'package:ente_crypto_dart/ente_crypto_dart.dart';
 import 'package:ente_crypto_dart/ente_crypto_dart.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
 
 
 class SecuritySectionWidget extends StatefulWidget {
 class SecuritySectionWidget extends StatefulWidget {
   const SecuritySectionWidget({super.key});
   const SecuritySectionWidget({super.key});
@@ -33,6 +34,7 @@ class SecuritySectionWidget extends StatefulWidget {
 class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
 class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
   final _config = Configuration.instance;
   final _config = Configuration.instance;
   late bool _hasLoggedIn;
   late bool _hasLoggedIn;
+  final Logger _logger = Logger('SecuritySectionWidget');
 
 
   @override
   @override
   void initState() {
   void initState() {
@@ -76,7 +78,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
             pressedColor: getEnteColorScheme(context).fillFaint,
             pressedColor: getEnteColorScheme(context).fillFaint,
             trailingIcon: Icons.chevron_right_outlined,
             trailingIcon: Icons.chevron_right_outlined,
             trailingIconIsMuted: true,
             trailingIconIsMuted: true,
-            onTap: () => PasskeyService.instance.openPasskeyPage(context),
+            onTap: () async => await onPasskeyClick(context),
           ),
           ),
         sectionOptionSpacing,
         sectionOptionSpacing,
         MenuItemWidget(
         MenuItemWidget(
@@ -119,6 +121,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
             );
             );
             await PlatformUtil.refocusWindows();
             await PlatformUtil.refocusWindows();
             if (hasAuthenticated) {
             if (hasAuthenticated) {
+              // ignore: unawaited_futures
               Navigator.of(context).push(
               Navigator.of(context).push(
                 MaterialPageRoute(
                 MaterialPageRoute(
                   builder: (BuildContext context) {
                   builder: (BuildContext context) {
@@ -162,6 +165,31 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
     );
     );
   }
   }
 
 
+  Future<void> onPasskeyClick(BuildContext buildContext) async {
+    try {
+      final isPassKeyResetEnabled =
+          await PasskeyService.instance.isPasskeyRecoveryEnabled();
+      if (!isPassKeyResetEnabled) {
+        final Uint8List recoveryKey = Configuration.instance.getRecoveryKey();
+        final resetKey = CryptoUtil.generateKey();
+        final resetKeyBase64 = CryptoUtil.bin2base64(resetKey);
+        final encryptionResult = CryptoUtil.encryptSync(
+          resetKey,
+          recoveryKey,
+        );
+        await PasskeyService.instance.configurePasskeyRecovery(
+          resetKeyBase64,
+          CryptoUtil.bin2base64(encryptionResult.encryptedData!),
+          CryptoUtil.bin2base64(encryptionResult.nonce!),
+        );
+      }
+      PasskeyService.instance.openPasskeyPage(buildContext).ignore();
+    } catch (e, s) {
+      _logger.severe("failed to open passkey page", e, s);
+      await showGenericErrorDialog(context: context);
+    }
+  }
+
   Future<void> updateEmailMFA(bool enableEmailMFA) async {
   Future<void> updateEmailMFA(bool enableEmailMFA) async {
     try {
     try {
       final UserDetails details =
       final UserDetails details =

+ 1 - 0
auth/lib/ui/settings/social_section_widget.dart

@@ -81,6 +81,7 @@ class SocialsMenuItemWidget extends StatelessWidget {
       trailingIcon: Icons.chevron_right_outlined,
       trailingIcon: Icons.chevron_right_outlined,
       trailingIconIsMuted: true,
       trailingIconIsMuted: true,
       onTap: () async {
       onTap: () async {
+        // ignore: unawaited_futures
         launchUrlString(
         launchUrlString(
           url,
           url,
           mode: launchInExternalApp
           mode: launchInExternalApp

+ 2 - 0
auth/lib/ui/settings/support_section_widget.dart

@@ -42,6 +42,7 @@ class _SupportSectionWidgetState extends State<SupportSectionWidget> {
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
+            // ignore: unawaited_futures
             showModalBottomSheet<void>(
             showModalBottomSheet<void>(
               backgroundColor: Theme.of(context).colorScheme.background,
               backgroundColor: Theme.of(context).colorScheme.background,
               barrierColor: Colors.black87,
               barrierColor: Colors.black87,
@@ -61,6 +62,7 @@ class _SupportSectionWidgetState extends State<SupportSectionWidget> {
           trailingIcon: Icons.chevron_right_outlined,
           trailingIcon: Icons.chevron_right_outlined,
           trailingIconIsMuted: true,
           trailingIconIsMuted: true,
           onTap: () async {
           onTap: () async {
+            // ignore: unawaited_futures
             launchUrlString(
             launchUrlString(
               githubIssuesUrl,
               githubIssuesUrl,
               mode: LaunchMode.externalApplication,
               mode: LaunchMode.externalApplication,

+ 2 - 0
auth/lib/ui/settings_page.dart

@@ -16,6 +16,7 @@ import 'package:ente_auth/ui/settings/account_section_widget.dart';
 import 'package:ente_auth/ui/settings/app_version_widget.dart';
 import 'package:ente_auth/ui/settings/app_version_widget.dart';
 import 'package:ente_auth/ui/settings/data/data_section_widget.dart';
 import 'package:ente_auth/ui/settings/data/data_section_widget.dart';
 import 'package:ente_auth/ui/settings/data/export_widget.dart';
 import 'package:ente_auth/ui/settings/data/export_widget.dart';
+import 'package:ente_auth/ui/settings/developer_settings_widget.dart';
 import 'package:ente_auth/ui/settings/general_section_widget.dart';
 import 'package:ente_auth/ui/settings/general_section_widget.dart';
 import 'package:ente_auth/ui/settings/security_section_widget.dart';
 import 'package:ente_auth/ui/settings/security_section_widget.dart';
 import 'package:ente_auth/ui/settings/social_section_widget.dart';
 import 'package:ente_auth/ui/settings/social_section_widget.dart';
@@ -156,6 +157,7 @@ class SettingsPage extends StatelessWidget {
       sectionSpacing,
       sectionSpacing,
       const AboutSectionWidget(),
       const AboutSectionWidget(),
       const AppVersionWidget(),
       const AppVersionWidget(),
+      const DeveloperSettingsWidget(),
       const SupportDevWidget(),
       const SupportDevWidget(),
       const Padding(
       const Padding(
         padding: EdgeInsets.only(bottom: 60),
         padding: EdgeInsets.only(bottom: 60),

+ 1 - 0
auth/lib/ui/tools/lock_screen.dart

@@ -56,6 +56,7 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
                     text: context.l10n.unlock,
                     text: context.l10n.unlock,
                     iconData: Icons.lock_open_outlined,
                     iconData: Icons.lock_open_outlined,
                     onTap: () async {
                     onTap: () async {
+                      // ignore: unawaited_futures
                       _showLockScreen(source: "tapUnlock");
                       _showLockScreen(source: "tapUnlock");
                     },
                     },
                   ),
                   ),

+ 7 - 2
auth/lib/ui/two_factor_authentication_page.dart

@@ -1,4 +1,5 @@
 import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/l10n/l10n.dart';
+import 'package:ente_auth/models/account/two_factor.dart';
 import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/ui/lifecycle_event_handler.dart';
 import 'package:ente_auth/ui/lifecycle_event_handler.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
@@ -121,7 +122,7 @@ class _TwoFactorAuthenticationPageState
           child: OutlinedButton(
           child: OutlinedButton(
             onPressed: _code.length == 6
             onPressed: _code.length == 6
                 ? () async {
                 ? () async {
-                    _verifyTwoFactorCode(_code);
+                    await _verifyTwoFactorCode(_code);
                   }
                   }
                 : null,
                 : null,
             child: Text(l10n.verify),
             child: Text(l10n.verify),
@@ -131,7 +132,11 @@ class _TwoFactorAuthenticationPageState
         GestureDetector(
         GestureDetector(
           behavior: HitTestBehavior.opaque,
           behavior: HitTestBehavior.opaque,
           onTap: () {
           onTap: () {
-            UserService.instance.recoverTwoFactor(context, widget.sessionID);
+            UserService.instance.recoverTwoFactor(
+              context,
+              widget.sessionID,
+              TwoFactorType.totp,
+            );
           },
           },
           child: Container(
           child: Container(
             padding: const EdgeInsets.all(10),
             padding: const EdgeInsets.all(10),

+ 4 - 0
auth/lib/ui/two_factor_recovery_page.dart

@@ -1,4 +1,5 @@
 import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/l10n/l10n.dart';
+import 'package:ente_auth/models/account/two_factor.dart';
 import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
@@ -7,8 +8,10 @@ class TwoFactorRecoveryPage extends StatefulWidget {
   final String sessionID;
   final String sessionID;
   final String encryptedSecret;
   final String encryptedSecret;
   final String secretDecryptionNonce;
   final String secretDecryptionNonce;
+  final TwoFactorType type;
 
 
   const TwoFactorRecoveryPage(
   const TwoFactorRecoveryPage(
+    this.type,
     this.sessionID,
     this.sessionID,
     this.encryptedSecret,
     this.encryptedSecret,
     this.secretDecryptionNonce, {
     this.secretDecryptionNonce, {
@@ -70,6 +73,7 @@ class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
                   ? () async {
                   ? () async {
                       await UserService.instance.removeTwoFactor(
                       await UserService.instance.removeTwoFactor(
                         context,
                         context,
+                        widget.type,
                         widget.sessionID,
                         widget.sessionID,
                         _recoveryKey.text,
                         _recoveryKey.text,
                         widget.encryptedSecret,
                         widget.encryptedSecret,

+ 62 - 5
auth/lib/utils/email_util.dart

@@ -101,14 +101,71 @@ Future<void> sendLogs(
           );
           );
         },
         },
       ),
       ),
-      ButtonWidget(
-        isInAlert: true,
-        buttonType: ButtonType.secondary,
-        labelText: l10n.cancel,
-        buttonAction: ButtonAction.cancel,
+      onPressed: () async {
+        // ignore: unawaited_futures
+        showDialog(
+          context: context,
+          builder: (BuildContext context) {
+            return LogFileViewer(SuperLogging.logFile!);
+          },
+          barrierColor: Colors.black87,
+          barrierDismissible: false,
+        );
+      },
+    ),
+    TextButton(
+      child: Text(
+        title,
+        style: TextStyle(
+          color: Theme.of(context).colorScheme.alternativeColor,
+        ),
+      ),
+      onPressed: () async {
+        Navigator.of(context, rootNavigator: true).pop('dialog');
+        await _sendLogs(context, toEmail, subject, body);
+        if (postShare != null) {
+          postShare();
+        }
+      },
+    ),
+  ];
+  final List<Widget> content = [];
+  content.addAll(
+    [
+      Text(
+        l10n.sendLogsDescription,
+        style: const TextStyle(
+          height: 1.5,
+          fontSize: 16,
+        ),
+      ),
+      const Padding(padding: EdgeInsets.all(12)),
+      Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: actions,
       ),
       ),
     ],
     ],
   );
   );
+  final confirmation = AlertDialog(
+    title: Text(
+      title,
+      style: const TextStyle(
+        fontSize: 18,
+      ),
+    ),
+    content: SingleChildScrollView(
+      child: ListBody(
+        children: content,
+      ),
+    ),
+  );
+  // ignore: unawaited_futures
+  showDialog(
+    context: context,
+    builder: (_) {
+      return confirmation;
+    },
+  );
 }
 }
 
 
 Future<void> _sendLogs(
 Future<void> _sendLogs(

+ 4 - 4
auth/lib/utils/toast_util.dart

@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 
 
-Future showToast(
+void showToast(
   BuildContext context,
   BuildContext context,
   String message, {
   String message, {
   toastLength = Toast.LENGTH_LONG,
   toastLength = Toast.LENGTH_LONG,
@@ -11,7 +11,7 @@ Future showToast(
 }) async {
 }) async {
   try {
   try {
     await Fluttertoast.cancel();
     await Fluttertoast.cancel();
-    return Fluttertoast.showToast(
+    await Fluttertoast.showToast(
       msg: message,
       msg: message,
       toastLength: toastLength,
       toastLength: toastLength,
       gravity: ToastGravity.BOTTOM,
       gravity: ToastGravity.BOTTOM,
@@ -47,6 +47,6 @@ Future showToast(
   }
   }
 }
 }
 
 
-Future<void> showShortToast(context, String message) {
-  return showToast(context, message, toastLength: Toast.LENGTH_SHORT);
+void showShortToast(context, String message) {
+  showToast(context, message, toastLength: Toast.LENGTH_SHORT);
 }
 }

+ 4 - 0
auth/migration-guides/README.md

@@ -0,0 +1,4 @@
+Migration guides have moved to the [help
+docs](https://help.ente.io/auth/migration-guides/). This folder just contains
+redirects for old links.
+

+ 2 - 62
auth/migration-guides/authy.md

@@ -1,62 +1,2 @@
-# Migrating from Authy
-A guide written by Green, an ente.io lover
-
----
-
-Migrating from Authy can be tiring, as you cannot export your 2FA codes through the app, meaning that you would have to reconfigure 2FA for all of your accounts for your new 2FA authenticator. But do not fear, as there is a much simpler way to migrate from Authy to ente!
-
-A user on GitHub has written a guide to export our data from Authy (morpheus on Discord found this and showed it to us), so we are going to be using that for the migration.
-
-## Exporting from Authy
-To export your data, please follow [this guide](https://gist.github.com/gboudreau/94bb0c11a6209c82418d01a59d958c93). This will create a new JSON file with all your Authy TOTP data in it. **Do not share this file with anyone!**
-
-Or, you can [use this tool by Neeraj](https://github.com/ua741/authy-export/releases/tag/v0.0.4) to simplify things and skip directly to importing to ente Authenticator.
-### *Do note that these tools may not export ALL of your codes. Make sure that all your accounts have been imported successfully before deleting any codes from your Authy account!*
-
-## Converting the export for ente Authenticator
-### Update: You can now directly import from Bitwarden JSON export, meaning you can skip this step! If it doesn't work for some reason, though, then continue with this step.
-So now that you have the JSON file, does that mean it can be imported into ente Authenticator? Yes, but if it doesn't work for some reason, then nope. (If you have a TXT file in the format ente Authenticator asked you for instead, then you probably used Neeraj's tool, so you can skip this step.)
-
-This is because the code in the guide exports your Authy data for Bitwarden, not ente Authenticator. If you have opened the JSON file, you might have noticed that the file created is not in a format that ente Authenticator asks for:
-
-<img width="454" alt="ente Authenticator Screenshot" src="https://github.com/gweeeen/auth/assets/41323182/30566a69-cfa0-4de0-9f0d-95967d4c5cad">
-
-So, this means that even if you try to import this file, nothing will happen. But don't worry, I've written a program in Python that converts the JSON file into a TXT file that ente Authenticator can use! (It's definitely not written **professionaly**, but hey it gets the job done so I'm happy with that.)
-
-You can download my program [here](https://github.com/gweeeen/ducky/blob/main/duckys_other_stuff/authy_to_ente.py). Or if you **really like making life hard**, then you can make a new Python file and copy this code to it:
-
-```py
-import json
-import os
-
-totp = []
-
-accounts = json.load(open('authy-to-bitwarden-export.json','r',encoding='utf-8'))
-
-for account in accounts['items']:
-    totp.append(account['login']['totp']+'\n')
-
-writer = open('auth_codes.txt','w+',encoding='utf-8')
-writer.writelines(totp)
-writer.close()
-
-print('Saved to ' + os.getcwd() + '/auth_codes.txt')
-```
-
-To convert the file with this program, you will need to install [Python](https://www.python.org/downloads/) on your computer.
-
-Before you run the program, make sure that both the Python program and the JSON file are in the same directory, otherwise this will not work!
-
-To run the Python program, open it in IDLE and press F5, or open your terminal and type `python3 authy_to_ente.py` or `py -3 authy_to_ente.py`, depending on which OS you have. Once you run it, a new TXT file called `auth_codes.txt` will be generated. You can now import your data to ente Authenticator!
-
-## Importing to ente Authenticator
-Now that we have the TXT file, let's import it. This should be the easiest part of the entire migration process.
-
-1. Copy the TXT file to one of your devices with ente Authenticator.
-2. Log in to your account (if you haven't already).
-3. Open the navigation menu (hamburger button on the top left), then press "Data", then press "Import codes".
-4. Select the TXT file that was made earlier.
-
-And that's it! You have now successfully migrated from Authy to ente Authenticator.
-
-Just one more thing: Now that your secrets are safely stored, I recommend you delete the unencrypted JSON and TXT files that were made during the migration process for security.
+Moved to
+[help.ente.io/auth/migration-guides/authy](https://help.ente.io/auth/migration-guides/authy/)

+ 2 - 63
auth/migration-guides/encrypted_export.md

@@ -1,63 +1,2 @@
-# Auth Encrypted Export format
-
-## Overview
-
-When we export the auth codes, the data is encrypted using a key derived from the user's password.
-This document describes the JSON structure used to organize exported data, including versioning and key derivation
-parameters.
-
-## Export JSON Sample
-
-```json
-{
-  "version": 1,
-  "kdfParams": {
-    "memLimit": 4096,
-    "opsLimit": 3,
-    "salt": "example_salt"
-  },
-  "encryptedData": "encrypted_data_here",
-  "encryptionNonce": "nonce_here"
-}
-```
-
-The main object used to represent the export data. It contains the following key-value pairs:
-
-- `version`: The version of the export format.
-- `kdfParams`:  Key derivation function parameters.
-- `encryptedData"`:  The encrypted authentication data.
-- `encryptionNonce`: The nonce used for encryption.
-
-### Version
-
-Export version is used to identify the format of the export data.
-
-#### Ver: 1
-
-* KDF Algorithm: `ARGON2ID`
-* Decrypted data format: `otpauth://totp/...`, separated by a new line.
-* Encryption Algo: `XChaCha20-Poly1305`
-
-#### Key Derivation Function  Params (KDF)
-
-This section contains the parameters that were using during KDF operation:
-
-- `memLimit`: Memory limit for the algorithm.
-- `opsLimit`: Operations limit for the algorithm.
-- `salt`:  The salt used in the derivation process.
-
-#### Encrypted Data
-
-As mentioned above, the auth data is encrypted using a key that's derived by using user provided password & kdf params.
-For encryption, we are using `XChaCha20-Poly1305` algorithm.
-
-## How to use the exported data
-
-* **Ente Authenticator app**: You can directly import the codes in the Ente Authenticator app.
-  > Settings -> Data -> Import Codes -> ente Encrypted export.
-
-* **Decrypt using Ente CLI** : Download the latest version of [Ente CLI](https://github.com/ente-io/ente/releases?q=CLI&expanded=false), and run the following command
-         
-```
-  ./ente auth decrypt <export_file> <output_file>
-```
+Moved to
+[help.ente.io/auth/migration-guides/export](https://help.ente.io/auth/migration-guides/export/)

+ 16 - 16
auth/pubspec.lock

@@ -213,7 +213,7 @@ packages:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
       name: collection
       name: collection
-      sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
+      sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
     version: "1.18.0"
     version: "1.18.0"
@@ -901,26 +901,26 @@ packages:
     dependency: transitive
     dependency: transitive
     description:
     description:
       name: matcher
       name: matcher
-      sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
+      sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
-    version: "0.12.16+1"
+    version: "0.12.16"
   material_color_utilities:
   material_color_utilities:
     dependency: transitive
     dependency: transitive
     description:
     description:
       name: material_color_utilities
       name: material_color_utilities
-      sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
+      sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
-    version: "0.8.0"
+    version: "0.5.0"
   meta:
   meta:
     dependency: transitive
     dependency: transitive
     description:
     description:
       name: meta
       name: meta
-      sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
+      sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
-    version: "1.11.0"
+    version: "1.9.1"
   mime:
   mime:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -1021,10 +1021,10 @@ packages:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
       name: path
       name: path
-      sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+      sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
-    version: "1.9.0"
+    version: "1.8.3"
   path_drawing:
   path_drawing:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -1419,10 +1419,10 @@ packages:
     dependency: transitive
     dependency: transitive
     description:
     description:
       name: stack_trace
       name: stack_trace
-      sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+      sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
-    version: "1.11.1"
+    version: "1.11.0"
   step_progress_indicator:
   step_progress_indicator:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -1435,10 +1435,10 @@ packages:
     dependency: transitive
     dependency: transitive
     description:
     description:
       name: stream_channel
       name: stream_channel
-      sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+      sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
-    version: "2.1.2"
+    version: "2.1.1"
   stream_transform:
   stream_transform:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -1483,7 +1483,7 @@ packages:
     dependency: transitive
     dependency: transitive
     description:
     description:
       name: test_api
       name: test_api
-      sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
+      sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
     version: "0.6.1"
     version: "0.6.1"
@@ -1643,10 +1643,10 @@ packages:
     dependency: transitive
     dependency: transitive
     description:
     description:
       name: vm_service
       name: vm_service
-      sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
+      sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
-    version: "13.0.0"
+    version: "11.10.0"
   watcher:
   watcher:
     dependency: transitive
     dependency: transitive
     description:
     description:

+ 0 - 2
auth/pubspec.yaml

@@ -64,13 +64,11 @@ dependencies:
   intl: ^0.18.0
   intl: ^0.18.0
   json_annotation: ^4.5.0
   json_annotation: ^4.5.0
   local_auth: ^2.1.7
   local_auth: ^2.1.7
-
   local_auth_android: ^1.0.31
   local_auth_android: ^1.0.31
   local_auth_ios: ^1.1.3
   local_auth_ios: ^1.1.3
   logging: ^1.0.1
   logging: ^1.0.1
   modal_bottom_sheet: ^3.0.0-pre
   modal_bottom_sheet: ^3.0.0-pre
   move_to_background: ^1.0.2
   move_to_background: ^1.0.2
-  open_filex: ^4.3.2
   otp: ^3.1.1
   otp: ^3.1.1
   package_info_plus: ^4.1.0
   package_info_plus: ^4.1.0
   password_strength: ^0.2.0
   password_strength: ^0.2.0

+ 49 - 27
cli/README.md

@@ -1,49 +1,77 @@
-# Command Line Utility for exporting data from [Ente](https://ente.io)
+# Ente CLI
+
+The Ente CLI is a Command Line Utility for exporting data from
+[Ente](https://ente.io). It also does a few more things, for example, you can
+use it to decrypting the export from Ente Auth.
 
 
 ## Install
 ## Install
 
 
-You can either download the binary from the [GitHub releases
-page](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0&expanded=true) or
-build it yourself.
+The easiest way is to download a pre-built binary from the [GitHub
+releases](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0).
+
+You can also build these binaries yourself
+
+```shell
+./release.sh
+```
 
 
-### Build from source
+Or you can build from source
 
 
 ```shell
 ```shell
  go build -o "bin/ente" main.go
  go build -o "bin/ente" main.go
 ```
 ```
 
 
-### Getting Started
+The generated binaries are standalone, static binaries with no dependencies. You
+can run them directly, or put them somewhere in your PATH.
+
+There is also an option to use [Docker](#docker).
+
+## Usage
 
 
 Run the help command to see all available commands.
 Run the help command to see all available commands.
+
 ```shell
 ```shell
 ente --help
 ente --help
 ```
 ```
 
 
-#### Accounts
+### Accounts
+
 If you wish, you can add multiple accounts (your own and that of your family members) and export all data using this tool.
 If you wish, you can add multiple accounts (your own and that of your family members) and export all data using this tool.
 
 
-##### Add an account
+#### Add an account
+
 ```shell
 ```shell
 ente account add
 ente account add
 ```
 ```
 
 
-##### List accounts
+#### List accounts
+
 ```shell
 ```shell
 ente account list
 ente account list
 ```
 ```
 
 
-##### Change export directory
+#### Change export directory
+
 ```shell
 ```shell
 ente account update --email email@domain.com --dir ~/photos
 ente account update --email email@domain.com --dir ~/photos
 ```
 ```
 
 
 ### Export
 ### Export
-##### Start export
+
+#### Start export
+
 ```shell
 ```shell
 ente export
 ente export
 ```
 ```
 
 
----
+### CLI Docs
+You can view more cli documents at [docs](docs/generated/ente.md).
+To update the docs, run the following command:
+
+```shell
+go run main.go docs
+```
+
 
 
 ## Docker
 ## Docker
 
 
@@ -51,16 +79,22 @@ If you fancy Docker, you can also run the CLI within a container.
 
 
 ### Configure
 ### Configure
 
 
-Modify the `docker-compose.yml` and add volume.
-``cli-data`` volume is mandatory, you can add more volumes for your export directory.
+Modify the `docker-compose.yml` and add volume. ``cli-data`` volume is
+mandatory, you can add more volumes for your export directory.
 
 
 Build the docker image
 Build the docker image
+
 ```shell
 ```shell
 docker build -t ente:latest .
 docker build -t ente:latest .
 ```
 ```
 
 
+Note that [BuildKit](https://docs.docker.com/go/buildkit/) is needed to build
+this image. If you face this issue, a quick fix is to add `DOCKER_BUILDKIT=1` in
+front of the build command.
+
 Start the container in detached mode
 Start the container in detached mode
-```bash
+
+```shell
 docker-compose up -d
 docker-compose up -d
 ```
 ```
 
 
@@ -69,20 +103,8 @@ docker-compose up -d
 docker-compose exec ente /bin/sh
 docker-compose exec ente /bin/sh
 ```
 ```
 
 
-
 #### Directly executing commands
 #### Directly executing commands
 
 
 ```shell
 ```shell
 docker run -it --rm ente:latest ls
 docker run -it --rm ente:latest ls
 ```
 ```
-
----
-
-## Releases
-
-Run the release script to build the binary and run it.
-
-```shell
-./release.sh
-```
-

+ 40 - 3
cli/cmd/account.go

@@ -62,7 +62,7 @@ var updateAccCmd = &cobra.Command{
 			fmt.Printf("invalid app. Accepted values are 'photos', 'locker', 'auth'")
 			fmt.Printf("invalid app. Accepted values are 'photos', 'locker', 'auth'")
 
 
 		}
 		}
-		err := ctrl.UpdateAccount(context.Background(), model.UpdateAccountParams{
+		err := ctrl.UpdateAccount(context.Background(), model.AccountCommandParams{
 			Email:     email,
 			Email:     email,
 			App:       api.StringToApp(app),
 			App:       api.StringToApp(app),
 			ExportDir: &exportDir,
 			ExportDir: &exportDir,
@@ -73,12 +73,49 @@ var updateAccCmd = &cobra.Command{
 	},
 	},
 }
 }
 
 
+// Subcommand for 'account update'
+var getTokenCmd = &cobra.Command{
+	Use:   "get-token",
+	Short: "Get token for an account for a specific app",
+	Run: func(cmd *cobra.Command, args []string) {
+		recoverWithLog()
+		app, _ := cmd.Flags().GetString("app")
+		email, _ := cmd.Flags().GetString("email")
+		if email == "" {
+
+			fmt.Println("email must be specified, use --help for more information")
+			// print help
+			return
+		}
+
+		validApps := map[string]bool{
+			"photos": true,
+			"locker": true,
+			"auth":   true,
+		}
+
+		if !validApps[app] {
+			fmt.Printf("invalid app. Accepted values are 'photos', 'locker', 'auth'")
+
+		}
+		err := ctrl.GetToken(context.Background(), model.AccountCommandParams{
+			Email: email,
+			App:   api.StringToApp(app),
+		})
+		if err != nil {
+			fmt.Printf("Error updating account: %v\n", err)
+		}
+	},
+}
+
 func init() {
 func init() {
 	// Add 'config' subcommands to the root command
 	// Add 'config' subcommands to the root command
 	rootCmd.AddCommand(accountCmd)
 	rootCmd.AddCommand(accountCmd)
 	// Add 'config' subcommands to the 'config' command
 	// Add 'config' subcommands to the 'config' command
 	updateAccCmd.Flags().String("dir", "", "update export directory")
 	updateAccCmd.Flags().String("dir", "", "update export directory")
-	updateAccCmd.Flags().String("email", "", "email address of the account to update")
+	updateAccCmd.Flags().String("email", "", "email address of the account")
 	updateAccCmd.Flags().String("app", "photos", "Specify the app, default is 'photos'")
 	updateAccCmd.Flags().String("app", "photos", "Specify the app, default is 'photos'")
-	accountCmd.AddCommand(listAccCmd, addAccCmd, updateAccCmd)
+	getTokenCmd.Flags().String("email", "", "email address of the account")
+	getTokenCmd.Flags().String("app", "photos", "Specify the app, default is 'photos'")
+	accountCmd.AddCommand(listAccCmd, addAccCmd, updateAccCmd, getTokenCmd)
 }
 }

+ 90 - 0
cli/cmd/admin.go

@@ -0,0 +1,90 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"github.com/ente-io/cli/pkg/model"
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+	"strings"
+)
+
+var _adminCmd = &cobra.Command{
+	Use:   "admin",
+	Short: "Commands for admin actions",
+	Long:  "Commands for admin actions like disable or enabling 2fa, bumping up the storage limit, etc.",
+}
+
+var _userDetailsCmd = &cobra.Command{
+	Use:   "get-user-id",
+	Short: "Get user id",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		recoverWithLog()
+		var flags = &model.AdminActionForUser{}
+		cmd.Flags().VisitAll(func(f *pflag.Flag) {
+			if f.Name == "admin-user" {
+				flags.AdminEmail = f.Value.String()
+			}
+			if f.Name == "user" {
+				flags.UserEmail = f.Value.String()
+			}
+		})
+		return ctrl.GetUserId(context.Background(), *flags)
+	},
+}
+
+var _disable2faCmd = &cobra.Command{
+	Use:   "disable-2fa",
+	Short: "Disable 2fa for a user",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		recoverWithLog()
+		var flags = &model.AdminActionForUser{}
+		cmd.Flags().VisitAll(func(f *pflag.Flag) {
+			if f.Name == "admin-user" {
+				flags.AdminEmail = f.Value.String()
+			}
+			if f.Name == "user" {
+				flags.UserEmail = f.Value.String()
+			}
+		})
+		fmt.Println("Not supported yet")
+		return nil
+	},
+}
+
+var _updateFreeUserStorage = &cobra.Command{
+	Use:   "update-subscription",
+	Short: "Update subscription for the free user",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		recoverWithLog()
+		var flags = &model.AdminActionForUser{}
+		noLimit := false
+		cmd.Flags().VisitAll(func(f *pflag.Flag) {
+			if f.Name == "admin-user" {
+				flags.AdminEmail = f.Value.String()
+			}
+			if f.Name == "user" {
+				flags.UserEmail = f.Value.String()
+			}
+			if f.Name == "no-limit" {
+				noLimit = strings.ToLower(f.Value.String()) == "true"
+			}
+		})
+		return ctrl.UpdateFreeStorage(context.Background(), *flags, noLimit)
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(_adminCmd)
+	_ = _userDetailsCmd.MarkFlagRequired("admin-user")
+	_ = _userDetailsCmd.MarkFlagRequired("user")
+	_userDetailsCmd.Flags().StringP("admin-user", "a", "", "The email of the admin user. (required)")
+	_userDetailsCmd.Flags().StringP("user", "u", "", "The email of the user to fetch details for. (required)")
+	_disable2faCmd.Flags().StringP("admin-user", "a", "", "The email of the admin user. (required)")
+	_disable2faCmd.Flags().StringP("user", "u", "", "The email of the user to disable 2FA for. (required)")
+	_updateFreeUserStorage.Flags().StringP("admin-user", "a", "", "The email of the admin user. (required)")
+	_updateFreeUserStorage.Flags().StringP("user", "u", "", "The email of the user to update subscription for. (required)")
+	// add a flag with no value --no-limit
+	_updateFreeUserStorage.Flags().String("no-limit", "True", "When true, sets 100TB as storage limit, and expiry to current date + 100 years")
+	_adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _updateFreeUserStorage)
+}

+ 8 - 2
cli/cmd/root.go

@@ -3,6 +3,7 @@ package cmd
 import (
 import (
 	"fmt"
 	"fmt"
 	"github.com/ente-io/cli/pkg"
 	"github.com/ente-io/cli/pkg"
+	"github.com/spf13/cobra/doc"
 	"os"
 	"os"
 	"runtime"
 	"runtime"
 
 
@@ -11,7 +12,7 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
-const AppVersion = "0.1.11"
+var version string
 
 
 var ctrl *pkg.ClICtrl
 var ctrl *pkg.ClICtrl
 
 
@@ -27,10 +28,15 @@ var rootCmd = &cobra.Command{
 	},
 	},
 }
 }
 
 
+func GenerateDocs() error {
+	return doc.GenMarkdownTree(rootCmd, "./docs/generated")
+}
+
 // Execute adds all child commands to the root command and sets flags appropriately.
 // Execute adds all child commands to the root command and sets flags appropriately.
 // This is called by main.main(). It only needs to happen once to the rootCmd.
 // This is called by main.main(). It only needs to happen once to the rootCmd.
-func Execute(controller *pkg.ClICtrl) {
+func Execute(controller *pkg.ClICtrl, ver string) {
 	ctrl = controller
 	ctrl = controller
+	version = ver
 	err := rootCmd.Execute()
 	err := rootCmd.Execute()
 	if err != nil {
 	if err != nil {
 		os.Exit(1)
 		os.Exit(1)

+ 1 - 1
cli/cmd/version.go

@@ -12,7 +12,7 @@ var versionCmd = &cobra.Command{
 	Short: "Prints the current version",
 	Short: "Prints the current version",
 	Long:  ``,
 	Long:  ``,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
-		fmt.Printf("Version %s\n", AppVersion)
+		fmt.Printf("Version %s\n", version)
 	},
 	},
 }
 }
 
 

+ 10 - 0
cli/config.yaml.example

@@ -0,0 +1,10 @@
+# You can put this configuration file in the following locations:
+# - $HOME/.ente/config.yaml
+# - config.yaml in the current working directory
+# - $ENTE_CLI_CONFIG_PATH/config.yaml
+
+endpoint:
+  api: "http://localhost:8080"
+
+log:
+ http: false # log status code & time taken by requests

+ 28 - 0
cli/docs/generated/ente.md

@@ -0,0 +1,28 @@
+## ente
+
+CLI tool for exporting your photos from ente.io
+
+### Synopsis
+
+Start by creating a config file in your home directory:
+
+```
+ente [flags]
+```
+
+### Options
+
+```
+  -h, --help     help for ente
+  -t, --toggle   Help message for toggle
+```
+
+### SEE ALSO
+
+* [ente account](ente_account.md)	 - Manage account settings
+* [ente admin](ente_admin.md)	 - Commands for admin actions
+* [ente auth](ente_auth.md)	 - Authenticator commands
+* [ente export](ente_export.md)	 - Starts the export process
+* [ente version](ente_version.md)	 - Prints the current version
+
+###### Auto generated by spf13/cobra on 13-Mar-2024

+ 19 - 0
cli/docs/generated/ente_account.md

@@ -0,0 +1,19 @@
+## ente account
+
+Manage account settings
+
+### Options
+
+```
+  -h, --help   help for account
+```
+
+### SEE ALSO
+
+* [ente](ente.md)	 - CLI tool for exporting your photos from ente.io
+* [ente account add](ente_account_add.md)	 - Add a new account
+* [ente account get-token](ente_account_get-token.md)	 - Get token for an account for a specific app
+* [ente account list](ente_account_list.md)	 - list configured accounts
+* [ente account update](ente_account_update.md)	 - Update an existing account's export directory
+
+###### Auto generated by spf13/cobra on 13-Mar-2024

+ 19 - 0
cli/docs/generated/ente_account_add.md

@@ -0,0 +1,19 @@
+## ente account add
+
+Add a new account
+
+```
+ente account add [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for add
+```
+
+### SEE ALSO
+
+* [ente account](ente_account.md)	 - Manage account settings
+
+###### Auto generated by spf13/cobra on 13-Mar-2024

+ 21 - 0
cli/docs/generated/ente_account_get-token.md

@@ -0,0 +1,21 @@
+## ente account get-token
+
+Get token for an account for a specific app
+
+```
+ente account get-token [flags]
+```
+
+### Options
+
+```
+      --app string     Specify the app, default is 'photos' (default "photos")
+      --email string   email address of the account
+  -h, --help           help for get-token
+```
+
+### SEE ALSO
+
+* [ente account](ente_account.md)	 - Manage account settings
+
+###### Auto generated by spf13/cobra on 13-Mar-2024

+ 19 - 0
cli/docs/generated/ente_account_list.md

@@ -0,0 +1,19 @@
+## ente account list
+
+list configured accounts
+
+```
+ente account list [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for list
+```
+
+### SEE ALSO
+
+* [ente account](ente_account.md)	 - Manage account settings
+
+###### Auto generated by spf13/cobra on 13-Mar-2024

+ 22 - 0
cli/docs/generated/ente_account_update.md

@@ -0,0 +1,22 @@
+## ente account update
+
+Update an existing account's export directory
+
+```
+ente account update [flags]
+```
+
+### Options
+
+```
+      --app string     Specify the app, default is 'photos' (default "photos")
+      --dir string     update export directory
+      --email string   email address of the account
+  -h, --help           help for update
+```
+
+### SEE ALSO
+
+* [ente account](ente_account.md)	 - Manage account settings
+
+###### Auto generated by spf13/cobra on 13-Mar-2024

+ 22 - 0
cli/docs/generated/ente_admin.md

@@ -0,0 +1,22 @@
+## ente admin
+
+Commands for admin actions
+
+### Synopsis
+
+Commands for admin actions like disable or enabling 2fa, bumping up the storage limit, etc.
+
+### Options
+
+```
+  -h, --help   help for admin
+```
+
+### SEE ALSO
+
+* [ente](ente.md)	 - CLI tool for exporting your photos from ente.io
+* [ente admin disable-2fa](ente_admin_disable-2fa.md)	 - Disable 2fa for a user
+* [ente admin get-user-id](ente_admin_get-user-id.md)	 - Get user id
+* [ente admin update-subscription](ente_admin_update-subscription.md)	 - Update subscription for the free user
+
+###### Auto generated by spf13/cobra on 13-Mar-2024

+ 21 - 0
cli/docs/generated/ente_admin_disable-2fa.md

@@ -0,0 +1,21 @@
+## ente admin disable-2fa
+
+Disable 2fa for a user
+
+```
+ente admin disable-2fa [flags]
+```
+
+### Options
+
+```
+  -a, --admin-user string   The email of the admin user. (required)
+  -h, --help                help for disable-2fa
+  -u, --user string         The email of the user to disable 2FA for. (required)
+```
+
+### SEE ALSO
+
+* [ente admin](ente_admin.md)	 - Commands for admin actions
+
+###### Auto generated by spf13/cobra on 13-Mar-2024

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff