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

This commit is contained in:
Prateek Sunal 2024-03-14 15:35:28 +05:30
commit 5634b50528
483 changed files with 7833 additions and 7473 deletions

3
.gitattributes vendored Normal file
View file

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

View file

@ -3,15 +3,16 @@ name: "Sync Crowdin translations (auth)"
on:
push:
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"
# Or the workflow itself is changed
- ".github/workflows/auth-crowdin.yml"
branches: [main]
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:
synchronize-with-crowdin:

View file

@ -1,11 +1,9 @@
name: "Lint (auth)"
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:
# See: [Note: Specify branch when specifying a path filter]
branches: ["**"]
# Only run if something changes in these paths
branches-ignore: [main]
paths:
- "auth/**"
- ".github/workflows/auth-lint.yml"

View file

@ -122,7 +122,7 @@ jobs:
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
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
build-windows:

View file

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

47
.github/workflows/docs-deploy.yml vendored Normal file
View file

@ -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
.github/workflows/docs-verify-build.yml vendored Normal file
View file

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

View file

@ -3,15 +3,16 @@ name: "Sync Crowdin translations (mobile)"
on:
push:
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"
# Or the workflow itself is changed
- ".github/workflows/mobile-crowdin.yml"
branches: [main]
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:
synchronize-with-crowdin:

View file

@ -1,11 +1,9 @@
name: "Lint (mobile)"
on:
# Run on every push (this also covers pull requests)
# Run on every push to a branch other than main that changes mobile/
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:
- "mobile/**"
- ".github/workflows/mobile-lint.yml"

View file

@ -39,7 +39,9 @@ jobs:
encodedString: ${{ secrets.SIGNING_KEY_PHOTOS }}
- 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:
SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks"
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }}
@ -47,10 +49,10 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }}
- 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
uses: ncipollo/release-action@v1
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

View file

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

View file

@ -3,15 +3,22 @@ name: "Sync Crowdin translations (web)"
on:
push:
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"
# Or the workflow itself is changed
- ".github/workflows/web-crowdin.yml"
branches: [main]
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:
synchronize-with-crowdin:

View file

@ -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
.github/workflows/web-deploy-auth.yml vendored Normal file
View file

@ -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
.github/workflows/web-deploy-cast.yml vendored Normal file
View file

@ -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
.github/workflows/web-deploy-photos.yml vendored Normal file
View file

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

View file

@ -1,20 +1,9 @@
name: "Lint (web)"
on:
# Run on every push (this also covers pull requests)
# Run on every push to a branch other than main that changes web/
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:
- "web/**"
- ".github/workflows/web-lint.yml"

94
.github/workflows/web-nightly.yml vendored Normal file
View file

@ -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
.github/workflows/web-preview.yml vendored Normal file
View file

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

11
.gitmodules vendored
View file

@ -9,20 +9,9 @@
[submodule "auth/assets/simple-icons"]
path = auth/assets/simple-icons
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"]
path = mobile/plugins/clip_ggml
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"]
path = web/apps/photos/thirdparty/ffmpeg-wasm
url = https://github.com/abhinavkgrd/ffmpeg.wasm.git

View file

@ -50,13 +50,13 @@ Thank you for your support.
## Document
_Coming soon!_
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
quantity, we feel this helps improve the quality and approachability of the
documentation by bringing in more diverse viewpoints and familiarity levels.
See [docs/](docs/README.md) for how to edit these documents.
## Code contributions
If you'd like to contribute code, it is best to start small.

View file

@ -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/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/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)
</div>

View file

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

3
auth/.gitignore vendored
View file

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

View file

@ -12,7 +12,7 @@ multi-device sync.
### Android
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,
without relying on third party stores.

View file

@ -27,6 +27,7 @@ linter:
- use_rethrow_when_possible
- directives_ordering
- always_use_package_imports
- unawaited_futures
analyzer:
errors:
@ -45,6 +46,8 @@ analyzer:
prefer_const_declarations: warning
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
exclude:

View file

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

View file

@ -1,7 +1,14 @@
# 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
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.
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
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

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

View file

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

View file

