Merge remote-tracking branch 'origin/main' into beta
This commit is contained in:
commit
5634b50528
483 changed files with 7833 additions and 7473 deletions
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal 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
|
9
.github/workflows/auth-crowdin.yml
vendored
9
.github/workflows/auth-crowdin.yml
vendored
|
@ -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:
|
||||
|
|
6
.github/workflows/auth-lint.yml
vendored
6
.github/workflows/auth-lint.yml
vendored
|
@ -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"
|
||||
|
|
2
.github/workflows/auth-release.yml
vendored
2
.github/workflows/auth-release.yml
vendored
|
@ -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:
|
||||
|
|
3
.github/workflows/cli-release.yml
vendored
3
.github/workflows/cli-release.yml
vendored
|
@ -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
47
.github/workflows/docs-deploy.yml
vendored
Normal 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
37
.github/workflows/docs-verify-build.yml
vendored
Normal 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
|
9
.github/workflows/mobile-crowdin.yml
vendored
9
.github/workflows/mobile-crowdin.yml
vendored
|
@ -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:
|
||||
|
|
6
.github/workflows/mobile-lint.yml
vendored
6
.github/workflows/mobile-lint.yml
vendored
|
@ -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"
|
||||
|
|
8
.github/workflows/mobile-release.yml
vendored
8
.github/workflows/mobile-release.yml
vendored
|
@ -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
|
||||
|
|
6
.github/workflows/server-lint.yml
vendored
6
.github/workflows/server-lint.yml
vendored
|
@ -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"
|
||||
|
|
15
.github/workflows/web-crowdin.yml
vendored
15
.github/workflows/web-crowdin.yml
vendored
|
@ -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:
|
||||
|
|
43
.github/workflows/web-deploy-accounts.yml
vendored
Normal file
43
.github/workflows/web-deploy-accounts.yml
vendored
Normal 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
43
.github/workflows/web-deploy-auth.yml
vendored
Normal 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
43
.github/workflows/web-deploy-cast.yml
vendored
Normal 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
43
.github/workflows/web-deploy-photos.yml
vendored
Normal 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"
|
15
.github/workflows/web-lint.yml
vendored
15
.github/workflows/web-lint.yml
vendored
|
@ -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
94
.github/workflows/web-nightly.yml
vendored
Normal 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
52
.github/workflows/web-preview.yml
vendored
Normal 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
11
.gitmodules
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
3
auth/.gitignore
vendored
|
@ -9,6 +9,9 @@
|
|||
.history
|
||||
.svn/
|
||||
|
||||
# Editors
|
||||
.vscode/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
3
auth/lib/events/endpoint_updated_event.dart
Normal file
3
auth/lib/events/endpoint_updated_event.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
import 'package:ente_auth/events/event.dart';
|
||||
|
||||
class EndpointUpdatedEvent extends Event {}
|
|
@ -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;
|
||||
|
|
1
auth/lib/l10n/arb/app_bg.arb
Normal file
1
auth/lib/l10n/arb/app_bg.arb
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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"
|
||||
}
|
|
@ -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}"
|
||||
}
|
|
@ -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} に接続しました"
|
||||
}
|
|
@ -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}"
|
||||
}
|
|
@ -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."
|
||||
}
|
|
@ -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}"
|
||||
}
|
|
@ -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),
|
||||
|
|
13
auth/lib/models/account/two_factor.dart
Normal file
13
auth/lib/models/account/two_factor.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -60,6 +60,7 @@ class LocalAuthenticationService {
|
|||
.setEnabled(Configuration.instance.shouldShowLockScreen());
|
||||
}
|
||||
} else {
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
context,
|
||||
errorDialogTitle,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -240,7 +240,7 @@ class DeleteAccountPage extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
);
|
||||
|
||||
// ignore: unawaited_futures
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
@ -106,6 +106,7 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
|
|||
),
|
||||
);
|
||||
} catch (e) {
|
||||
// ignore: unawaited_futures
|
||||
showGenericErrorDialog(context: context);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -146,6 +146,7 @@ class ProgressDialog {
|
|||
try {
|
||||
if (!_isShowing) {
|
||||
_dialog = _Body();
|
||||
// ignore: unawaited_futures
|
||||
showDialog<dynamic>(
|
||||
context: _context!,
|
||||
barrierDismissible: _barrierDismissible,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ class DangerSectionWidget extends StatelessWidget {
|
|||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
// ignore: unawaited_futures
|
||||
routeToPage(context, const DeleteAccountPage());
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
90
auth/lib/ui/settings/developer_settings_page.dart
Normal file
90
auth/lib/ui/settings/developer_settings_page.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
27
auth/lib/ui/settings/developer_settings_widget.dart
Normal file
27
auth/lib/ui/settings/developer_settings_widget.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -48,6 +48,7 @@ class _AdvancedSectionWidgetState extends State<AdvancedSectionWidget> {
|
|||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
final locale = await getLocale();
|
||||
// ignore: unawaited_futures
|
||||
routeToPage(
|
||||
context,
|
||||
LanguageSelectorPage(
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -81,6 +81,7 @@ class SocialsMenuItemWidget extends StatelessWidget {
|
|||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
// ignore: unawaited_futures
|
||||
launchUrlString(
|
||||
url,
|
||||
mode: launchInExternalApp
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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");
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
4
auth/migration-guides/README.md
Normal file
4
auth/migration-guides/README.md
Normal 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.
|
||||
|
|
@ -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/)
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
@ -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
90
cli/cmd/admin.go
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
10
cli/config.yaml.example
Normal 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
|
28
cli/docs/generated/ente.md
Normal file
28
cli/docs/generated/ente.md
Normal 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
|
19
cli/docs/generated/ente_account.md
Normal file
19
cli/docs/generated/ente_account.md
Normal 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
|
19
cli/docs/generated/ente_account_add.md
Normal file
19
cli/docs/generated/ente_account_add.md
Normal 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
|
21
cli/docs/generated/ente_account_get-token.md
Normal file
21
cli/docs/generated/ente_account_get-token.md
Normal 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
|
19
cli/docs/generated/ente_account_list.md
Normal file
19
cli/docs/generated/ente_account_list.md
Normal 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
|
22
cli/docs/generated/ente_account_update.md
Normal file
22
cli/docs/generated/ente_account_update.md
Normal 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
|
22
cli/docs/generated/ente_admin.md
Normal file
22
cli/docs/generated/ente_admin.md
Normal 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
|
21
cli/docs/generated/ente_admin_disable-2fa.md
Normal file
21
cli/docs/generated/ente_admin_disable-2fa.md
Normal 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
Loading…
Reference in a new issue