Merge branch 'main' into remove_awaits
This commit is contained in:
commit
b52cf1605d
56 changed files with 1137 additions and 283 deletions
.github/workflows
auth-crowdin.ymldocs-deploy.ymlmobile-crowdin.ymlweb-crowdin.ymlweb-deploy-accounts.ymlweb-deploy-auth.ymlweb-deploy-cast.ymlweb-deploy-photos.ymlweb-nightly.yml
CONTRIBUTING.mdREADME.mdSUPPORT.mdauth
docs
mobile/lib
generated
l10n
models/account
services
ui
server
cmd/museum
ente
migrations
pkg
web
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 every 24 hours]
|
||||
- cron: "50 1 * * *"
|
||||
# Also allow manually running the workflow
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
synchronize-with-crowdin:
|
||||
|
|
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"
|
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 every 24 hours]
|
||||
- cron: "40 1 * * *"
|
||||
# Also allow manually running the workflow
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
synchronize-with-crowdin:
|
||||
|
|
9
.github/workflows/web-crowdin.yml
vendored
9
.github/workflows/web-crowdin.yml
vendored
|
@ -3,15 +3,16 @@ 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
|
||||
# See: [Note: Run every 24 hours]
|
||||
- cron: "20 1 * * *"
|
||||
# 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"
|
92
.github/workflows/web-nightly.yml
vendored
Normal file
92
.github/workflows/web-nightly.yml
vendored
Normal file
|
@ -0,0 +1,92 @@
|
|||
name: "Nightly (web)"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# [Note: Run 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
|
||||
|
||||
- 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"
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -1,14 +1,34 @@
|
|||
# Docs
|
||||
|
||||
Help and documentation for Ente's products
|
||||
Help and documentation for Ente's products.
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> **Currently not published**. There are bits we need to clean up before
|
||||
> publishing these docs. They'll likely be available at help.ente.io once we
|
||||
> wrap those loose ends up.
|
||||
You can find the live version of these at
|
||||
**[help.ente.io](https://help.ente.io)**.
|
||||
|
||||
## Running
|
||||
## Quick edits
|
||||
|
||||
You can edit these files directly on GitHub and open a pull request.
|
||||
[help.ente.io](https://help.ente.io) will automatically get updated with your
|
||||
changes in a few minutes after your pull request is merged.
|
||||
|
||||
## Running locally
|
||||
|
||||
|
||||
The above workflow is great since it doesn't require you to setup anything on
|
||||
your local machine. But if you plan on contributing frequently, you might find
|
||||
it easier to run things locally.
|
||||
|
||||
Clone this repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/ente-io/ente
|
||||
```
|
||||
|
||||
Change to this directory
|
||||
|
||||
```sh
|
||||
cd ente/docs
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
|
||||
|
@ -22,27 +42,12 @@ Then start a local server
|
|||
yarn dev
|
||||
```
|
||||
|
||||
## Workflow
|
||||
For an editor, VSCode is a good choice. Also install the Prettier extension for
|
||||
VSCode, and set VSCode to format on save. This way the editor will automatically
|
||||
format and wrap the text using the project's standard, so you can just focus on
|
||||
the content.
|
||||
|
||||
You can edit these files directly on GitHub and open a pull request. That is the
|
||||
easiest workflow to get started without needing to install anything on your
|
||||
local machine.
|
||||
|
||||
If you plan on contributing frequently, we recommend using an editor. VSCode is
|
||||
a good choice. Also install the Prettier extension for VSCode, and set VSCode to
|
||||
format on save. This way the editor will automatically format and wrap the text
|
||||
using the project's standard, so you can just focus on the content.
|
||||
|
||||
Note that we currently don't enforce these formatting standards to make it easy
|
||||
for people unfamiliar with programming to also be able to make edits from GitHub
|
||||
directly.
|
||||
|
||||
This is a common theme - unlike the rest of the codebase where we expect some
|
||||
baseline understanding of the tools involved, the docs are meant to be a place
|
||||
for non-technical people to also provide their input. The reason for this is not
|
||||
to increase the number of docs, but to bring more diversity to them. Such
|
||||
diversity of viewpoints is essential for evolving documents that can be of help
|
||||
to people of varying level of familiarity with tech.
|
||||
## Have fun!
|
||||
|
||||
If you're unsure about how to do something, just look around in the other files
|
||||
and copy paste whatever seems to match the look of what you're trying to do. And
|
||||
|
|
|
@ -7,6 +7,7 @@ export default defineConfig({
|
|||
description: "Documentation and help for Ente's products",
|
||||
head: [["link", { rel: "icon", type: "image/png", href: "/favicon.png" }]],
|
||||
cleanUrls: true,
|
||||
ignoreDeadLinks: 'localhostLinks',
|
||||
themeConfig: {
|
||||
// We use the default theme (with some CSS color overrides). This
|
||||
// themeConfig block can be used to further customize the default theme.
|
||||
|
|
|
@ -74,6 +74,41 @@ export const sidebar = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Self hosting",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: "Getting started", link: "/self-hosting/" },
|
||||
{
|
||||
text: "Guides",
|
||||
items: [
|
||||
{ text: "Introduction", link: "/self-hosting/guides/" },
|
||||
{
|
||||
text: "System requirements",
|
||||
link: "/self-hosting/guides/system-requirements",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "FAQ",
|
||||
items: [
|
||||
{
|
||||
text: "Verification code",
|
||||
link: "/self-hosting/faq/otp",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Troubleshooting",
|
||||
items: [
|
||||
{
|
||||
text: "Yarn",
|
||||
link: "/self-hosting/troubleshooting/yarn",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "About",
|
||||
link: "/about/",
|
||||
|
|
|
@ -11,5 +11,4 @@ can use it to safely store your 2FA codes (second-factor authentication codes).
|
|||
> [!CAUTION]
|
||||
>
|
||||
> These docs are still incomplete. If you feel like documenting an issue you ran
|
||||
> into and then found a solution to, help us [fill them
|
||||
> in](/about/contributing).
|
||||
> into and then found a solution to, help us [fill them in](/about/contribute).
|
||||
|
|
|
@ -10,4 +10,4 @@ Ende-zu-Ende-verschlüsselte Authenticator-App für jedermann. Wir sind froh, da
|
|||
du hier bist!
|
||||
|
||||
**Please note that this German translation is currently just a placeholder.**
|
||||
Know German? [Help us fill this in!](/about/contributing).
|
||||
Know German? [Help us fill this in!](/about/contribute).
|
||||
|
|
|
@ -15,5 +15,5 @@ If you are using VPN, please try disabling the VPN or switching provider.
|
|||
|
||||
The desktop/web app tries to detect if a particular file is video or image. If
|
||||
the detection fails, then the app skips the upload. Please contact our
|
||||
[support](support.ente.io) if you find that a valid file did not get detected
|
||||
and uploaded.
|
||||
[support](mailto:support@ente.io) if you find that a valid file did not get
|
||||
detected and uploaded.
|
||||
|
|
|
@ -15,7 +15,7 @@ the logs just make the process a bit faster and easier.
|
|||
|
||||
### Mobile
|
||||
|
||||
Placeholder
|
||||
Steps for mobile. Still a placeholder.
|
||||
|
||||
### Desktop
|
||||
|
||||
|
|
19
docs/docs/self-hosting/faq/otp.md
Normal file
19
docs/docs/self-hosting/faq/otp.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
title: Verification code
|
||||
description: Getting the OTP for a self host Ente
|
||||
---
|
||||
|
||||
# Verification code
|
||||
|
||||
The self-hosted Ente by default does not send out emails, so you can pick the
|
||||
verification code by:
|
||||
|
||||
* Getting it from the server logs, or
|
||||
|
||||
* Reading it from the DB (otts table)
|
||||
|
||||
You can also set pre-defined hardcoded OTTs for certain users when running
|
||||
locally by creating a `museum.yaml` and adding the `internal.hardcoded-ott`
|
||||
configuration setting to it. See
|
||||
[local.yaml](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml)
|
||||
in the server source code for details about how to define this.
|
9
docs/docs/self-hosting/guides/index.md
Normal file
9
docs/docs/self-hosting/guides/index.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
title: Self Hosting
|
||||
description: Guides for self hosting Ente Photos and/or Ente Auth
|
||||
---
|
||||
|
||||
# Guides
|
||||
|
||||
If you've figured out how to do something, help others out by adding
|
||||
walkthroughs, tutorials and other FAQ pages in this directory.
|
14
docs/docs/self-hosting/guides/system-requirements.md
Normal file
14
docs/docs/self-hosting/guides/system-requirements.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: System requirements
|
||||
description: System requirements for running Ente's server
|
||||
---
|
||||
|
||||
# System requirements
|
||||
|
||||
There aren't any "minimum" system requirements as such, the server process is
|
||||
very light weight - it's just a single go binary, and it doesn't do any server
|
||||
side ML, so I feel it should be able to run on anything reasonable.
|
||||
|
||||
We've used the server quite easily on small cloud instances, old laptops etc. A
|
||||
community member also reported being able to run the server on [very low-end
|
||||
embedded devices](https://github.com/ente-io/ente/discussions/594).
|
64
docs/docs/self-hosting/index.md
Normal file
64
docs/docs/self-hosting/index.md
Normal file
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
title: Self Hosting
|
||||
description: Getting started self hosting Ente Photos and/or Ente Auth
|
||||
---
|
||||
|
||||
# Self Hosting
|
||||
|
||||
The entire source code for Ente is open source, including the servers. This is
|
||||
the same code we use for our own cloud service.
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> To get some context, you might find our [blog
|
||||
> post](https://ente.io/blog/open-sourcing-our-server/) announcing the open
|
||||
> sourcing of our server useful.
|
||||
|
||||
## Getting started
|
||||
|
||||
Start the server
|
||||
|
||||
```sh
|
||||
git clone https://github.com/ente-io/ente
|
||||
cd ente/server
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Then in a separate terminal, you can run (e.g) the web client
|
||||
|
||||
```sh
|
||||
cd ente/web
|
||||
git submodule update --init --recursive
|
||||
yarn install
|
||||
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev
|
||||
```
|
||||
|
||||
That's about it. If you open http://localhost:3000, you will be able to create
|
||||
an account on a Ente Photos web app running on your machine, and this web app
|
||||
will be connecting to the server running on your local machine at
|
||||
localhost:8080.
|
||||
|
||||
## Next steps
|
||||
|
||||
* More details about the server are in its
|
||||
[README](https://github.com/ente-io/ente/tree/main/server#readme)
|
||||
|
||||
* More details about running the server (with or without Docker) are in
|
||||
[RUNNING](https://github.com/ente-io/ente/blob/main/server/RUNNING.md)
|
||||
|
||||
* If you have questions around self-hosting that are not answered in any of the
|
||||
existing documentation, you can ask in our [GitHub
|
||||
Discussions](https://github.com/ente-io/ente/discussions). **Please remember
|
||||
to search first if the query has been already asked and answered.**
|
||||
|
||||
## Contributing!
|
||||
|
||||
While we would love to provide a completely seamless self-hosting experience,
|
||||
right now we do not have the engineering bandwidth to answer all queries,
|
||||
document everything exactly etc. We will try (that's why we're writing this!),
|
||||
but we also hope that community members will step up to fill any gaps.
|
||||
|
||||
One particular way in which you can help is by adding new [guides](guides/) on
|
||||
this help site. The documentation is written in Markdown and adding new pages is
|
||||
[easy](https://github.com/ente-io/ente/tree/main/docs#readme). Editing existing
|
||||
pages is even easier: at the bottom of each page is an _Edit this page_ link.
|
10
docs/docs/self-hosting/troubleshooting/yarn.md
Normal file
10
docs/docs/self-hosting/troubleshooting/yarn.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Yarn errors
|
||||
description: Fixing yarn install errors when trying to self host Ente
|
||||
---
|
||||
|
||||
# Yarn
|
||||
|
||||
If your `yarn install` is failing, make sure you are using Yarn Classic
|
||||
|
||||
* https://classic.yarnpkg.com/lang/en/docs/install
|
7
mobile/lib/generated/intl/messages_en.dart
generated
7
mobile/lib/generated/intl/messages_en.dart
generated
|
@ -958,7 +958,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"pair": MessageLookupByLibrary.simpleMessage("Pair"),
|
||||
"passkey": MessageLookupByLibrary.simpleMessage("Passkey"),
|
||||
"passkeyAuthTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Passkey authentication"),
|
||||
MessageLookupByLibrary.simpleMessage("Passkey verification"),
|
||||
"password": MessageLookupByLibrary.simpleMessage("Password"),
|
||||
"passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage(
|
||||
"Password changed successfully"),
|
||||
|
@ -1446,6 +1446,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"verifyEmail": MessageLookupByLibrary.simpleMessage("Verify email"),
|
||||
"verifyEmailID": m64,
|
||||
"verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verify"),
|
||||
"verifyPasskey": MessageLookupByLibrary.simpleMessage("Verify passkey"),
|
||||
"verifyPassword":
|
||||
MessageLookupByLibrary.simpleMessage("Verify password"),
|
||||
"verifying": MessageLookupByLibrary.simpleMessage("Verifying..."),
|
||||
|
@ -1465,8 +1466,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"viewer": MessageLookupByLibrary.simpleMessage("Viewer"),
|
||||
"visitWebToManage": MessageLookupByLibrary.simpleMessage(
|
||||
"Please visit web.ente.io to manage your subscription"),
|
||||
"waitingForBrowserRequest": MessageLookupByLibrary.simpleMessage(
|
||||
"Waiting for browser request..."),
|
||||
"waitingForVerification":
|
||||
MessageLookupByLibrary.simpleMessage("Waiting for verification..."),
|
||||
"waitingForWifi":
|
||||
MessageLookupByLibrary.simpleMessage("Waiting for WiFi..."),
|
||||
"weAreOpenSource":
|
||||
|
|
5
mobile/lib/generated/intl/messages_pt.dart
generated
5
mobile/lib/generated/intl/messages_pt.dart
generated
|
@ -820,6 +820,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"language": MessageLookupByLibrary.simpleMessage("Idioma"),
|
||||
"lastUpdated":
|
||||
MessageLookupByLibrary.simpleMessage("Última atualização"),
|
||||
"launchPasskeyUrlAgain": MessageLookupByLibrary.simpleMessage(
|
||||
"Iniciar a URL de chave de acesso novamente"),
|
||||
"leave": MessageLookupByLibrary.simpleMessage("Sair"),
|
||||
"leaveAlbum": MessageLookupByLibrary.simpleMessage("Sair do álbum"),
|
||||
"leaveFamily": MessageLookupByLibrary.simpleMessage("Sair da família"),
|
||||
|
@ -984,6 +986,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"orPickAnExistingOne":
|
||||
MessageLookupByLibrary.simpleMessage("Ou escolha um existente"),
|
||||
"pair": MessageLookupByLibrary.simpleMessage("Parear"),
|
||||
"passkey": MessageLookupByLibrary.simpleMessage("Chave de acesso"),
|
||||
"passkeyAuthTitle": MessageLookupByLibrary.simpleMessage(
|
||||
"Autenticação via Chave de acesso"),
|
||||
"password": MessageLookupByLibrary.simpleMessage("Senha"),
|
||||
"passwordChangedSuccessfully":
|
||||
MessageLookupByLibrary.simpleMessage("Senha alterada com sucesso"),
|
||||
|
|
78
mobile/lib/generated/intl/messages_zh.dart
generated
78
mobile/lib/generated/intl/messages_zh.dart
generated
|
@ -62,7 +62,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
|
||||
static String m14(albumName) => "这将删除用于访问\"${albumName}\"的公共链接。";
|
||||
|
||||
static String m15(supportEmail) => "请从您注册的电子邮件地址拖放一封邮件到 ${supportEmail}";
|
||||
static String m15(supportEmail) => "请从您注册的邮箱发送一封邮件到 ${supportEmail}";
|
||||
|
||||
static String m16(count, storageSaved) =>
|
||||
"您已经清理了 ${Intl.plural(count, other: '${count} 个重复文件')}, 释放了 (${storageSaved}!)";
|
||||
|
@ -81,7 +81,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"此相册中的 ${Intl.plural(count, one: '1 个文件', other: '${formattedNumber} 个文件')} 已安全备份";
|
||||
|
||||
static String m22(storageAmountInGB) =>
|
||||
"每当有人注册付费计划时${storageAmountInGB} GB 并应用了您的代码";
|
||||
"每当有人使用您的代码注册付费计划时您将获得${storageAmountInGB} GB";
|
||||
|
||||
static String m23(freeAmount, storageUnit) =>
|
||||
"${freeAmount} ${storageUnit} 空闲";
|
||||
|
@ -126,7 +126,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
|
||||
static String m40(storeName) => "在 ${storeName} 上给我们评分";
|
||||
|
||||
static String m41(storageInGB) => "3. 你都可以免费获得 ${storageInGB} GB*";
|
||||
static String m41(storageInGB) => "3. 你和朋友都将免费获得 ${storageInGB} GB*";
|
||||
|
||||
static String m42(userEmail) =>
|
||||
"${userEmail} 将从这个共享相册中删除\n\nTA们添加的任何照片也将从相册中删除";
|
||||
|
@ -143,10 +143,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
static String m47(verificationID) => "这是我的ente.io 的验证 ID: ${verificationID}。";
|
||||
|
||||
static String m48(verificationID) =>
|
||||
"嘿,你能确认这是你的 ente.io 验证 ID:${verificationID}";
|
||||
"嘿,你能确认这是你的 ente.io 验证 ID吗:${verificationID}";
|
||||
|
||||
static String m49(referralCode, referralStorageInGB) =>
|
||||
"ente转发码: ${referralCode} \n\n在设置 → 常规 → 推荐中应用它以在注册付费计划后可以免费获得 ${referralStorageInGB} GB\n\nhttps://ente.io";
|
||||
"ente推荐码: ${referralCode} \n\n注册付费计划后在设置 → 常规 → 推荐中应用它以免费获得 ${referralStorageInGB} GB空间\n\nhttps://ente.io";
|
||||
|
||||
static String m50(numberOfPeople) =>
|
||||
"${Intl.plural(numberOfPeople, zero: '与特定人员共享', one: '与 1 人共享', other: '与 ${numberOfPeople} 人共享')}";
|
||||
|
@ -247,7 +247,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"androidBiometricNotRecognized":
|
||||
MessageLookupByLibrary.simpleMessage("无法识别。请重试。"),
|
||||
"androidBiometricRequiredTitle":
|
||||
MessageLookupByLibrary.simpleMessage("需要生物量"),
|
||||
MessageLookupByLibrary.simpleMessage("需要生物识别认证"),
|
||||
"androidBiometricSuccess": MessageLookupByLibrary.simpleMessage("成功"),
|
||||
"androidCancelButton": MessageLookupByLibrary.simpleMessage("取消"),
|
||||
"androidDeviceCredentialsRequiredTitle":
|
||||
|
@ -255,7 +255,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"androidDeviceCredentialsSetupDescription":
|
||||
MessageLookupByLibrary.simpleMessage("需要设备凭据"),
|
||||
"androidGoToSettingsDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"未在您的设备上设置生物鉴别身份验证。前往“设置>安全”添加生物鉴别身份验证。"),
|
||||
"您未在该设备上设置生物识别身份验证。前往“设置>安全”添加生物识别身份验证。"),
|
||||
"androidIosWebDesktop":
|
||||
MessageLookupByLibrary.simpleMessage("安卓, iOS, 网页端, 桌面端"),
|
||||
"androidSignInTitle": MessageLookupByLibrary.simpleMessage("需要身份验证"),
|
||||
|
@ -286,7 +286,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
MessageLookupByLibrary.simpleMessage("您删除账户的主要原因是什么?"),
|
||||
"askYourLovedOnesToShare":
|
||||
MessageLookupByLibrary.simpleMessage("请您的亲人分享"),
|
||||
"atAFalloutShelter": MessageLookupByLibrary.simpleMessage("在一个保护所中"),
|
||||
"atAFalloutShelter": MessageLookupByLibrary.simpleMessage("在一个庇护所中"),
|
||||
"authToChangeEmailVerificationSetting":
|
||||
MessageLookupByLibrary.simpleMessage("请进行身份验证以更改电子邮件验证"),
|
||||
"authToChangeLockscreenSetting":
|
||||
|
@ -360,14 +360,14 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"clickOnTheOverflowMenu":
|
||||
MessageLookupByLibrary.simpleMessage("• 点击溢出菜单"),
|
||||
"close": MessageLookupByLibrary.simpleMessage("关闭"),
|
||||
"clubByCaptureTime": MessageLookupByLibrary.simpleMessage("按抓取时间断开"),
|
||||
"clubByCaptureTime": MessageLookupByLibrary.simpleMessage("按拍摄时间分组"),
|
||||
"clubByFileName": MessageLookupByLibrary.simpleMessage("按文件名排序"),
|
||||
"codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("代码已应用"),
|
||||
"codeCopiedToClipboard":
|
||||
MessageLookupByLibrary.simpleMessage("代码已复制到剪贴板"),
|
||||
"codeUsedByYou": MessageLookupByLibrary.simpleMessage("您所使用的代码"),
|
||||
"collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"创建一个链接以允许人们在您的共享相册中添加和查看照片,而无需应用程序或账户。 非常适合收集活动照片。"),
|
||||
"创建一个链接以允许其他人在您的共享相册中添加和查看照片,而无需应用程序或ente账户。 非常适合收集活动照片。"),
|
||||
"collaborativeLink": MessageLookupByLibrary.simpleMessage("协作链接"),
|
||||
"collaborativeLinkCreatedFor": m9,
|
||||
"collaborator": MessageLookupByLibrary.simpleMessage("协作者"),
|
||||
|
@ -408,7 +408,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"couldNotUpdateSubscription":
|
||||
MessageLookupByLibrary.simpleMessage("无法升级订阅"),
|
||||
"count": MessageLookupByLibrary.simpleMessage("计数"),
|
||||
"crashReporting": MessageLookupByLibrary.simpleMessage("崩溃报告"),
|
||||
"crashReporting": MessageLookupByLibrary.simpleMessage("上报崩溃"),
|
||||
"create": MessageLookupByLibrary.simpleMessage("创建"),
|
||||
"createAccount": MessageLookupByLibrary.simpleMessage("创建账户"),
|
||||
"createAlbumActionHint":
|
||||
|
@ -427,7 +427,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"dayYesterday": MessageLookupByLibrary.simpleMessage("昨天"),
|
||||
"decrypting": MessageLookupByLibrary.simpleMessage("解密中..."),
|
||||
"decryptingVideo": MessageLookupByLibrary.simpleMessage("正在解密视频..."),
|
||||
"deduplicateFiles": MessageLookupByLibrary.simpleMessage("重复文件"),
|
||||
"deduplicateFiles": MessageLookupByLibrary.simpleMessage("文件去重"),
|
||||
"delete": MessageLookupByLibrary.simpleMessage("删除"),
|
||||
"deleteAccount": MessageLookupByLibrary.simpleMessage("删除账户"),
|
||||
"deleteAccountFeedbackPrompt":
|
||||
|
@ -438,7 +438,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"deleteAlbumDialog": MessageLookupByLibrary.simpleMessage(
|
||||
"也删除此相册中存在的照片(和视频),从 <bold>他们所加入的所有</bold> 其他相册?"),
|
||||
"deleteAlbumsDialogBody": MessageLookupByLibrary.simpleMessage(
|
||||
"这将删除所有空相册。 当您想减少相册列表中的混乱时,这很有用。"),
|
||||
"这将删除所有空相册。 当您想减少相册列表的混乱时,这很有用。"),
|
||||
"deleteAll": MessageLookupByLibrary.simpleMessage("全部删除"),
|
||||
"deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage(
|
||||
"此账户已链接到其他 ente 旗下的应用程序(如果您使用任何 ente 旗下的应用程序)。\\n\\n您在所有 ente 旗下的应用程序中上传的数据将被安排删除,并且您的账户将被永久删除。"),
|
||||
|
@ -456,7 +456,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"deleteProgress": m13,
|
||||
"deleteReason1": MessageLookupByLibrary.simpleMessage("找不到我想要的功能"),
|
||||
"deleteReason2":
|
||||
MessageLookupByLibrary.simpleMessage("应用或某个功能不会有 行为。我认为它应该有的"),
|
||||
MessageLookupByLibrary.simpleMessage("应用或某个功能没有按我的预期运行"),
|
||||
"deleteReason3":
|
||||
MessageLookupByLibrary.simpleMessage("我找到了另一个我喜欢更好的服务"),
|
||||
"deleteReason4": MessageLookupByLibrary.simpleMessage("我的原因未被列出"),
|
||||
|
@ -489,8 +489,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"discord": MessageLookupByLibrary.simpleMessage("Discord"),
|
||||
"dismiss": MessageLookupByLibrary.simpleMessage("忽略"),
|
||||
"distanceInKMUnit": MessageLookupByLibrary.simpleMessage("公里"),
|
||||
"doNotSignOut": MessageLookupByLibrary.simpleMessage("不要退登"),
|
||||
"doThisLater": MessageLookupByLibrary.simpleMessage("稍后再做"),
|
||||
"doNotSignOut": MessageLookupByLibrary.simpleMessage("不要登出"),
|
||||
"doThisLater": MessageLookupByLibrary.simpleMessage("稍后再说"),
|
||||
"doYouWantToDiscardTheEditsYouHaveMade":
|
||||
MessageLookupByLibrary.simpleMessage("您想要放弃您所做的编辑吗?"),
|
||||
"done": MessageLookupByLibrary.simpleMessage("已完成"),
|
||||
|
@ -559,11 +559,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"exif": MessageLookupByLibrary.simpleMessage("EXIF"),
|
||||
"existingUser": MessageLookupByLibrary.simpleMessage("现有用户"),
|
||||
"expiredLinkInfo":
|
||||
MessageLookupByLibrary.simpleMessage("此链接已过期。请选择新的过期时间或禁用链接过期。"),
|
||||
MessageLookupByLibrary.simpleMessage("此链接已过期。请选择新的过期时间或禁用链接有效期。"),
|
||||
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
|
||||
"exportYourData": MessageLookupByLibrary.simpleMessage("导出您的数据"),
|
||||
"faces": MessageLookupByLibrary.simpleMessage("人脸"),
|
||||
"failedToApplyCode": MessageLookupByLibrary.simpleMessage("无法应用代码"),
|
||||
"failedToApplyCode": MessageLookupByLibrary.simpleMessage("无法使用此代码"),
|
||||
"failedToCancel": MessageLookupByLibrary.simpleMessage("取消失败"),
|
||||
"failedToDownloadVideo": MessageLookupByLibrary.simpleMessage("视频下载失败"),
|
||||
"failedToFetchOriginalForEdit":
|
||||
|
@ -575,7 +575,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"failedToVerifyPaymentStatus":
|
||||
MessageLookupByLibrary.simpleMessage("验证支付状态失败"),
|
||||
"familyPlanOverview": MessageLookupByLibrary.simpleMessage(
|
||||
"在您现有的计划中添加 5 名家庭成员,无需支付额外费用。\n\n每个成员都有自己的私人空间,除非共享,否则无法看到彼此的文件。\n\n家庭计划适用于付费订阅的客户。\n\n立即订阅以开始使用!"),
|
||||
"在您现有的计划中添加 5 名家庭成员而无需支付额外费用。\n\n每个成员都有自己的私人空间,除非共享,否则无法看到彼此的文件。\n\n家庭计划适用于已有付费订阅的客户。\n\n立即订阅以开始使用!"),
|
||||
"familyPlanPortalTitle": MessageLookupByLibrary.simpleMessage("家庭"),
|
||||
"familyPlans": MessageLookupByLibrary.simpleMessage("家庭计划"),
|
||||
"faq": MessageLookupByLibrary.simpleMessage("常见问题"),
|
||||
|
@ -614,7 +614,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"goToSettings": MessageLookupByLibrary.simpleMessage("前往设置"),
|
||||
"googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"),
|
||||
"grantFullAccessPrompt":
|
||||
MessageLookupByLibrary.simpleMessage("请在“设置”应用中将权限更改为允许访问所有所有照片"),
|
||||
MessageLookupByLibrary.simpleMessage("请在手机“设置”中授权软件访问所有照片"),
|
||||
"grantPermission": MessageLookupByLibrary.simpleMessage("授予权限"),
|
||||
"groupNearbyPhotos": MessageLookupByLibrary.simpleMessage("将附近的照片分组"),
|
||||
"hearUsExplanation": MessageLookupByLibrary.simpleMessage(
|
||||
|
@ -629,9 +629,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"howToViewShareeVerificationID": MessageLookupByLibrary.simpleMessage(
|
||||
"请让他们在设置屏幕上长按他们的电子邮件地址,并验证两台设备上的 ID 是否匹配。"),
|
||||
"iOSGoToSettingsDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"未在您的设备上设置生物鉴别身份验证。请在您的手机上启用 Touch ID或Face ID。"),
|
||||
"您未在该设备上设置生物识别身份验证。请在您的手机上启用 Touch ID或Face ID。"),
|
||||
"iOSLockOut":
|
||||
MessageLookupByLibrary.simpleMessage("生物鉴别认证已禁用。请锁定并解锁您的屏幕以启用它。"),
|
||||
MessageLookupByLibrary.simpleMessage("生物识别认证已禁用。请锁定并解锁您的屏幕以启用它。"),
|
||||
"iOSOkButton": MessageLookupByLibrary.simpleMessage("好的"),
|
||||
"ignoreUpdate": MessageLookupByLibrary.simpleMessage("忽略"),
|
||||
"ignoredFolderUploadReason": MessageLookupByLibrary.simpleMessage(
|
||||
|
@ -673,6 +673,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
MessageLookupByLibrary.simpleMessage("请帮助我们了解这个信息"),
|
||||
"language": MessageLookupByLibrary.simpleMessage("语言"),
|
||||
"lastUpdated": MessageLookupByLibrary.simpleMessage("最后更新"),
|
||||
"launchPasskeyUrlAgain":
|
||||
MessageLookupByLibrary.simpleMessage("再次启动 通行密钥 URL"),
|
||||
"leave": MessageLookupByLibrary.simpleMessage("离开"),
|
||||
"leaveAlbum": MessageLookupByLibrary.simpleMessage("离开相册"),
|
||||
"leaveFamily": MessageLookupByLibrary.simpleMessage("离开家庭计划"),
|
||||
|
@ -691,9 +693,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"livePhotos": MessageLookupByLibrary.simpleMessage("实况照片"),
|
||||
"loadMessage1": MessageLookupByLibrary.simpleMessage("您可以与家庭分享您的订阅"),
|
||||
"loadMessage2":
|
||||
MessageLookupByLibrary.simpleMessage("到目前为止,我们已经保存了1 000多万个回忆"),
|
||||
MessageLookupByLibrary.simpleMessage("到目前为止,我们已经保存了超过3 000万个回忆"),
|
||||
"loadMessage3":
|
||||
MessageLookupByLibrary.simpleMessage("我们保存你的3个数据副本,一个在地下安全屋中"),
|
||||
MessageLookupByLibrary.simpleMessage("我们保存你的3个数据副本,其中一个在地下安全屋中"),
|
||||
"loadMessage4": MessageLookupByLibrary.simpleMessage("我们所有的应用程序都是开源的"),
|
||||
"loadMessage5":
|
||||
MessageLookupByLibrary.simpleMessage("我们的源代码和加密技术已经由外部审计"),
|
||||
|
@ -722,7 +724,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"logInLabel": MessageLookupByLibrary.simpleMessage("登录"),
|
||||
"loggingOut": MessageLookupByLibrary.simpleMessage("正在退出登录..."),
|
||||
"loginTerms": MessageLookupByLibrary.simpleMessage(
|
||||
"点击登录后,我同意 <u-terms>服务条款</u-terms> 和 <u-policy>隐私政策</u-policy>"),
|
||||
"点击登录时,默认我同意 <u-terms>服务条款</u-terms> 和 <u-policy>隐私政策</u-policy>"),
|
||||
"logout": MessageLookupByLibrary.simpleMessage("退出登录"),
|
||||
"logsDialogBody": MessageLookupByLibrary.simpleMessage(
|
||||
"这将跨日志发送以帮助我们调试您的问题。 请注意,将包含文件名以帮助跟踪特定文件的问题。"),
|
||||
|
@ -732,7 +734,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"machineLearning": MessageLookupByLibrary.simpleMessage("机器学习"),
|
||||
"magicSearch": MessageLookupByLibrary.simpleMessage("魔法搜索"),
|
||||
"magicSearchDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"请使用我们的桌面应用程序来为您库中的待处理项目建立索引。"),
|
||||
"请注意,在所有项目完成索引之前,这将使用更高的带宽和电量。"),
|
||||
"manage": MessageLookupByLibrary.simpleMessage("管理"),
|
||||
"manageDeviceStorage": MessageLookupByLibrary.simpleMessage("管理设备存储"),
|
||||
"manageFamily": MessageLookupByLibrary.simpleMessage("管理家庭计划"),
|
||||
|
@ -811,13 +813,15 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"orPickAnExistingOne":
|
||||
MessageLookupByLibrary.simpleMessage("或者选择一个现有的"),
|
||||
"pair": MessageLookupByLibrary.simpleMessage("配对"),
|
||||
"passkey": MessageLookupByLibrary.simpleMessage("通行密钥"),
|
||||
"passkeyAuthTitle": MessageLookupByLibrary.simpleMessage("通行密钥认证"),
|
||||
"password": MessageLookupByLibrary.simpleMessage("密码"),
|
||||
"passwordChangedSuccessfully":
|
||||
MessageLookupByLibrary.simpleMessage("密码修改成功"),
|
||||
"passwordLock": MessageLookupByLibrary.simpleMessage("密码锁"),
|
||||
"passwordStrength": m34,
|
||||
"passwordWarning": MessageLookupByLibrary.simpleMessage(
|
||||
"我们不储存这个密码,所以如果忘记, <underline>我们不能解密您的数据</underline>"),
|
||||
"我们不储存这个密码,所以如果忘记, <underline>我们将无法解密您的数据</underline>"),
|
||||
"paymentDetails": MessageLookupByLibrary.simpleMessage("付款明细"),
|
||||
"paymentFailed": MessageLookupByLibrary.simpleMessage("支付失败"),
|
||||
"paymentFailedTalkToProvider": m35,
|
||||
|
@ -896,11 +900,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"如果您忘记了您的密码,您的恢复密钥是恢复您的照片的唯一途径。 您可以在“设置 > 账户”中找到您的恢复密钥。\n\n请在此输入您的恢复密钥以确认您已经正确地保存了它。"),
|
||||
"recoverySuccessful": MessageLookupByLibrary.simpleMessage("恢复成功!"),
|
||||
"recreatePasswordBody": MessageLookupByLibrary.simpleMessage(
|
||||
"当前设备的功能不足以验证您的密码,但我们可以以适用于所有设备的方式重新生成。\n\n请使用您的恢复密钥登录并重新生成您的密码(如果您愿意,可以再次使用相同的密码)。"),
|
||||
"当前设备的功能不足以验证您的密码,但我们可以以适用于所有设备的方式重新生成。\n\n请使用您的恢复密钥登录并重新生成您的密码(如果您希望,可以再次使用相同的密码)。"),
|
||||
"recreatePasswordTitle": MessageLookupByLibrary.simpleMessage("重新创建密码"),
|
||||
"reddit": MessageLookupByLibrary.simpleMessage("Reddit"),
|
||||
"referFriendsAnd2xYourPlan":
|
||||
MessageLookupByLibrary.simpleMessage("推荐朋友和 2 倍您的计划"),
|
||||
MessageLookupByLibrary.simpleMessage("把我们推荐给你的朋友然后获得延长一倍的订阅计划"),
|
||||
"referralStep1": MessageLookupByLibrary.simpleMessage("1. 将此代码提供给您的朋友"),
|
||||
"referralStep2": MessageLookupByLibrary.simpleMessage("2. 他们注册一个付费计划"),
|
||||
"referralStep3": m41,
|
||||
|
@ -911,9 +915,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"同时从“设置”->“存储”中清空“最近删除”以领取释放的空间"),
|
||||
"remindToEmptyEnteTrash":
|
||||
MessageLookupByLibrary.simpleMessage("同时清空您的“回收站”以领取释放的空间"),
|
||||
"remoteImages": MessageLookupByLibrary.simpleMessage("远程图像"),
|
||||
"remoteThumbnails": MessageLookupByLibrary.simpleMessage("远程缩略图"),
|
||||
"remoteVideos": MessageLookupByLibrary.simpleMessage("远程视频"),
|
||||
"remoteImages": MessageLookupByLibrary.simpleMessage("云端图像"),
|
||||
"remoteThumbnails": MessageLookupByLibrary.simpleMessage("云端缩略图"),
|
||||
"remoteVideos": MessageLookupByLibrary.simpleMessage("云端视频"),
|
||||
"remove": MessageLookupByLibrary.simpleMessage("移除"),
|
||||
"removeDuplicates": MessageLookupByLibrary.simpleMessage("移除重复内容"),
|
||||
"removeFromAlbum": MessageLookupByLibrary.simpleMessage("从相册中移除"),
|
||||
|
@ -954,9 +958,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"saveCopy": MessageLookupByLibrary.simpleMessage("保存副本"),
|
||||
"saveKey": MessageLookupByLibrary.simpleMessage("保存密钥"),
|
||||
"saveYourRecoveryKeyIfYouHaventAlready":
|
||||
MessageLookupByLibrary.simpleMessage("如果你还没有就请保存你的恢复密钥"),
|
||||
MessageLookupByLibrary.simpleMessage("若您尚未保存,请妥善保存此恢复密钥"),
|
||||
"saving": MessageLookupByLibrary.simpleMessage("正在保存..."),
|
||||
"scanCode": MessageLookupByLibrary.simpleMessage("扫描代码"),
|
||||
"scanCode": MessageLookupByLibrary.simpleMessage("扫描二维码/条码"),
|
||||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage("用您的身份验证器应用\n扫描此条码"),
|
||||
"searchAlbumsEmptySection": MessageLookupByLibrary.simpleMessage("相册"),
|
||||
|
@ -1058,7 +1062,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
MessageLookupByLibrary.simpleMessage("它将从所有相册中删除。"),
|
||||
"singleFileInBothLocalAndRemote": m53,
|
||||
"singleFileInRemoteOnly": m54,
|
||||
"skip": MessageLookupByLibrary.simpleMessage("略过"),
|
||||
"skip": MessageLookupByLibrary.simpleMessage("跳过"),
|
||||
"social": MessageLookupByLibrary.simpleMessage("社交"),
|
||||
"someItemsAreInBothEnteAndYourDevice":
|
||||
MessageLookupByLibrary.simpleMessage("有些项目既在ente 也在您的设备中。"),
|
||||
|
@ -1109,7 +1113,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"syncProgress": m59,
|
||||
"syncStopped": MessageLookupByLibrary.simpleMessage("同步已停止"),
|
||||
"syncing": MessageLookupByLibrary.simpleMessage("正在同步···"),
|
||||
"systemTheme": MessageLookupByLibrary.simpleMessage("系统"),
|
||||
"systemTheme": MessageLookupByLibrary.simpleMessage("适应系统"),
|
||||
"tapToCopy": MessageLookupByLibrary.simpleMessage("点击以复制"),
|
||||
"tapToEnterCode": MessageLookupByLibrary.simpleMessage("点击以输入代码"),
|
||||
"tempErrorContactSupportIfPersists":
|
||||
|
@ -1137,7 +1141,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"thisAlbumAlreadyHDACollaborativeLink":
|
||||
MessageLookupByLibrary.simpleMessage("此相册已经有一个协作链接"),
|
||||
"thisCanBeUsedToRecoverYourAccountIfYou":
|
||||
MessageLookupByLibrary.simpleMessage("如果您丢失了双因素,这可以用来恢复您的账户"),
|
||||
MessageLookupByLibrary.simpleMessage("如果您丢失了双因素验证方式,这可以用来恢复您的账户"),
|
||||
"thisDevice": MessageLookupByLibrary.simpleMessage("此设备"),
|
||||
"thisEmailIsAlreadyInUse":
|
||||
MessageLookupByLibrary.simpleMessage("这个邮箱地址已经被使用"),
|
||||
|
|
22
mobile/lib/generated/l10n.dart
generated
22
mobile/lib/generated/l10n.dart
generated
|
@ -8308,11 +8308,11 @@ class S {
|
|||
);
|
||||
}
|
||||
|
||||
/// `Waiting for browser request...`
|
||||
String get waitingForBrowserRequest {
|
||||
/// `Waiting for verification...`
|
||||
String get waitingForVerification {
|
||||
return Intl.message(
|
||||
'Waiting for browser request...',
|
||||
name: 'waitingForBrowserRequest',
|
||||
'Waiting for verification...',
|
||||
name: 'waitingForVerification',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
|
@ -8338,16 +8338,26 @@ class S {
|
|||
);
|
||||
}
|
||||
|
||||
/// `Passkey authentication`
|
||||
/// `Passkey verification`
|
||||
String get passkeyAuthTitle {
|
||||
return Intl.message(
|
||||
'Passkey authentication',
|
||||
'Passkey verification',
|
||||
name: 'passkeyAuthTitle',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Verify passkey`
|
||||
String get verifyPasskey {
|
||||
return Intl.message(
|
||||
'Verify passkey',
|
||||
name: 'verifyPasskey',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Play album on TV`
|
||||
String get playOnTv {
|
||||
return Intl.message(
|
||||
|
|
|
@ -1188,10 +1188,11 @@
|
|||
"changeLocationOfSelectedItems": "Change location of selected items?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
|
||||
"cleanUncategorized": "Clean Uncategorized",
|
||||
"waitingForBrowserRequest": "Waiting for browser request...",
|
||||
"waitingForVerification": "Waiting for verification...",
|
||||
"launchPasskeyUrlAgain": "Launch passkey URL again",
|
||||
"passkey": "Passkey",
|
||||
"passkeyAuthTitle": "Passkey authentication",
|
||||
"passkeyAuthTitle": "Passkey verification",
|
||||
"verifyPasskey": "Verify passkey",
|
||||
"playOnTv": "Play album on TV",
|
||||
"pair": "Pair",
|
||||
"deviceNotFound": "Device not found",
|
||||
|
|
13
mobile/lib/models/account/two_factor.dart
Normal file
13
mobile/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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -16,6 +16,7 @@ import "package:photos/events/account_configured_event.dart";
|
|||
import 'package:photos/events/two_factor_status_change_event.dart';
|
||||
import 'package:photos/events/user_details_changed_event.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/account/two_factor.dart";
|
||||
import "package:photos/models/api/user/srp.dart";
|
||||
import 'package:photos/models/delete_account.dart';
|
||||
import 'package:photos/models/key_attributes.dart';
|
||||
|
@ -807,7 +808,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, S.of(context).pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
|
@ -815,6 +820,7 @@ class UserService {
|
|||
_config.getHttpEndpoint() + "/users/two-factor/recover",
|
||||
queryParameters: {
|
||||
"sessionID": sessionID,
|
||||
"twoFactorType": twoFactorTypeToString(type),
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
|
@ -823,6 +829,7 @@ class UserService {
|
|||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return TwoFactorRecoveryPage(
|
||||
type,
|
||||
sessionID,
|
||||
response.data["encryptedSecret"],
|
||||
response.data["secretDecryptionNonce"],
|
||||
|
@ -833,6 +840,7 @@ class UserService {
|
|||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response!.statusCode == 404) {
|
||||
showToast(context, S.of(context).sessionExpired);
|
||||
|
@ -854,6 +862,7 @@ class UserService {
|
|||
);
|
||||
}
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
|
@ -868,6 +877,7 @@ class UserService {
|
|||
|
||||
Future<void> removeTwoFactor(
|
||||
BuildContext context,
|
||||
TwoFactorType type,
|
||||
String sessionID,
|
||||
String recoveryKey,
|
||||
String encryptedSecret,
|
||||
|
@ -906,7 +916,8 @@ class UserService {
|
|||
_config.getHttpEndpoint() + "/users/two-factor/remove",
|
||||
data: {
|
||||
"sessionID": sessionID,
|
||||
"secret": secret,
|
||||
"secret": utf8.decode(base64.decode(secret)),
|
||||
"twoFactorType": twoFactorTypeToString(type),
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
|
@ -926,7 +937,8 @@ class UserService {
|
|||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
_logger.severe(e);
|
||||
await dialog.hide();
|
||||
_logger.severe("error during recovery", e);
|
||||
if (e.response != null && e.response!.statusCode == 404) {
|
||||
showToast(context, "Session expired");
|
||||
// ignore: unawaited_futures
|
||||
|
@ -947,7 +959,9 @@ class UserService {
|
|||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
await dialog.hide();
|
||||
_logger.severe('unexpcted error during recovery', e);
|
||||
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
context,
|
||||
|
|
|
@ -3,9 +3,11 @@ import 'dart:convert';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/account/two_factor.dart";
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
@ -99,27 +101,49 @@ class _PasskeyPageState extends State<PasskeyPage> {
|
|||
|
||||
Widget _getBody() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).waitingForBrowserRequest,
|
||||
style: const TextStyle(
|
||||
height: 1.4,
|
||||
fontSize: 16,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).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(S.of(context).launchPasskeyUrlAgain),
|
||||
const SizedBox(height: 16),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: S.of(context).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(
|
||||
S.of(context).recoverAccount,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,8 +16,11 @@ class RequestPasswordVerificationPage extends StatefulWidget {
|
|||
final OnPasswordVerifiedFn onPasswordVerified;
|
||||
final Function? onPasswordError;
|
||||
|
||||
const RequestPasswordVerificationPage(
|
||||
{super.key, required this.onPasswordVerified, this.onPasswordError,});
|
||||
const RequestPasswordVerificationPage({
|
||||
super.key,
|
||||
required this.onPasswordVerified,
|
||||
this.onPasswordError,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RequestPasswordVerificationPage> createState() =>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/account/two_factor.dart";
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/ui/lifecycle_event_handler.dart';
|
||||
import 'package:pinput/pin_put/pin_put.dart';
|
||||
|
@ -124,7 +125,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),
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:ui';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/account/two_factor.dart";
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
|
||||
|
@ -9,8 +10,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, {
|
||||
|
@ -71,6 +74,7 @@ class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
|
|||
? () async {
|
||||
await UserService.instance.removeTwoFactor(
|
||||
context,
|
||||
widget.type,
|
||||
widget.sessionID,
|
||||
_recoveryKey.text,
|
||||
widget.encryptedSecret,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import 'dart:async';
|
||||
import "dart:convert";
|
||||
import "dart:typed_data";
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:logging/logging.dart";
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
|
@ -22,8 +24,10 @@ import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
|
|||
import 'package:photos/ui/components/toggle_switch_widget.dart';
|
||||
import 'package:photos/ui/settings/common_settings.dart';
|
||||
import "package:photos/utils/crypto_util.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
import "package:photos/utils/toast_util.dart";
|
||||
import "package:uuid/uuid.dart";
|
||||
|
||||
class SecuritySectionWidget extends StatefulWidget {
|
||||
const SecuritySectionWidget({Key? key}) : super(key: key);
|
||||
|
@ -37,7 +41,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
|||
|
||||
late StreamSubscription<TwoFactorStatusChangeEvent>
|
||||
_twoFactorStatusChangeEvent;
|
||||
|
||||
final Logger _logger = Logger('SecuritySectionWidget');
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -110,7 +114,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(
|
||||
|
@ -232,6 +236,33 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> onPasskeyClick(BuildContext buildContext) async {
|
||||
try {
|
||||
final isPassKeyResetEnabled =
|
||||
await PasskeyService.instance.isPasskeyRecoveryEnabled();
|
||||
if (!isPassKeyResetEnabled) {
|
||||
final Uint8List recoveryKey =
|
||||
await UserService.instance.getOrCreateRecoveryKey(context);
|
||||
final resetSecret = const Uuid().v4().toString();
|
||||
final bytes = utf8.encode(resetSecret);
|
||||
final base64Str = base64.encode(bytes);
|
||||
final encryptionResult = CryptoUtil.encryptSync(
|
||||
CryptoUtil.base642bin(base64Str),
|
||||
recoveryKey,
|
||||
);
|
||||
await PasskeyService.instance.configurePasskeyRecovery(
|
||||
resetSecret,
|
||||
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, error: e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateEmailMFA(bool isEnabled) async {
|
||||
try {
|
||||
final UserDetails details =
|
||||
|
|
|
@ -41,8 +41,7 @@ class ZoomableImage extends StatefulWidget {
|
|||
State<ZoomableImage> createState() => _ZoomableImageState();
|
||||
}
|
||||
|
||||
class _ZoomableImageState extends State<ZoomableImage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
class _ZoomableImageState extends State<ZoomableImage> {
|
||||
late Logger _logger;
|
||||
late EnteFile _photo;
|
||||
ImageProvider? _imageProvider;
|
||||
|
@ -54,6 +53,7 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
ValueChanged<PhotoViewScaleState>? _scaleStateChangedCallback;
|
||||
bool _isZooming = false;
|
||||
PhotoViewController _photoViewController = PhotoViewController();
|
||||
final _scaleStateController = PhotoViewScaleStateController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -74,6 +74,7 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
@override
|
||||
void dispose() {
|
||||
_photoViewController.dispose();
|
||||
_scaleStateController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -90,8 +91,10 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
content = PhotoViewGestureDetectorScope(
|
||||
axis: Axis.vertical,
|
||||
child: PhotoView(
|
||||
key: ValueKey(_loadedFinalImage),
|
||||
imageProvider: _imageProvider,
|
||||
controller: _photoViewController,
|
||||
scaleStateController: _scaleStateController,
|
||||
scaleStateChangedCallback: _scaleStateChangedCallback,
|
||||
minScale: widget.shouldCover
|
||||
? PhotoViewComputedScale.covered
|
||||
|
@ -272,15 +275,13 @@ class _ZoomableImageState extends State<ZoomableImage>
|
|||
final scale = _photoViewController.scale! /
|
||||
(finalImageInfo.image.width / prevImageInfo.image.width);
|
||||
final currentPosition = _photoViewController.value.position;
|
||||
final positionScaleFactor = 1 / scale;
|
||||
final newPosition = currentPosition.scale(
|
||||
positionScaleFactor,
|
||||
positionScaleFactor,
|
||||
);
|
||||
_photoViewController = PhotoViewController(
|
||||
initialPosition: newPosition,
|
||||
initialPosition: currentPosition,
|
||||
initialScale: scale,
|
||||
);
|
||||
// Fix for auto-zooming when final image is loaded after double tapping
|
||||
//twice.
|
||||
_scaleStateController.scaleState = PhotoViewScaleState.zoomedIn;
|
||||
}
|
||||
final bool canUpdateMetadata = _photo.canEditMetaInfo;
|
||||
// forcefully get finalImageInfo is dimensions are not available in metadata
|
||||
|
|
|
@ -14,6 +14,8 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ente-io/museum/pkg/repo/two_factor_recovery"
|
||||
|
||||
"github.com/ente-io/museum/pkg/controller/cast"
|
||||
|
||||
"github.com/ente-io/museum/pkg/controller/commonbilling"
|
||||
|
@ -137,6 +139,7 @@ func main() {
|
|||
|
||||
twoFactorRepo := &repo.TwoFactorRepository{DB: db, SecretEncryptionKey: secretEncryptionKeyBytes}
|
||||
userAuthRepo := &repo.UserAuthRepository{DB: db}
|
||||
twoFactorRecoveryRepo := &two_factor_recovery.Repository{Db: db, SecretEncryptionKey: secretEncryptionKeyBytes}
|
||||
billingRepo := &repo.BillingRepository{DB: db}
|
||||
userEntityRepo := &userEntityRepo.Repository{DB: db}
|
||||
locationTagRepository := &locationtagRepo.Repository{DB: db}
|
||||
|
@ -304,6 +307,7 @@ func main() {
|
|||
usageRepo,
|
||||
userAuthRepo,
|
||||
twoFactorRepo,
|
||||
twoFactorRecoveryRepo,
|
||||
passkeysRepo,
|
||||
storagBonusRepo,
|
||||
fileRepo,
|
||||
|
@ -429,6 +433,8 @@ func main() {
|
|||
publicAPI.POST("/users/two-factor/remove", userHandler.RemoveTwoFactor)
|
||||
publicAPI.POST("/users/two-factor/passkeys/begin", userHandler.BeginPasskeyAuthenticationCeremony)
|
||||
publicAPI.POST("/users/two-factor/passkeys/finish", userHandler.FinishPasskeyAuthenticationCeremony)
|
||||
privateAPI.GET("/users/two-factor/recovery-status", userHandler.GetTwoFactorRecoveryStatus)
|
||||
privateAPI.POST("/users/two-factor/passkeys/configure-recovery", userHandler.ConfigurePasskeyRecovery)
|
||||
privateAPI.GET("/users/two-factor/status", userHandler.GetTwoFactorStatus)
|
||||
privateAPI.POST("/users/two-factor/setup", userHandler.SetupTwoFactor)
|
||||
privateAPI.POST("/users/two-factor/enable", userHandler.EnableTwoFactor)
|
||||
|
|
|
@ -12,3 +12,19 @@ type Passkey struct {
|
|||
}
|
||||
|
||||
var MaxPasskeys = 10
|
||||
|
||||
type SetPasskeyRecoveryRequest struct {
|
||||
Secret uuid.UUID `json:"secret" binding:"required"`
|
||||
// The UserSecretCipher has SkipSecret encrypted with the user's recoveryKey
|
||||
// If the user sends the correct UserSecretCipher, we can be sure that the user has the recoveryKey,
|
||||
// and we can allow the user to recover their MFA.
|
||||
UserSecretCipher string `json:"userSecretCipher" binding:"required"`
|
||||
UserSecretNonce string `json:"userSecretNonce" binding:"required"`
|
||||
}
|
||||
|
||||
type TwoFactorRecoveryStatus struct {
|
||||
// AllowAdminReset is a boolean that determines if the admin can reset the user's MFA.
|
||||
// If true, in the event that the user loses their MFA device, the admin can reset the user's MFA.
|
||||
AllowAdminReset bool `json:"allowAdminReset" binding:"required"`
|
||||
IsPasskeyRecoveryEnabled bool `json:"isPasskeyRecoveryEnabled" binding:"required"`
|
||||
}
|
||||
|
|
|
@ -192,8 +192,9 @@ type TwoFactorRecoveryResponse struct {
|
|||
|
||||
// TwoFactorRemovalRequest represents the the body of two factor removal request consist of decrypted two factor secret and sessionID
|
||||
type TwoFactorRemovalRequest struct {
|
||||
Secret string `json:"secret"`
|
||||
SessionID string `json:"sessionID"`
|
||||
Secret string `json:"secret"`
|
||||
SessionID string `json:"sessionID"`
|
||||
TwoFactorType string `json:"twoFactorType"`
|
||||
}
|
||||
|
||||
type ProfileData struct {
|
||||
|
|
2
server/migrations/80_two_factor_recovery.down.sql
Normal file
2
server/migrations/80_two_factor_recovery.down.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE IF EXISTS two_factor_recovery;
|
||||
DROP TRIGGER IF EXISTS update_two_factor_recovery_updated_at ON two_factor_recovery;
|
18
server/migrations/80_two_factor_recovery.up.sql
Normal file
18
server/migrations/80_two_factor_recovery.up.sql
Normal file
|
@ -0,0 +1,18 @@
|
|||
CREATE TABLE IF NOT EXISTS two_factor_recovery (
|
||||
user_id bigint NOT NULL PRIMARY KEY,
|
||||
-- if false, the support team team will not be able to reset the MFA for the user
|
||||
enable_admin_mfa_reset boolean NOT NULL DEFAULT true,
|
||||
server_passkey_secret_data bytea,
|
||||
server_passkey_secret_nonce bytea,
|
||||
user_passkey_secret_data text,
|
||||
user_passkey_secret_nonce text,
|
||||
created_at bigint NOT NULL DEFAULT now_utc_micro_seconds(),
|
||||
updated_at bigint NOT NULL DEFAULT now_utc_micro_seconds()
|
||||
);
|
||||
|
||||
CREATE TRIGGER update_two_factor_recovery_updated_at
|
||||
BEFORE UPDATE
|
||||
ON two_factor_recovery
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE
|
||||
trigger_updated_at_microseconds_column();
|
|
@ -244,6 +244,31 @@ func (h *UserHandler) GetTwoFactorStatus(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, gin.H{"status": status})
|
||||
}
|
||||
|
||||
func (h *UserHandler) GetTwoFactorRecoveryStatus(c *gin.Context) {
|
||||
res, err := h.UserController.GetTwoFactorRecoveryStatus(c)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
// ConfigurePasskeyRecovery configures the passkey skip challenge for a user. In case the user does not
|
||||
// have access to passkey, the user can bypass the passkey by providing the recovery key
|
||||
func (h *UserHandler) ConfigurePasskeyRecovery(c *gin.Context) {
|
||||
var request ente.SetPasskeyRecoveryRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
err := h.UserController.ConfigurePasskeyRecovery(c, &request)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
// SetupTwoFactor generates a two factor secret and sends it to user to setup his authenticator app with
|
||||
func (h *UserHandler) SetupTwoFactor(c *gin.Context) {
|
||||
userID := auth.GetUserID(c.Request.Header)
|
||||
|
@ -352,6 +377,16 @@ func (h *UserHandler) FinishPasskeyAuthenticationCeremony(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *UserHandler) IsPasskeyRecoveryEnabled(c *gin.Context) {
|
||||
userID := auth.GetUserID(c.Request.Header)
|
||||
response, err := h.UserController.GetKeyAttributeAndToken(c, userID)
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// DisableTwoFactor disables the two factor authentication for a user
|
||||
func (h *UserHandler) DisableTwoFactor(c *gin.Context) {
|
||||
userID := auth.GetUserID(c.Request.Header)
|
||||
|
@ -367,7 +402,14 @@ func (h *UserHandler) DisableTwoFactor(c *gin.Context) {
|
|||
// recoveryKeyEncryptedTwoFactorSecret for the user to decrypt it and make twoFactor removal api call
|
||||
func (h *UserHandler) RecoverTwoFactor(c *gin.Context) {
|
||||
sessionID := c.Query("sessionID")
|
||||
response, err := h.UserController.RecoverTwoFactor(sessionID)
|
||||
twoFactorType := c.Query("twoFactorType")
|
||||
var response *ente.TwoFactorRecoveryResponse
|
||||
var err error
|
||||
if twoFactorType == "passkey" {
|
||||
response, err = h.UserController.GetPasskeyRecoveryResponse(c, sessionID)
|
||||
} else {
|
||||
response, err = h.UserController.RecoverTwoFactor(sessionID)
|
||||
}
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
|
@ -383,7 +425,13 @@ func (h *UserHandler) RemoveTwoFactor(c *gin.Context) {
|
|||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
}
|
||||
response, err := h.UserController.RemoveTwoFactor(c, request.SessionID, request.Secret)
|
||||
var response *ente.TwoFactorAuthorizationResponse
|
||||
var err error
|
||||
if request.TwoFactorType == "passkey" {
|
||||
response, err = h.UserController.SkipPasskeyVerification(c, &request)
|
||||
} else {
|
||||
response, err = h.UserController.RemoveTOTPTwoFactor(c, request.SessionID, request.Secret)
|
||||
}
|
||||
if err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
|
|
61
server/pkg/controller/user/passkey.go
Normal file
61
server/pkg/controller/user/passkey.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/museum/pkg/utils/auth"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetTwoFactorRecoveryStatus returns a user's passkey reset status
|
||||
func (c *UserController) GetTwoFactorRecoveryStatus(ctx *gin.Context) (*ente.TwoFactorRecoveryStatus, error) {
|
||||
userID := auth.GetUserID(ctx.Request.Header)
|
||||
return c.TwoFactorRecoveryRepo.GetStatus(userID)
|
||||
}
|
||||
|
||||
func (c *UserController) ConfigurePasskeyRecovery(ctx *gin.Context, req *ente.SetPasskeyRecoveryRequest) error {
|
||||
userID := auth.GetUserID(ctx.Request.Header)
|
||||
return c.TwoFactorRecoveryRepo.SetPasskeyRecovery(ctx, userID, req)
|
||||
}
|
||||
|
||||
func (c *UserController) GetPasskeyRecoveryResponse(ctx *gin.Context, passKeySessionID string) (*ente.TwoFactorRecoveryResponse, error) {
|
||||
userID, err := c.PasskeyRepo.GetUserIDWithPasskeyTwoFactorSession(passKeySessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recoveryStatus, err := c.TwoFactorRecoveryRepo.GetStatus(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !recoveryStatus.IsPasskeyRecoveryEnabled {
|
||||
return nil, ente.NewBadRequestWithMessage("Passkey reset is not configured")
|
||||
}
|
||||
|
||||
result, err := c.TwoFactorRecoveryRepo.GetPasskeyRecoveryData(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, ente.NewBadRequestWithMessage("Passkey reset is not configured")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *UserController) SkipPasskeyVerification(context *gin.Context, req *ente.TwoFactorRemovalRequest) (*ente.TwoFactorAuthorizationResponse, error) {
|
||||
userID, err := c.PasskeyRepo.GetUserIDWithPasskeyTwoFactorSession(req.SessionID)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
exists, err := c.TwoFactorRecoveryRepo.ValidatePasskeyRecoverySecret(userID, req.Secret)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
if !exists {
|
||||
return nil, stacktrace.Propagate(ente.ErrPermissionDenied, "")
|
||||
}
|
||||
response, err := c.GetKeyAttributeAndToken(context, userID)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return &response, nil
|
||||
}
|
|
@ -131,47 +131,47 @@ func (c *UserController) DisableTwoFactor(userID int64) error {
|
|||
|
||||
// RecoverTwoFactor handles the two factor recovery request by sending the
|
||||
// recoveryKeyEncryptedTwoFactorSecret for the user to decrypt it and make twoFactor removal api call
|
||||
func (c *UserController) RecoverTwoFactor(sessionID string) (ente.TwoFactorRecoveryResponse, error) {
|
||||
func (c *UserController) RecoverTwoFactor(sessionID string) (*ente.TwoFactorRecoveryResponse, error) {
|
||||
userID, err := c.TwoFactorRepo.GetUserIDWithTwoFactorSession(sessionID)
|
||||
if err != nil {
|
||||
return ente.TwoFactorRecoveryResponse{}, stacktrace.Propagate(err, "")
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
response, err := c.TwoFactorRepo.GetRecoveryKeyEncryptedTwoFactorSecret(userID)
|
||||
if err != nil {
|
||||
return ente.TwoFactorRecoveryResponse{}, stacktrace.Propagate(err, "")
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return response, nil
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// RemoveTwoFactor handles two factor deactivation request if user lost his device
|
||||
// RemoveTOTPTwoFactor handles two factor deactivation request if user lost his device
|
||||
// by authenticating him using his twoFactorsessionToken and twoFactor secret
|
||||
func (c *UserController) RemoveTwoFactor(context *gin.Context, sessionID string, secret string) (ente.TwoFactorAuthorizationResponse, error) {
|
||||
func (c *UserController) RemoveTOTPTwoFactor(context *gin.Context, sessionID string, secret string) (*ente.TwoFactorAuthorizationResponse, error) {
|
||||
userID, err := c.TwoFactorRepo.GetUserIDWithTwoFactorSession(sessionID)
|
||||
if err != nil {
|
||||
return ente.TwoFactorAuthorizationResponse{}, stacktrace.Propagate(err, "")
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
secretHash, err := crypto.GetHash(secret, c.HashingKey)
|
||||
if err != nil {
|
||||
return ente.TwoFactorAuthorizationResponse{}, stacktrace.Propagate(err, "")
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
|
||||
}
|
||||
exists, err := c.TwoFactorRepo.VerifyTwoFactorSecret(userID, secretHash)
|
||||
if err != nil {
|
||||
return ente.TwoFactorAuthorizationResponse{}, stacktrace.Propagate(err, "")
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
|
||||
}
|
||||
if !exists {
|
||||
return ente.TwoFactorAuthorizationResponse{}, stacktrace.Propagate(ente.ErrPermissionDenied, "")
|
||||
return nil, stacktrace.Propagate(ente.ErrPermissionDenied, "")
|
||||
}
|
||||
err = c.TwoFactorRepo.UpdateTwoFactorStatus(userID, false)
|
||||
if err != nil {
|
||||
return ente.TwoFactorAuthorizationResponse{}, stacktrace.Propagate(err, "")
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
response, err := c.GetKeyAttributeAndToken(context, userID)
|
||||
if err != nil {
|
||||
return ente.TwoFactorAuthorizationResponse{}, stacktrace.Propagate(err, "")
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return response, nil
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func (c *UserController) GetKeyAttributeAndToken(context *gin.Context, userID int64) (ente.TwoFactorAuthorizationResponse, error) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package user
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ente-io/museum/pkg/repo/two_factor_recovery"
|
||||
"strings"
|
||||
|
||||
cache2 "github.com/ente-io/museum/ente/cache"
|
||||
|
@ -30,6 +31,7 @@ import (
|
|||
// UserController exposes request handlers for all user related requests
|
||||
type UserController struct {
|
||||
UserRepo *repo.UserRepository
|
||||
TwoFactorRecoveryRepo *two_factor_recovery.Repository
|
||||
UsageRepo *repo.UsageRepository
|
||||
UserAuthRepo *repo.UserAuthRepository
|
||||
TwoFactorRepo *repo.TwoFactorRepository
|
||||
|
@ -99,6 +101,7 @@ func NewUserController(
|
|||
usageRepo *repo.UsageRepository,
|
||||
userAuthRepo *repo.UserAuthRepository,
|
||||
twoFactorRepo *repo.TwoFactorRepository,
|
||||
twoFactorRecoveryRepo *two_factor_recovery.Repository,
|
||||
passkeyRepo *passkey.Repository,
|
||||
storageBonusRepo *storageBonusRepo.Repository,
|
||||
fileRepo *repo.FileRepository,
|
||||
|
@ -121,6 +124,7 @@ func NewUserController(
|
|||
return &UserController{
|
||||
UserRepo: userRepo,
|
||||
UsageRepo: usageRepo,
|
||||
TwoFactorRecoveryRepo: twoFactorRecoveryRepo,
|
||||
UserAuthRepo: userAuthRepo,
|
||||
StorageBonusRepo: storageBonusRepo,
|
||||
TwoFactorRepo: twoFactorRepo,
|
||||
|
|
80
server/pkg/repo/two_factor_recovery/repository.go
Normal file
80
server/pkg/repo/two_factor_recovery/repository.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package two_factor_recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/museum/pkg/utils/crypto"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
Db *sql.DB
|
||||
SecretEncryptionKey []byte
|
||||
}
|
||||
|
||||
// GetStatus returns `ente.TwoFactorRecoveryStatus` for a user
|
||||
func (r *Repository) GetStatus(userID int64) (*ente.TwoFactorRecoveryStatus, error) {
|
||||
var isAdminResetEnabled bool
|
||||
var resetKey []byte
|
||||
row := r.Db.QueryRow(`SELECT enable_admin_mfa_reset, server_passkey_secret_data FROM two_factor_recovery WHERE user_id = $1`, userID)
|
||||
err := row.Scan(&isAdminResetEnabled, &resetKey)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// by default, admin
|
||||
return &ente.TwoFactorRecoveryStatus{
|
||||
AllowAdminReset: true,
|
||||
IsPasskeyRecoveryEnabled: false,
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &ente.TwoFactorRecoveryStatus{AllowAdminReset: isAdminResetEnabled, IsPasskeyRecoveryEnabled: len(resetKey) > 0}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) SetPasskeyRecovery(ctx context.Context, userID int64, req *ente.SetPasskeyRecoveryRequest) error {
|
||||
serveEncPasskey, encErr := crypto.Encrypt(req.Secret.String(), r.SecretEncryptionKey)
|
||||
if encErr != nil {
|
||||
return stacktrace.Propagate(encErr, "failed to encrypt passkey secret")
|
||||
}
|
||||
_, err := r.Db.ExecContext(ctx, `INSERT INTO two_factor_recovery
|
||||
(user_id, server_passkey_secret_data, server_passkey_secret_nonce, user_passkey_secret_data, user_passkey_secret_nonce)
|
||||
VALUES ($1, $2, $3, $4, $5) ON CONFLICT (user_id)
|
||||
DO UPDATE SET server_passkey_secret_data = $2, server_passkey_secret_nonce = $3, user_passkey_secret_data = $4, user_passkey_secret_nonce = $5
|
||||
WHERE two_factor_recovery.user_passkey_secret_data IS NULL AND two_factor_recovery.server_passkey_secret_data IS NULL`,
|
||||
userID, serveEncPasskey.Cipher, serveEncPasskey.Nonce, req.UserSecretCipher, req.UserSecretNonce)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) GetPasskeyRecoveryData(ctx context.Context, userID int64) (*ente.TwoFactorRecoveryResponse, error) {
|
||||
var result ente.TwoFactorRecoveryResponse
|
||||
err := r.Db.QueryRowContext(ctx, "SELECT user_passkey_secret_data, user_passkey_secret_nonce FROM two_factor_recovery WHERE user_id= $1", userID).Scan(&result.EncryptedSecret, &result.SecretDecryptionNonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ValidatePasskeyRecoverySecret checks if the passkey skip secret is valid for a user
|
||||
func (r *Repository) ValidatePasskeyRecoverySecret(userID int64, secret string) (bool, error) {
|
||||
// get server_passkey_secret_data and server_passkey_secret_nonce for given user id
|
||||
var severSecreteData, serverSecretNonce []byte
|
||||
row := r.Db.QueryRow(`SELECT server_passkey_secret_data, server_passkey_secret_nonce FROM two_factor_recovery WHERE user_id = $1`, userID)
|
||||
err := row.Scan(&severSecreteData, &serverSecretNonce)
|
||||
if err != nil {
|
||||
return false, stacktrace.Propagate(err, "")
|
||||
}
|
||||
// decrypt server_passkey_secret_data
|
||||
serverSkipSecretKey, decErr := crypto.Decrypt(severSecreteData, r.SecretEncryptionKey, serverSecretNonce)
|
||||
// serverSkipSecretKey, decErr := crypto.Decrypt(severSecreteData,serverSecretNonce, r.SecretEncryptionKey )
|
||||
if decErr != nil {
|
||||
return false, stacktrace.Propagate(decErr, "failed to decrypt passkey reset key")
|
||||
}
|
||||
if secret != serverSkipSecretKey {
|
||||
logrus.Warn("invalid passkey skip secret")
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
|
@ -32,8 +32,8 @@ yarn dev
|
|||
|
||||
That's it. The web app will automatically hot reload when you make changes.
|
||||
|
||||
If you're new to web development and unsure about how to get started, see
|
||||
[docs/new](docs/new.md).
|
||||
If you're new to web development and unsure about how to get started, or are
|
||||
facing some problems when running the above steps, see [docs/new](docs/new.md).
|
||||
|
||||
## Other apps
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"HERO_SLIDE_1_TITLE": "",
|
||||
"HERO_SLIDE_1": "",
|
||||
"HERO_SLIDE_2_TITLE": "",
|
||||
"HERO_SLIDE_1_TITLE": "추억을 안전하게 백업하세요",
|
||||
"HERO_SLIDE_1": "종단간 암호화가 기본지원입니다",
|
||||
"HERO_SLIDE_2_TITLE": "낙진대피소에 안전하게 보관됩니다",
|
||||
"HERO_SLIDE_2": "",
|
||||
"HERO_SLIDE_3_TITLE": "",
|
||||
"HERO_SLIDE_3": "",
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
"SIGN_UP": "",
|
||||
"NEW_USER": "",
|
||||
"EXISTING_USER": "",
|
||||
"ENTER_NAME": "",
|
||||
"ENTER_NAME": "Ange namn",
|
||||
"PUBLIC_UPLOADER_NAME_MESSAGE": "",
|
||||
"ENTER_EMAIL": "",
|
||||
"ENTER_EMAIL": "Ange e-postadress",
|
||||
"EMAIL_ERROR": "",
|
||||
"REQUIRED": "",
|
||||
"EMAIL_SENT": "",
|
||||
|
@ -24,11 +24,11 @@
|
|||
"EXPIRED_CODE": "",
|
||||
"SENDING": "",
|
||||
"SENT": "",
|
||||
"PASSWORD": "",
|
||||
"PASSWORD": "Lösenord",
|
||||
"LINK_PASSWORD": "",
|
||||
"RETURN_PASSPHRASE_HINT": "",
|
||||
"RETURN_PASSPHRASE_HINT": "Lösenord",
|
||||
"SET_PASSPHRASE": "",
|
||||
"VERIFY_PASSPHRASE": "",
|
||||
"VERIFY_PASSPHRASE": "Logga in",
|
||||
"INCORRECT_PASSPHRASE": "",
|
||||
"ENTER_ENC_PASSPHRASE": "",
|
||||
"PASSPHRASE_DISCLAIMER": "",
|
||||
|
@ -36,19 +36,19 @@
|
|||
"WELCOME_TO_ENTE_SUBHEADING": "",
|
||||
"WHERE_YOUR_BEST_PHOTOS_LIVE": "",
|
||||
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
|
||||
"PASSPHRASE_HINT": "",
|
||||
"CONFIRM_PASSPHRASE": "",
|
||||
"PASSPHRASE_HINT": "Lösenord",
|
||||
"CONFIRM_PASSPHRASE": "Bekräfta lösenord",
|
||||
"REFERRAL_CODE_HINT": "",
|
||||
"REFERRAL_INFO": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "",
|
||||
"PASSPHRASE_MATCH_ERROR": "Lösenorden matchar inte",
|
||||
"CONSOLE_WARNING_STOP": "",
|
||||
"CONSOLE_WARNING_DESC": "",
|
||||
"CREATE_COLLECTION": "",
|
||||
"ENTER_ALBUM_NAME": "",
|
||||
"CLOSE_OPTION": "",
|
||||
"ENTER_FILE_NAME": "",
|
||||
"CLOSE": "",
|
||||
"NO": "",
|
||||
"ENTER_FILE_NAME": "Filnamn",
|
||||
"CLOSE": "Stäng",
|
||||
"NO": "Nej",
|
||||
"NOTHING_HERE": "",
|
||||
"UPLOAD": "",
|
||||
"IMPORT": "",
|
||||
|
@ -108,7 +108,7 @@
|
|||
"SESSION_EXPIRED_MESSAGE": "",
|
||||
"SESSION_EXPIRED": "",
|
||||
"PASSWORD_GENERATION_FAILED": "",
|
||||
"CHANGE_PASSWORD": "",
|
||||
"CHANGE_PASSWORD": "Ändra lösenord",
|
||||
"GO_BACK": "",
|
||||
"RECOVERY_KEY": "",
|
||||
"SAVE_LATER": "",
|
||||
|
@ -116,7 +116,7 @@
|
|||
"RECOVERY_KEY_DESCRIPTION": "",
|
||||
"RECOVER_KEY_GENERATION_FAILED": "",
|
||||
"KEY_NOT_STORED_DISCLAIMER": "",
|
||||
"FORGOT_PASSWORD": "",
|
||||
"FORGOT_PASSWORD": "Glömt lösenord",
|
||||
"RECOVER_ACCOUNT": "",
|
||||
"RECOVERY_KEY_HINT": "",
|
||||
"RECOVER": "",
|
||||
|
@ -369,11 +369,11 @@
|
|||
"shared_with_people_zero": "",
|
||||
"shared_with_people_one": "",
|
||||
"shared_with_people_other": "",
|
||||
"participants_zero": "",
|
||||
"participants_one": "",
|
||||
"participants_zero": "Inga deltagare",
|
||||
"participants_one": "1 deltagare",
|
||||
"participants_other": "",
|
||||
"ADD_VIEWERS": "",
|
||||
"PARTICIPANTS": "",
|
||||
"PARTICIPANTS": "Deltagare",
|
||||
"CHANGE_PERMISSIONS_TO_VIEWER": "",
|
||||
"CHANGE_PERMISSIONS_TO_COLLABORATOR": "",
|
||||
"CONVERT_TO_VIEWER": "",
|
||||
|
@ -416,14 +416,14 @@
|
|||
"PASSWORD_LOCK": "",
|
||||
"LOCK": "",
|
||||
"DOWNLOAD_UPLOAD_LOGS": "",
|
||||
"UPLOAD_FILES": "",
|
||||
"UPLOAD_DIRS": "",
|
||||
"UPLOAD_FILES": "Fil",
|
||||
"UPLOAD_DIRS": "Mapp",
|
||||
"UPLOAD_GOOGLE_TAKEOUT": "",
|
||||
"DEDUPLICATE_FILES": "",
|
||||
"AUTHENTICATOR_SECTION": "",
|
||||
"NO_DUPLICATES_FOUND": "",
|
||||
"CLUB_BY_CAPTURE_TIME": "",
|
||||
"FILES": "",
|
||||
"FILES": "Filer",
|
||||
"EACH": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "",
|
||||
|
@ -432,8 +432,8 @@
|
|||
"STOP_DOWNLOADS_HEADER": "",
|
||||
"YES_STOP_DOWNLOADS": "",
|
||||
"STOP_ALL_DOWNLOADS_MESSAGE": "",
|
||||
"albums_one": "",
|
||||
"albums_other": "",
|
||||
"albums_one": "1 album",
|
||||
"albums_other": "{{count, number}} album",
|
||||
"ALL_ALBUMS": "",
|
||||
"ALBUMS": "",
|
||||
"ALL_HIDDEN_ALBUMS": "",
|
||||
|
@ -464,14 +464,14 @@
|
|||
"STOP_WATCHING_FOLDER": "",
|
||||
"STOP_WATCHING_DIALOG_MESSAGE": "",
|
||||
"YES_STOP": "",
|
||||
"MONTH_SHORT": "",
|
||||
"MONTH_SHORT": "mån",
|
||||
"YEAR": "",
|
||||
"FAMILY_PLAN": "",
|
||||
"DOWNLOAD_LOGS": "",
|
||||
"DOWNLOAD_LOGS_MESSAGE": "",
|
||||
"CHANGE_FOLDER": "",
|
||||
"TWO_MONTHS_FREE": "",
|
||||
"GB": "",
|
||||
"GB": "GB",
|
||||
"POPULAR": "",
|
||||
"FREE_PLAN_OPTION_LABEL": "",
|
||||
"FREE_PLAN_DESCRIPTION": "",
|
||||
|
@ -492,7 +492,7 @@
|
|||
"IGNORE_THIS_VERSION": "",
|
||||
"TODAY": "",
|
||||
"YESTERDAY": "",
|
||||
"NAME_PLACEHOLDER": "",
|
||||
"NAME_PLACEHOLDER": "Namn...",
|
||||
"ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "",
|
||||
"ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "",
|
||||
"CHOSE_THEME": "",
|
||||
|
@ -514,7 +514,7 @@
|
|||
"PASSPHRASE_STRENGTH_MODERATE": "",
|
||||
"PASSPHRASE_STRENGTH_STRONG": "",
|
||||
"PREFERENCES": "",
|
||||
"LANGUAGE": "",
|
||||
"LANGUAGE": "Språk",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST": "",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "",
|
||||
"SUBSCRIPTION_VERIFICATION_ERROR": "",
|
||||
|
@ -532,8 +532,8 @@
|
|||
"MONTH": "",
|
||||
"YEAR": ""
|
||||
},
|
||||
"COPY_LINK": "",
|
||||
"DONE": "",
|
||||
"COPY_LINK": "Kopiera länk",
|
||||
"DONE": "Klar",
|
||||
"LINK_SHARE_TITLE": "",
|
||||
"REMOVE_LINK": "",
|
||||
"CREATE_PUBLIC_SHARING": "",
|
||||
|
@ -579,7 +579,7 @@
|
|||
"HIDE": "",
|
||||
"UNHIDE": "",
|
||||
"UNHIDE_TO_COLLECTION": "",
|
||||
"SORT_BY": "",
|
||||
"SORT_BY": "Sortera efter",
|
||||
"NEWEST_FIRST": "",
|
||||
"OLDEST_FIRST": "",
|
||||
"CONVERSION_FAILED_NOTIFICATION_MESSAGE": "",
|
||||
|
@ -595,7 +595,7 @@
|
|||
"CHRISTMAS_EVE": "",
|
||||
"NEW_YEAR": "",
|
||||
"NEW_YEAR_EVE": "",
|
||||
"IMAGE": "",
|
||||
"IMAGE": "Bild",
|
||||
"VIDEO": "",
|
||||
"LIVE_PHOTO": "",
|
||||
"CONVERT": "",
|
||||
|
@ -616,10 +616,10 @@
|
|||
"SAVE_A_COPY_TO_ENTE": "",
|
||||
"RESTORE_ORIGINAL": "",
|
||||
"TRANSFORM": "",
|
||||
"COLORS": "",
|
||||
"COLORS": "Färger",
|
||||
"FLIP": "",
|
||||
"ROTATION": "",
|
||||
"RESET": "",
|
||||
"RESET": "Återställ",
|
||||
"PHOTO_EDITOR": "",
|
||||
"FASTER_UPLOAD": "",
|
||||
"FASTER_UPLOAD_DESCRIPTION": "",
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"SIGN_UP": "注册",
|
||||
"NEW_USER": "刚来到 ente",
|
||||
"EXISTING_USER": "现有用户",
|
||||
"ENTER_NAME": "现有用户",
|
||||
"ENTER_NAME": "输入名字",
|
||||
"PUBLIC_UPLOADER_NAME_MESSAGE": "请添加一个名字,以便您的朋友知晓该感谢谁拍摄了这些精美的照片!",
|
||||
"ENTER_EMAIL": "请输入电子邮件地址",
|
||||
"EMAIL_ERROR": "请输入有效的电子邮件",
|
||||
|
@ -85,9 +85,9 @@
|
|||
"ZOOM_IN_OUT": "放大/缩小",
|
||||
"PREVIOUS": "上一个 (←)",
|
||||
"NEXT": "下一个 (→)",
|
||||
"TITLE_PHOTOS": "ente 照片",
|
||||
"TITLE_ALBUMS": "ente 照片",
|
||||
"TITLE_AUTH": "ente 验证器",
|
||||
"TITLE_PHOTOS": "Ente 照片",
|
||||
"TITLE_ALBUMS": "Ente 照片",
|
||||
"TITLE_AUTH": "Ente 验证器",
|
||||
"UPLOAD_FIRST_PHOTO": "上传您的第一张照片",
|
||||
"IMPORT_YOUR_FOLDERS": "导入您的文件夹",
|
||||
"UPLOAD_DROPZONE_MESSAGE": "拖放以备份您的文件",
|
||||
|
|
|
@ -1,40 +1,45 @@
|
|||
# Deploying the web apps
|
||||
# Deploying
|
||||
|
||||
## tl;dr;
|
||||
The various web apps and static sites in this repository are deployed on
|
||||
Cloudflare Pages.
|
||||
|
||||
```sh
|
||||
yarn deploy:photos
|
||||
```
|
||||
* Production deployments are triggered by pushing to the `deploy/*` branches.
|
||||
|
||||
## Details
|
||||
* [help.ente.io](https://help.ente.io) gets deployed whenever a PR that changes
|
||||
anything inside `docs/` gets merged to `main`.
|
||||
|
||||
The various web apps (Ente Photos, Ente Auth) are deployed on Cloudflare Pages.
|
||||
* Every night, all the web apps get automatically deployed to a nightly preview
|
||||
URLs using the current code in main.
|
||||
|
||||
The deployment is done using the GitHub app provided by Cloudflare Pages. The
|
||||
Cloudflare integration watches for pushes to all branches named "deploy/*". In
|
||||
all cases, it runs the same script, `scripts/deploy.sh`, using the
|
||||
`CF_PAGES_BRANCH` environment variable to decide what exactly to build ([CF
|
||||
docs](https://developers.cloudflare.com/pages/how-to/build-commands-branches/)).
|
||||
Use the various `yarn deploy:*` commands to help with production deployments.
|
||||
For example, `yarn deploy:photos` will open a PR to merge the current `main`
|
||||
onto `deploy/photos`, which'll trigger the deployment workflow, which'll build
|
||||
and publish to [web.ente.io](https://web.ente.io).
|
||||
|
||||
For each of these branches, we have configured CNAME aliases (Cloudflare calls
|
||||
them Custom Domains) to give a stable URL to the deployments.
|
||||
> When merging these deployment PRs, remember to use rebase and merge so that
|
||||
> their HEAD is a fast forward of `main` instead of diverging from it because of
|
||||
> the merge commit.
|
||||
|
||||
- `deploy/photos` → _web.ente.io_
|
||||
- `deploy/auth` → _auth.ente.io_
|
||||
- `deploy/accounts` → _accounts.ente.io_
|
||||
- `deploy/cast` → _cast.ente.io_
|
||||
## Deployments
|
||||
Here is a list of all the deployments, whether or not they are production
|
||||
deployments, and the action that triggers them:
|
||||
|
||||
Thus to trigger a, say, production deployment of the photos app, we can open and
|
||||
merge a PR into the `deploy/photos` branch. Cloudflare will then build and
|
||||
deploy the code to _web.ente.io_.
|
||||
| URL | Type |Deployment action |
|
||||
|-----|------|------------------|
|
||||
| [web.ente.io](https://web.ente.io) | Production | Push to `deploy/photos` |
|
||||
| [photos.ente.io](https://photos.ente.io) | Production | Alias of [web.ente.io](https://web.ente.io) |
|
||||
| [auth.ente.io](https://auth.ente.io) | Production | Push to `deploy/auth` |
|
||||
| [accounts.ente.io](https://accounts.ente.io) | Production | Push to `deploy/accounts` |
|
||||
| [cast.ente.io](https://cast.ente.io) | Production | Push to `deploy/cast` |
|
||||
| [help.ente.io](https://help.ente.io) | Production | Push to `main` + changes in `docs/` |
|
||||
| [accounts.ente.sh](https://accounts.ente.sh) | Preview | Nightly deploy of `main` |
|
||||
| [auth.ente.sh](https://auth.ente.sh) | Preview | Nightly deploy of `main` |
|
||||
| [cast.ente.sh](https://cast.ente.sh) | Preview | Nightly deploy of `main` |
|
||||
| [photos.ente.sh](https://photos.ente.sh) | Preview | Nightly deploy of `main` |
|
||||
|
||||
The command `yarn deploy:photos` just does that - it'll open a new PR to fast
|
||||
forward the current main onto `deploy/photos`. There are similar `yarn deploy:*`
|
||||
commands for the other apps.
|
||||
### Other subdomains
|
||||
|
||||
## Other subdomains
|
||||
|
||||
Apart from this, there are also some subdomains:
|
||||
Apart from this, there are also some other deployments:
|
||||
|
||||
- `albums.ente.io` is a CNAME alias to the production deployment
|
||||
(`web.ente.io`). However, when the code detects that it is being served from
|
||||
|
@ -44,22 +49,67 @@ Apart from this, there are also some subdomains:
|
|||
- `payments.ente.io` and `family.ente.io` are currently in a separate
|
||||
repositories (Enhancement: bring them in here).
|
||||
|
||||
## NODE_VERSION
|
||||
---
|
||||
|
||||
In Cloudflare Pages setting the `NODE_VERSION` environment variables is defined.
|
||||
## Details
|
||||
|
||||
This determines which version of Node is used when we do `yarn build:foo`.
|
||||
Currently this is set to `20.11.1`. The major version here should match that of
|
||||
`@types/node` in our dev dependencies.
|
||||
The rest of the document describes details about how things were setup. You
|
||||
likely don't need to know them to be able to deploy.
|
||||
|
||||
It is a good idea to also use the same major version of node on your machine.
|
||||
For example, for macOS you can install the the latest from the v20 series using
|
||||
`brew install node@20`.
|
||||
## First time preparation
|
||||
|
||||
## Adding a new app
|
||||
Create a new Pages project in Cloudflare, setting it up to use [Direct
|
||||
Upload](https://developers.cloudflare.com/pages/get-started/direct-upload/).
|
||||
|
||||
1. Add a mapping in `scripts/deploy.sh`.
|
||||
> [!NOTE]
|
||||
>
|
||||
> Direct upload doesn't work for existing projects tied to your repository using
|
||||
> the [Git
|
||||
> integration](https://developers.cloudflare.com/pages/get-started/git-integration/).
|
||||
>
|
||||
> If you want to keep the pages.dev domain from an existing project, you should
|
||||
> be able to delete your existing project and recreate it (assuming no one
|
||||
> claims the domain in the middle). I've not seen this documented anywhere, but
|
||||
> it worked when I tried, and it seems to have worked for [other people
|
||||
> too](https://community.cloudflare.com/t/linking-git-repo-to-existing-cf-pages-project/530888).
|
||||
|
||||
|
||||
There are two ways to create a new project, using Wrangler
|
||||
[[1](https://github.com/cloudflare/pages-action/issues/51)] or using the
|
||||
Cloudflare dashboard
|
||||
[[2](https://github.com/cloudflare/pages-action/issues/115)]. Since this is one
|
||||
time thing, the second option might be easier.
|
||||
|
||||
The remaining steps are documented in [Cloudflare's guide for using Direct
|
||||
Upload with
|
||||
CI](https://developers.cloudflare.com/pages/how-to/use-direct-upload-with-continuous-integration/).
|
||||
As a checklist,
|
||||
|
||||
- Generate `CLOUDFLARE_API_TOKEN`
|
||||
- Add `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` to the GitHub secrets
|
||||
- Add your workflow. e.g. see `docs-deploy.yml`.
|
||||
|
||||
This is the basic setup, and should already work.
|
||||
|
||||
## Deploying multiple sites
|
||||
|
||||
However, we wish to deploy multiple sites from this same repository, so the
|
||||
standard Cloudflare conception of a single "production" branch doesn't work for
|
||||
us.
|
||||
|
||||
Instead, we tie each deployment to a branch name. Note that we don't have to
|
||||
actually create the branch or push to it, this branch name is just used as the
|
||||
the `branch` parameter that gets passed to `cloudflare/pages-action`.
|
||||
|
||||
Since our root pages project is `ente.pages.dev`, so a branch named `foo` would
|
||||
be available at `foo.ente.pages.dev`.
|
||||
|
||||
Finally, we create CNAME aliases using a [Custom Domain in
|
||||
Cloudflare](https://developers.cloudflare.com/pages/how-to/custom-branch-aliases/)
|
||||
to point to these deployments from our user facing DNS names.
|
||||
|
||||
As a concrete example, the GitHub workflow that deploys `docs/` passes "help" as
|
||||
the branch name. The resulting deployment is available at "help.ente.pages.dev".
|
||||
Finally, we add a custom domain to point to it from
|
||||
[help.ente.io](https://help.ente.io).
|
||||
|
||||
2. Add a [Custom Domain in
|
||||
Cloudflare](https://developers.cloudflare.com/pages/how-to/custom-branch-aliases/)
|
||||
pointing to this branch's deployment.
|
||||
|
|
|
@ -13,3 +13,14 @@ development, here is a recommended workflow:
|
|||
`yarn` comes with it.
|
||||
|
||||
That's it. Enjoy coding!
|
||||
|
||||
### Yarn
|
||||
|
||||
Note that we use Yarn classic
|
||||
|
||||
```
|
||||
$ yarn --version
|
||||
1.22.21
|
||||
```
|
||||
|
||||
You should be seeing a 1.xx.xx version, otherwise your `yarn install` will fail.
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
# This script is run by the Cloudflare Pages integration when deploying the apps
|
||||
# in this repository. The app to build is decided based on the value of the
|
||||
# CF_PAGES_BRANCH environment variable.
|
||||
#
|
||||
# The Cloudflare Pages build configuration is set to use `out/` as the build
|
||||
# output directory, so once we're done building we copy the app specific output
|
||||
# to `out/` (symlinking didn't work).
|
||||
#
|
||||
# Ref: https://developers.cloudflare.com/pages/how-to/build-commands-branches/
|
||||
#
|
||||
# To test this script locally, run
|
||||
#
|
||||
# CF_PAGES_BRANCH=deploy/foo ./scripts/deploy.sh
|
||||
#
|
||||
|
||||
set -o errexit
|
||||
set -o xtrace
|
||||
|
||||
if test "$(basename $(pwd))" != "web"
|
||||
then
|
||||
echo "ERROR: This script should be run from the web directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf out
|
||||
|
||||
case "$CF_PAGES_BRANCH" in
|
||||
deploy/accounts)
|
||||
yarn build:accounts
|
||||
cp -R apps/accounts/out .
|
||||
;;
|
||||
deploy/auth)
|
||||
yarn build:auth
|
||||
cp -R apps/auth/out .
|
||||
;;
|
||||
deploy/cast)
|
||||
yarn build:cast
|
||||
cp -R apps/cast/out .
|
||||
;;
|
||||
deploy/photos)
|
||||
yarn build:photos
|
||||
cp -R apps/photos/out .
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: We don't know how to build and deploy a branch named $CF_PAGES_BRANCH."
|
||||
echo " Maybe you forgot to add a new case in web/scripts/deploy.sh"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
Loading…
Add table
Reference in a new issue