@ -6,6 +6,7 @@ import 'dart:typed_data';
import 'package:bip39/bip39.dart' as bip39;
import 'package:ente_auth/core/constants.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_out_event.dart';
import 'package:ente_auth/models/key_attributes.dart';
@ -42,6 +43,7 @@ class Configuration {
static const userIDKey = "user_id";
static const hasMigratedSecureStorageKey = "has_migrated_secure_storage";
static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode";
static const endPointKey = "endpoint";
final List<String> onlineSecureKeys = [
keyKey,
secretKeyKey,
@ -318,7 +320,12 @@ class Configuration {
}
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() {

View file

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

View file

@ -13,11 +13,6 @@ import 'package:uuid/uuid.dart';
int kConnectTimeout = 15000;
class Network {
// apiEndpoint points to the Ente server's API endpoint
static const apiEndpoint = String.fromEnvironment(
"endpoint",
defaultValue: kDefaultProductionEndpoint,
);
late Dio _dio;
late Dio _enteDio;
@ -41,7 +36,7 @@ class Network {
},
),
);
_dio.interceptors.add(RequestIdInterceptor());
_enteDio = Dio(
BaseOptions(
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();
@ -65,34 +66,41 @@ class Network {
Dio getDio() => _dio;
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 {
@override
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);
}
}
class EnteRequestInterceptor extends Interceptor {
final SharedPreferences _preferences;
final String enteEndpoint;
final String endpoint;
EnteRequestInterceptor(this._preferences, this.enteEndpoint);
EnteRequestInterceptor(this.endpoint);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (kDebugMode) {
assert(
options.baseUrl == enteEndpoint,
options.baseUrl == 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) {
options.headers.putIfAbsent("X-Auth-Token", () => tokenValue);
}

View file

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

View file

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

View file

@ -0,0 +1 @@
{}

View file

@ -144,6 +144,7 @@
"enterCodeHint": "Geben Sie den 6-stelligen Code \naus Ihrer Authentifikator-App ein.",
"lostDeviceTitle": "Gerät verloren?",
"twoFactorAuthTitle": "Zwei-Faktor-Authentifizierung",
"passkeyAuthTitle": "Passkey Authentifizierung",
"recoverAccount": "Konto wiederherstellen",
"enterRecoveryKeyHint": "Geben Sie Ihren Wiederherstellungsschlüssel ein",
"recover": "Wiederherstellen",
@ -404,5 +405,15 @@
"signOutOtherDevices": "Andere Geräte abmelden",
"doNotSignOut": "Nicht abmelden",
"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"
}

View file

@ -144,7 +144,8 @@
"enterCodeHint": "Enter the 6-digit code from\nyour authenticator app",
"lostDeviceTitle": "Lost device?",
"twoFactorAuthTitle": "Two-factor authentication",
"passkeyAuthTitle": "Passkey authentication",
"passkeyAuthTitle": "Passkey verification",
"verifyPasskey": "Verify passkey",
"recoverAccount": "Recover account",
"enterRecoveryKeyHint": "Enter your recovery key",
"recover": "Recover",
@ -412,6 +413,13 @@
"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!",
"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}"
}

View file

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

View file

@ -144,6 +144,7 @@
"enterCodeHint": "Digite o código de 6 dígitos de\nseu aplicativo autenticador",
"lostDeviceTitle": "Perdeu seu dispositivo?",
"twoFactorAuthTitle": "Autenticação de dois fatores",
"passkeyAuthTitle": "Autenticação via Chave de acesso",
"recoverAccount": "Recuperar conta",
"enterRecoveryKeyHint": "Digite sua chave de recuperação",
"recover": "Recuperar",
@ -404,5 +405,15 @@
"signOutOtherDevices": "Terminar sessão em outros dispositivos",
"doNotSignOut": "Não encerrar sessão",
"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}"
}

View file

@ -59,6 +59,12 @@
"recreatePassword": "Återskapa lösenord",
"useRecoveryKey": "Använd återställningsnyckel",
"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",
"existingUser": "Befintlig användare",
"delete": "Radera",
@ -68,9 +74,23 @@
"suggestFeatures": "Föreslå funktionalitet",
"faq": "FAQ",
"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",
"strongStrength": "Stark",
"moderateStrength": "Måttligt",
"confirmPassword": "Bekräfta lösenord",
"close": "Stäng",
"language": "Språk",
"searchHint": "Sök...",
"search": "Sök",
"sorryUnableToGenCode": "Tyvärr, det gick inte att generera en kod för {issuerName}",
@ -83,5 +103,57 @@
"copiedNextToClipboard": "Kopierade nästa kod till urklipp",
"error": "Fel",
"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."
}

View file

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

View file

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

View file

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

View file

@ -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/models/button_result.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/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
@ -34,8 +36,12 @@ class OnboardingPage extends StatefulWidget {
}
class _OnboardingPageState extends State<OnboardingPage> {
static const kDeveloperModeTapCountThreshold = 7;
late StreamSubscription<TriggerLogoutEvent> _triggerLogoutEvent;
int _developerModeTapCount = 0;
@override
void initState() {
_triggerLogoutEvent =
@ -57,125 +63,152 @@ class _OnboardingPageState extends State<OnboardingPage> {
final l10n = context.l10n;
return Scaffold(
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"),
),
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: 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,
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"),
),
),
],
),
const SizedBox(height: 100),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: GradientButton(
onTap: _navigateToSignUpPage,
text: l10n.newUser,
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,
),
),
],
),
),
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: 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: 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) {
await Configuration.instance.optForOfflineMode();
// ignore: unawaited_futures
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {

View file

@ -5,7 +5,6 @@ import 'dart:math';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/errors.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/signed_in_event.dart';
import 'package:ente_auth/events/trigger_logout_event.dart';
@ -56,7 +55,7 @@ class AuthenticatorService {
_prefs = await SharedPreferences.getInstance();
_db = AuthenticatorDB.instance;
_offlineDb = OfflineAuthenticatorDB.instance;
_gateway = AuthenticatorGateway(Network.instance.getDio(), _config);
_gateway = AuthenticatorGateway();
if (Configuration.instance.hasConfiguredAccount()) {
unawaited(onlineSync());
}
@ -210,8 +209,8 @@ class AuthenticatorService {
if (deletedIDs.isNotEmpty) {
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) {
_logger.info("Diff limit reached, pulling again");
await _remoteToLocalSync();

View file

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

View file

@ -17,6 +17,28 @@ class PasskeyService {
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 {
try {
final jwtToken = await getJwtToken();

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:ente_auth/core/constants.dart';
@ -71,9 +72,11 @@ class UpdateService {
if (shouldUpdate &&
hasBeen3DaysSinceLastNotification &&
_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);
} else {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,9 +2,12 @@ import 'dart:convert';
import 'package:app_links/app_links.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/models/account/two_factor.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:logging/logging.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -49,14 +52,27 @@ class _PasskeyPageState extends State<PasskeyPage> {
if (!context.mounted ||
Configuration.instance.hasConfiguredAccount() ||
link == null) {
_logger.warning(
'ignored deeplink: contextMounted ${context.mounted} hasConfiguredAccount ${Configuration.instance.hasConfiguredAccount()}',
);
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() {
final l10n = context.l10n;
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,
),
),
),
),
),
],
),
),
);
}

View file

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

View file

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

View file

@ -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/services/update_service.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/utils/platform_util.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';
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;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,6 +22,7 @@ import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
class SecuritySectionWidget extends StatefulWidget {
const SecuritySectionWidget({super.key});
@ -33,6 +34,7 @@ class SecuritySectionWidget extends StatefulWidget {
class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
final _config = Configuration.instance;
late bool _hasLoggedIn;
final Logger _logger = Logger('SecuritySectionWidget');
@override
void initState() {
@ -76,7 +78,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () => PasskeyService.instance.openPasskeyPage(context),
onTap: () async => await onPasskeyClick(context),
),
sectionOptionSpacing,
MenuItemWidget(
@ -119,6 +121,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
);
await PlatformUtil.refocusWindows();
if (hasAuthenticated) {
// ignore: unawaited_futures
Navigator.of(context).push(
MaterialPageRoute(
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 {
try {
final UserDetails details =

View file

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

View file

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

View file

@ -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/data/data_section_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/security_section_widget.dart';
import 'package:ente_auth/ui/settings/social_section_widget.dart';
@ -156,6 +157,7 @@ class SettingsPage extends StatelessWidget {
sectionSpacing,
const AboutSectionWidget(),
const AppVersionWidget(),
const DeveloperSettingsWidget(),
const SupportDevWidget(),
const Padding(
padding: EdgeInsets.only(bottom: 60),

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
Future showToast(
void showToast(
BuildContext context,
String message, {
toastLength = Toast.LENGTH_LONG,
@ -11,7 +11,7 @@ Future showToast(
}) async {
try {
await Fluttertoast.cancel();
return Fluttertoast.showToast(
await Fluttertoast.showToast(
msg: message,
toastLength: toastLength,
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);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
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).
### Build from source
You can also build these binaries yourself
```shell
./release.sh
```
Or you can build from source
```shell
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.
```shell
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.
##### Add an account
#### Add an account
```shell
ente account add
```
##### List accounts
#### List accounts
```shell
ente account list
```
##### Change export directory
#### Change export directory
```shell
ente account update --email email@domain.com --dir ~/photos
```
### Export
##### Start export
#### Start export
```shell
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
@ -51,16 +79,22 @@ If you fancy Docker, you can also run the CLI within a container.
### 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
```shell
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
```bash
```shell
docker-compose up -d
```
@ -69,20 +103,8 @@ docker-compose up -d
docker-compose exec ente /bin/sh
```
#### Directly executing commands
```shell
docker run -it --rm ente:latest ls
```
---
## Releases
Run the release script to build the binary and run it.
```shell
./release.sh
```

View file

@ -62,7 +62,7 @@ var updateAccCmd = &cobra.Command{
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,
App: api.StringToApp(app),
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() {
// Add 'config' subcommands to the root command
rootCmd.AddCommand(accountCmd)
// Add 'config' subcommands to the 'config' command
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'")
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
cli/cmd/admin.go Normal file
View file

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

View file

@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"github.com/ente-io/cli/pkg"
"github.com/spf13/cobra/doc"
"os"
"runtime"
@ -11,7 +12,7 @@ import (
"github.com/spf13/cobra"
)
const AppVersion = "0.1.11"
var version string
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.
// 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
version = ver
err := rootCmd.Execute()
if err != nil {
os.Exit(1)

View file

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

10
cli/config.yaml.example Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more