From 20c284d42b1f0d5b714090c83c055e2032379cc3 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 15 Feb 2023 23:08:39 -0500 Subject: [PATCH 1/5] Combine prod and dev docker setups using build-arg - Fixes #2603 --- .drone.yml | 8 +- docker/Dockerfile | 31 ++++ docker/{prod => }/Dockerfile.arm | 0 docker/dev/Dockerfile | 44 ------ docker/{dev => }/docker-compose.yml | 6 +- docker/docker_db_backup.sh | 2 - docker/{dev => }/docker_update.sh | 0 docker/federation/start-local-instances.bash | 2 +- docker/{dev => }/lemmy.hjson | 2 +- docker/{dev => }/nginx.conf | 0 docker/{dev => }/otel.yml | 0 docker/prod/Dockerfile | 26 ---- docker/prod/docker-compose.yml | 86 ----------- docker/prod/lemmy.hjson | 43 ------ docker/prod/nginx.conf | 147 ------------------- docker/{dev => }/test_deploy.sh | 2 +- scripts/release.sh | 10 +- scripts/restore_db.sh | 2 +- 18 files changed, 48 insertions(+), 363 deletions(-) create mode 100644 docker/Dockerfile rename docker/{prod => }/Dockerfile.arm (100%) delete mode 100644 docker/dev/Dockerfile rename docker/{dev => }/docker-compose.yml (97%) rename docker/{dev => }/docker_update.sh (100%) rename docker/{dev => }/lemmy.hjson (92%) rename docker/{dev => }/nginx.conf (100%) rename docker/{dev => }/otel.yml (100%) delete mode 100644 docker/prod/Dockerfile delete mode 100644 docker/prod/docker-compose.yml delete mode 100644 docker/prod/lemmy.hjson delete mode 100644 docker/prod/nginx.conf rename docker/{dev => }/test_deploy.sh (81%) diff --git a/.drone.yml b/.drone.yml index 9b8588e94e..4c1eda4cd8 100644 --- a/.drone.yml +++ b/.drone.yml @@ -120,7 +120,8 @@ steps: - name: nightly build image: plugins/docker settings: - dockerfile: docker/prod/Dockerfile + dockerfile: docker/Dockerfile + build_args: RUST_RELEASE_MODE=release username: from_secret: docker_username password: @@ -136,7 +137,8 @@ steps: - name: publish release docker image image: plugins/docker settings: - dockerfile: docker/prod/Dockerfile + dockerfile: docker/Dockerfile + build_args: RUST_RELEASE_MODE=release username: from_secret: docker_username password: @@ -285,7 +287,7 @@ steps: - name: publish release docker image image: plugins/docker settings: - dockerfile: docker/prod/Dockerfile.arm + dockerfile: docker/Dockerfile.arm username: from_secret: docker_username password: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000000..d3b9a85431 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,31 @@ +FROM clux/muslrust:1.67.0 as builder +WORKDIR /app +ARG CARGO_BUILD_TARGET=x86_64-unknown-linux-musl + +# This can be set to release using --build-arg +ARG RUST_RELEASE_MODE="debug" + +COPY . . + +# Build the project +RUN echo "pub const VERSION: &str = \"$(git describe --tag)\";" > "crates/utils/src/version.rs" + +RUN --mount=type=cache,target=/app/target \ + if [ "$RUST_RELEASE_MODE" = "release" ] ; then \ + cargo build --target ${CARGO_BUILD_TARGET} --release; \ + else \ + cargo build --target ${CARGO_BUILD_TARGET}; \ + fi \ + && cp ./target/$CARGO_BUILD_TARGET/$RUST_RELEASE_MODE/lemmy_server /app/lemmy_server + +# The alpine runner +FROM alpine:3 as lemmy + +# Install libpq for postgres +RUN apk add libpq + +# Copy resources +COPY --from=builder /app/lemmy_server /app/lemmy + +EXPOSE 8536 +CMD ["/app/lemmy"] diff --git a/docker/prod/Dockerfile.arm b/docker/Dockerfile.arm similarity index 100% rename from docker/prod/Dockerfile.arm rename to docker/Dockerfile.arm diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile deleted file mode 100644 index d52a52d3a7..0000000000 --- a/docker/dev/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -ARG RUST_BUILDER_IMAGE=clux/muslrust:1.67.0 - -FROM $RUST_BUILDER_IMAGE as chef -USER root -RUN cargo install cargo-chef -WORKDIR /app - -# Cargo chef plan -FROM chef as planner -ENV RUSTFLAGS="--cfg tokio_unstable" - -# Copy dirs -COPY . . - -RUN cargo chef prepare --recipe-path recipe.json - -FROM chef as builder -ARG CARGO_BUILD_TARGET=x86_64-unknown-linux-musl -ARG RUSTRELEASEDIR="debug" -ENV RUSTFLAGS="--cfg tokio_unstable" - -COPY --from=planner /app/recipe.json ./recipe.json -RUN cargo chef cook --recipe-path recipe.json --target ${CARGO_BUILD_TARGET} - -# Copy the rest of the dirs -COPY . . - -# Build the project -RUN echo "pub const VERSION: &str = \"$(git describe --tag)\";" > "crates/utils/src/version.rs" -RUN cargo build --target ${CARGO_BUILD_TARGET} - -RUN cp ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/lemmy_server /app/lemmy_server - -# The alpine runner -FROM alpine:3 as lemmy - -# Install libpq for postgres -RUN apk add libpq - -# Copy resources -COPY --from=builder /app/lemmy_server /app/lemmy - -EXPOSE 8536 -CMD ["/app/lemmy"] diff --git a/docker/dev/docker-compose.yml b/docker/docker-compose.yml similarity index 97% rename from docker/dev/docker-compose.yml rename to docker/docker-compose.yml index ba2785fa9d..f923c97d49 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/docker-compose.yml @@ -30,8 +30,10 @@ services: # use this to build your local lemmy server image for development # run docker compose up --build build: - context: ../.. - dockerfile: docker/dev/Dockerfile + context: ../ + dockerfile: docker/Dockerfile + # args: + # RUST_RELEASE_MODE: release # this hostname is used in nginx reverse proxy and also for lemmy ui to connect to the backend, do not change hostname: lemmy networks: diff --git a/docker/docker_db_backup.sh b/docker/docker_db_backup.sh index e9473a2902..0bfa68029f 100755 --- a/docker/docker_db_backup.sh +++ b/docker/docker_db_backup.sh @@ -1,4 +1,2 @@ #!/bin/bash -pushd dev docker-compose exec postgres pg_dumpall -c -U lemmy > dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql -popd diff --git a/docker/dev/docker_update.sh b/docker/docker_update.sh similarity index 100% rename from docker/dev/docker_update.sh rename to docker/docker_update.sh diff --git a/docker/federation/start-local-instances.bash b/docker/federation/start-local-instances.bash index 7527108d62..b2350d3a89 100755 --- a/docker/federation/start-local-instances.bash +++ b/docker/federation/start-local-instances.bash @@ -3,7 +3,7 @@ set -e sudo docker-compose down -sudo docker build ../../ --file ../dev/Dockerfile -t lemmy-federation:latest +sudo docker build ../../ --file ../Dockerfile -t lemmy-federation:latest for Item in alpha beta gamma delta epsilon ; do sudo mkdir -p volumes/pictrs_$Item diff --git a/docker/dev/lemmy.hjson b/docker/lemmy.hjson similarity index 92% rename from docker/dev/lemmy.hjson rename to docker/lemmy.hjson index cac95ffdc1..f7ffcc1e3d 100644 --- a/docker/dev/lemmy.hjson +++ b/docker/lemmy.hjson @@ -23,5 +23,5 @@ # api_key: "API_KEY" } - opentelemetry_url: "http://otel:4137" + #opentelemetry_url: "http://otel:4137" } diff --git a/docker/dev/nginx.conf b/docker/nginx.conf similarity index 100% rename from docker/dev/nginx.conf rename to docker/nginx.conf diff --git a/docker/dev/otel.yml b/docker/otel.yml similarity index 100% rename from docker/dev/otel.yml rename to docker/otel.yml diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile deleted file mode 100644 index 2ea67f8199..0000000000 --- a/docker/prod/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# Build the project -FROM clux/muslrust:1.67.0 as builder - -ARG CARGO_BUILD_TARGET=x86_64-unknown-linux-musl -ARG RUSTRELEASEDIR="release" - -WORKDIR /app - -COPY ./ ./ - -RUN echo "pub const VERSION: &str = \"$(git describe --tag)\";" > "crates/utils/src/version.rs" -RUN cargo build --release - -RUN cp ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/lemmy_server /app/lemmy_server - -# The alpine runner -FROM alpine:3 as lemmy - -# Install libpq for postgres -RUN apk update && apk add libpq - -# Copy resources -COPY --from=builder /app/lemmy_server /app/lemmy - -EXPOSE 8536 -CMD ["/app/lemmy"] diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml deleted file mode 100644 index 5e7b7568df..0000000000 --- a/docker/prod/docker-compose.yml +++ /dev/null @@ -1,86 +0,0 @@ -version: "3.3" - -networks: - # communication to web and clients - lemmyexternalproxy: - # communication between lemmy services - lemmyinternal: - driver: bridge - internal: true - -services: - proxy: - image: nginx:1-alpine - networks: - - lemmyinternal - - lemmyexternalproxy - ports: - # only ports facing any connection from outside - - 80:80 - - 443:443 - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - # setup your certbot and letsencrypt config - - ./certbot:/var/www/certbot - - ./letsencrypt:/etc/letsencrypt/live - restart: always - depends_on: - - pictrs - - lemmy-ui - - lemmy: - image: dessalines/lemmy:0.17.1 - hostname: lemmy - networks: - - lemmyinternal - restart: always - environment: - - RUST_LOG="warn,lemmy_server=info,lemmy_api=info,lemmy_api_common=info,lemmy_api_crud=info,lemmy_apub=info,lemmy_db_schema=info,lemmy_db_views=info,lemmy_db_views_actor=info,lemmy_db_views_moderator=info,lemmy_routes=info,lemmy_utils=info,lemmy_websocket=info" - volumes: - - ./lemmy.hjson:/config/config.hjson - depends_on: - - postgres - - pictrs - - lemmy-ui: - image: dessalines/lemmy-ui:0.17.1 - networks: - - lemmyinternal - environment: - # this needs to match the hostname defined in the lemmy service - - LEMMY_INTERNAL_HOST=lemmy:8536 - # set the outside hostname here - - LEMMY_EXTERNAL_HOST=localhost:1236 - - LEMMY_HTTPS=true - depends_on: - - lemmy - restart: always - - pictrs: - image: asonix/pictrs:0.3.1 - # this needs to match the pictrs url in lemmy.hjson - hostname: pictrs - # we can set options to pictrs like this, here we set max. image size and forced format for conversion - # entrypoint: /sbin/tini -- /usr/local/bin/pict-rs -p /mnt -m 4 --image-format webp - networks: - - lemmyinternal - environment: - - PICTRS__API_KEY=API_KEY - user: 991:991 - volumes: - - ./volumes/pictrs:/mnt - restart: always - - postgres: - image: postgres:15-alpine - # this needs to match the database host in lemmy.hson - hostname: postgres - networks: - - lemmyinternal - environment: - - POSTGRES_USER=lemmy - - POSTGRES_PASSWORD=password - - POSTGRES_DB=lemmy - volumes: - - ./volumes/postgres:/var/lib/postgresql/data - restart: always diff --git a/docker/prod/lemmy.hjson b/docker/prod/lemmy.hjson deleted file mode 100644 index edbb25356a..0000000000 --- a/docker/prod/lemmy.hjson +++ /dev/null @@ -1,43 +0,0 @@ -{ - # for more info about the config, check out the documentation - # https://join-lemmy.org/docs/en/administration/configuration.html - # only few config options are covered in this example config - - setup: { - # username for the admin user - admin_username: "lemmy" - # password for the admin user - admin_password: "lemmylemmy" - # name of the site (can be changed later) - site_name: "mylemmyinstance" - } - - # the domain name of your instance (eg "lemmy.ml") - hostname: "mydomain.ml" - # address where lemmy should listen for incoming requests - bind: "0.0.0.0" - # port where lemmy should listen for incoming requests - port: 8536 - # Whether the site is available over TLS. Needs to be true for federation to work. - tls_enabled: true - - # pictrs host - pictrs_url: "http://pictrs:8080" - - # settings related to the postgresql database - database: { - # name of the postgres database for lemmy - database: "lemmy" - # username to connect to postgres - user: "lemmy" - # password to connect to postgres - password: "password" - # host where postgres is running - host: "postgres" - # port where postgres can be accessed - port: 5432 - # maximum number of active sql connections - pool_size: 5 - } -} - diff --git a/docker/prod/nginx.conf b/docker/prod/nginx.conf deleted file mode 100644 index 38d64d295e..0000000000 --- a/docker/prod/nginx.conf +++ /dev/null @@ -1,147 +0,0 @@ -# nginx example config -# replace {{yourdomain}} and review the certbot/letsencrypt config - -limit_req_zone $binary_remote_addr zone={{yourdomain}}_ratelimit:10m rate=1r/s; - -upstream lemmy { - # this needs to map to the lemmy (server) docker service hostname - server "lemmy:8536"; -} -upstream lemmy-ui { - # this needs to map to the lemmy-ui docker service hostname - server "lemmy-ui:1234"; -} - -server { - # allow letsencrypt challenge - # redirect everything else to 443 - listen 80; - listen [::]:80; - server_name {{yourdomain}}; - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - location / { - return 301 https://$host$request_uri; - } -} - -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name {{yourdomain}}; - - ssl_certificate /etc/letsencrypt/live/{{yourdomain}}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{{yourdomain}}/privkey.pem; - - # Various TLS hardening settings - # https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html - ssl_protocols TLSv1.2 TLSv1.3; - ssl_prefer_server_ciphers on; - ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; - ssl_session_timeout 10m; - ssl_session_cache shared:SSL:10m; - ssl_session_tickets on; - ssl_stapling on; - ssl_stapling_verify on; - - # Hide nginx version - server_tokens off; - - # Enable compression for JS/CSS/HTML bundle, for improved client load times. - # It might be nice to compress JSON, but leaving that out to protect against potential - # compression+encryption information leak attacks like BREACH. - gzip on; - gzip_types text/css application/javascript image/svg+xml; - gzip_vary on; - - # Only connect to this site via HTTPS for the two years - add_header Strict-Transport-Security "max-age=63072000"; - - # Various content security headers - add_header Referrer-Policy "same-origin"; - add_header X-Content-Type-Options "nosniff"; - add_header X-Frame-Options "DENY"; - add_header X-XSS-Protection "1; mode=block"; - - # Upload limit for pictrs - client_max_body_size 20M; - - # frontend - location / { - # distinguish between ui requests and backend - # don't change lemmy-ui or lemmy here, they refer to the upstream definitions on top - set $proxpass "http://lemmy-ui"; - - if ($http_accept = "application/activity+json") { - set $proxpass "http://lemmy"; - } - if ($http_accept = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") { - set $proxpass "http://lemmy"; - } - if ($request_method = POST) { - set $proxpass "http://lemmy"; - } - proxy_pass $proxpass; - - rewrite ^(.+)/+$ $1 permanent; - - # Send actual client IP upstream - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - # backend - location ~ ^/(api|feeds|nodeinfo|.well-known) { - proxy_pass "http://lemmy"; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - # Rate limit - limit_req zone={{yourdomain}}_ratelimit burst=30 nodelay; - - # Add IP forwarding headers - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - # pictrs only - for adding browser cache control. - location ~ ^/(pictrs) { - # allow browser cache, images never update, we can apply long term cache - expires 120d; - add_header Pragma "public"; - add_header Cache-Control "public"; - - proxy_pass "http://lemmy"; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - # Rate limit - limit_req zone={{yourdomain}}_ratelimit burst=30 nodelay; - - # Add IP forwarding headers - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - # Redirect pictshare images to pictrs - location ~ /pictshare/(.*)$ { - return 301 /pictrs/image/$1; - } -} - -# Anonymize IP addresses -# https://www.supertechcrew.com/anonymizing-logs-nginx-apache/ -map $remote_addr $remote_addr_anon { - ~(?P\d+\.\d+\.\d+)\. $ip.0; - ~(?P[^:]+:[^:]+): $ip::; - 127.0.0.1 $remote_addr; - ::1 $remote_addr; - default 0.0.0.0; -} -access_log /var/log/nginx/access.log combined; diff --git a/docker/dev/test_deploy.sh b/docker/test_deploy.sh similarity index 81% rename from docker/dev/test_deploy.sh rename to docker/test_deploy.sh index 9a734be06d..fe91ea317b 100755 --- a/docker/dev/test_deploy.sh +++ b/docker/test_deploy.sh @@ -5,7 +5,7 @@ export COMPOSE_DOCKER_CLI_BUILD=1 export DOCKER_BUILDKIT=1 # Rebuilding dev docker -sudo docker build ../../ -f . -t "dessalines/lemmy:dev" +sudo docker build ../ -f . -t "dessalines/lemmy:dev" sudo docker push "dessalines/lemmy:dev" # Run the playbook diff --git a/scripts/release.sh b/scripts/release.sh index a909fa51a0..4b9fdd8e18 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -9,13 +9,11 @@ third_semver=$(echo $new_tag | cut -d "." -f 3) # The ansible and docker installs should only update for non release-candidates # IE, when the third semver is a number, not '2-rc' if [ ! -z "${third_semver##*[!0-9]*}" ]; then - pushd ../docker/prod/ - sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../prod/docker-compose.yml - sed -i "s/dessalines\/lemmy-ui:.*/dessalines\/lemmy-ui:$new_tag/" ../prod/docker-compose.yml - sed -i "s/dessalines\/lemmy-ui:.*/dessalines\/lemmy-ui:$new_tag/" ../dev/docker-compose.yml + pushd ../docker + sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../docker-compose.yml + sed -i "s/dessalines\/lemmy-ui:.*/dessalines\/lemmy-ui:$new_tag/" ../docker-compose.yml sed -i "s/dessalines\/lemmy-ui:.*/dessalines\/lemmy-ui:$new_tag/" ../federation/docker-compose.yml - git add ../prod/docker-compose.yml - git add ../dev/docker-compose.yml + git add ../docker-compose.yml git add ../federation/docker-compose.yml popd diff --git a/scripts/restore_db.sh b/scripts/restore_db.sh index 318b99099d..a886fc08d5 100755 --- a/scripts/restore_db.sh +++ b/scripts/restore_db.sh @@ -1,5 +1,5 @@ #!/bin/bash psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" -cat docker/dev/lemmy_dump_2021-01-29_16_13_40.sqldump | psql -U lemmy +cat docker/lemmy_dump_2021-01-29_16_13_40.sqldump | psql -U lemmy psql -U lemmy -c "alter user lemmy with password 'password'" From 8e1d5a302a9f821026e74a9ebf29a3e27b04aa49 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 16 Feb 2023 10:55:56 -0500 Subject: [PATCH 2/5] Dont use cache for release build. --- docker/Dockerfile | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d3b9a85431..068f55c22a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -10,13 +10,19 @@ COPY . . # Build the project RUN echo "pub const VERSION: &str = \"$(git describe --tag)\";" > "crates/utils/src/version.rs" +# Debug mode build RUN --mount=type=cache,target=/app/target \ + if [ "$RUST_RELEASE_MODE" = "debug" ] ; then \ + cargo build --target ${CARGO_BUILD_TARGET} \ + && cp ./target/$CARGO_BUILD_TARGET/$RUST_RELEASE_MODE/lemmy_server /app/lemmy_server; \ + fi + +# Release mode build +RUN \ if [ "$RUST_RELEASE_MODE" = "release" ] ; then \ - cargo build --target ${CARGO_BUILD_TARGET} --release; \ - else \ - cargo build --target ${CARGO_BUILD_TARGET}; \ - fi \ - && cp ./target/$CARGO_BUILD_TARGET/$RUST_RELEASE_MODE/lemmy_server /app/lemmy_server + cargo build --target ${CARGO_BUILD_TARGET} --release \ + && cp ./target/$CARGO_BUILD_TARGET/$RUST_RELEASE_MODE/lemmy_server /app/lemmy_server; \ + fi # The alpine runner FROM alpine:3 as lemmy From 30b38cc564994e8bd23fdce5844fc39323818dc5 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 17 Feb 2023 13:15:28 -0500 Subject: [PATCH 3/5] Adding 2FA / TOTP support. - Fixes #2363 --- Cargo.lock | 35 ++++++++++++ crates/api/src/local_user/login.rs | 25 ++++++-- crates/api/src/local_user/save_settings.rs | 34 ++++++++--- crates/api_common/src/person.rs | 3 + crates/db_schema/src/impls/local_user.rs | 6 ++ crates/db_schema/src/schema.rs | 2 + crates/db_schema/src/source/local_user.rs | 10 ++++ .../src/registration_application_view.rs | 2 + crates/utils/Cargo.toml | 1 + crates/utils/src/utils/validation.rs | 57 +++++++++++++++++++ .../down.sql | 2 + .../2023-02-16-194139_add_totp_secret/up.sql | 2 + 12 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 migrations/2023-02-16-194139_add_totp_secret/down.sql create mode 100644 migrations/2023-02-16-194139_add_totp_secret/up.sql diff --git a/Cargo.lock b/Cargo.lock index de31dbf0eb..5d93f015d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -599,6 +599,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + [[package]] name = "base64" version = "0.13.1" @@ -945,6 +951,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "constant_time_eq" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279" + [[package]] name = "convert_case" version = "0.4.0" @@ -2633,6 +2645,7 @@ dependencies = [ "strum", "strum_macros", "tokio", + "totp-rs", "tracing", "tracing-error", "typed-builder", @@ -5021,6 +5034,22 @@ dependencies = [ "syn 1.0.103", ] +[[package]] +name = "totp-rs" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdd21080b6cf581e0c8fe849626ad627b42af1a0f71ce980244f2d6b1a47836" +dependencies = [ + "base32", + "constant_time_eq", + "hmac", + "rand 0.8.5", + "sha1", + "sha2", + "url", + "urlencoding", +] + [[package]] name = "tower" version = "0.4.13" @@ -5375,6 +5404,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + [[package]] name = "utf-8" version = "0.7.6" diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs index c60c0dcdf4..fe9be6fbf7 100644 --- a/crates/api/src/local_user/login.rs +++ b/crates/api/src/local_user/login.rs @@ -6,9 +6,13 @@ use lemmy_api_common::{ person::{Login, LoginResponse}, utils::{check_registration_application, check_user_valid}, }; -use lemmy_db_schema::source::local_site::LocalSite; -use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::{claims::Claims, error::LemmyError, ConnectionId}; +use lemmy_db_views::structs::{LocalUserView, SiteView}; +use lemmy_utils::{ + claims::Claims, + error::LemmyError, + utils::validation::check_totp_valid, + ConnectionId, +}; #[async_trait::async_trait(?Send)] impl Perform for Login { @@ -22,7 +26,7 @@ impl Perform for Login { ) -> Result { let data: &Login = self; - let local_site = LocalSite::read(context.pool()).await?; + let site_view = SiteView::read_local(context.pool()).await?; // Fetch that username / email let username_or_email = data.username_or_email.clone(); @@ -45,11 +49,20 @@ impl Perform for Login { local_user_view.person.deleted, )?; - if local_site.require_email_verification && !local_user_view.local_user.email_verified { + if site_view.local_site.require_email_verification && !local_user_view.local_user.email_verified + { return Err(LemmyError::from_message("email_not_verified")); } - check_registration_application(&local_user_view, &local_site, context.pool()).await?; + check_registration_application(&local_user_view, &site_view.local_site, context.pool()).await?; + + // Check the totp + check_totp_valid( + &local_user_view.local_user.totp_secret, + &data.totp_token, + &site_view.site.name, + &local_user_view.person.name, + )?; // Return the jwt Ok(LoginResponse { diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs index f3f7a8478d..3fb9244373 100644 --- a/crates/api/src/local_user/save_settings.rs +++ b/crates/api/src/local_user/save_settings.rs @@ -8,17 +8,22 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ actor_language::LocalUserLanguage, - local_site::LocalSite, local_user::{LocalUser, LocalUserUpdateForm}, person::{Person, PersonUpdateForm}, }, traits::Crud, utils::{diesel_option_overwrite, diesel_option_overwrite_to_url}, }; +use lemmy_db_views::structs::SiteView; use lemmy_utils::{ claims::Claims, error::LemmyError, - utils::validation::{is_valid_display_name, is_valid_matrix_id}, + utils::validation::{ + build_totp, + generate_totp_secret, + is_valid_display_name, + is_valid_matrix_id, + }, ConnectionId, }; @@ -35,14 +40,13 @@ impl Perform for SaveUserSettings { let data: &SaveUserSettings = self; let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - let local_site = LocalSite::read(context.pool()).await?; + let site_view = SiteView::read_local(context.pool()).await?; let avatar = diesel_option_overwrite_to_url(&data.avatar)?; let banner = diesel_option_overwrite_to_url(&data.banner)?; let bio = diesel_option_overwrite(&data.bio); let display_name = diesel_option_overwrite(&data.display_name); let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id); - let bot_account = data.bot_account; let email_deref = data.email.as_deref().map(str::to_lowercase); let email = diesel_option_overwrite(&email_deref); @@ -57,7 +61,7 @@ impl Perform for SaveUserSettings { // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value if let Some(email) = &email { - if email.is_none() && local_site.require_email_verification { + if email.is_none() && site_view.local_site.require_email_verification { return Err(LemmyError::from_message("email_required")); } } @@ -71,7 +75,7 @@ impl Perform for SaveUserSettings { if let Some(Some(display_name)) = &display_name { if !is_valid_display_name( display_name.trim(), - local_site.actor_name_max_length as usize, + site_view.local_site.actor_name_max_length as usize, ) { return Err(LemmyError::from_message("invalid_username")); } @@ -92,7 +96,7 @@ impl Perform for SaveUserSettings { .display_name(display_name) .bio(bio) .matrix_user_id(matrix_user_id) - .bot_account(bot_account) + .bot_account(data.bot_account) .avatar(avatar) .banner(banner) .build(); @@ -105,6 +109,20 @@ impl Perform for SaveUserSettings { LocalUserLanguage::update(context.pool(), discussion_languages, local_user_id).await?; } + // If generate_totp is Some(false), this will clear it out from the database. + let (totp_secret, totp_url) = if let Some(generate) = data.generate_totp { + if generate { + let secret = generate_totp_secret(); + let url = + build_totp(&site_view.site.name, &local_user_view.person.name, &secret)?.get_url(); + (Some(Some(secret)), Some(Some(url))) + } else { + (Some(None), Some(None)) + } + } else { + (None, None) + }; + let local_user_form = LocalUserUpdateForm::builder() .email(email) .show_avatars(data.show_avatars) @@ -118,6 +136,8 @@ impl Perform for SaveUserSettings { .default_listing_type(default_listing_type) .theme(data.theme.clone()) .interface_language(data.interface_language.clone()) + .totp_secret(totp_secret) + .totp_url(totp_url) .build(); let local_user_res = LocalUser::update(context.pool(), local_user_id, &local_user_form).await; diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 897dd998ec..c1ca712b05 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -17,6 +17,7 @@ use serde::{Deserialize, Serialize}; pub struct Login { pub username_or_email: Sensitive, pub password: Sensitive, + pub totp_token: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Default)] @@ -70,6 +71,8 @@ pub struct SaveUserSettings { pub show_read_posts: Option, pub show_new_post_notifs: Option, pub discussion_languages: Option>, + /// None leaves it as is, true will generate or regenerate it, false clears it out + pub generate_totp: Option, pub auth: Sensitive, } diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 650eac65c1..8275d22684 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -37,6 +37,8 @@ mod safe_settings_type { show_read_posts, show_scores, theme, + totp_secret, + totp_url, validator_time, }, source::local_user::LocalUser, @@ -61,6 +63,8 @@ mod safe_settings_type { show_new_post_notifs, email_verified, accepted_application, + totp_secret, + totp_url, ); impl ToSafeSettings for LocalUser { @@ -86,6 +90,8 @@ mod safe_settings_type { show_new_post_notifs, email_verified, accepted_application, + totp_secret, + totp_url, ) } } diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 60152d6f83..cc85a5a790 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -168,6 +168,8 @@ table! { show_new_post_notifs -> Bool, email_verified -> Bool, accepted_application -> Bool, + totp_secret -> Nullable, + totp_url -> Nullable, } } diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 37a559bc6d..911469902b 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -26,6 +26,9 @@ pub struct LocalUser { pub show_new_post_notifs: bool, pub email_verified: bool, pub accepted_application: bool, + #[serde(skip)] + pub totp_secret: Option, + pub totp_url: Option, } /// A local user view that removes password encrypted @@ -50,6 +53,9 @@ pub struct LocalUserSettings { pub show_new_post_notifs: bool, pub email_verified: bool, pub accepted_application: bool, + #[serde(skip)] + pub totp_secret: Option, + pub totp_url: Option, } #[derive(Clone, TypedBuilder)] @@ -75,6 +81,8 @@ pub struct LocalUserInsertForm { pub show_new_post_notifs: Option, pub email_verified: Option, pub accepted_application: Option, + pub totp_secret: Option>, + pub totp_url: Option>, } #[derive(Clone, TypedBuilder)] @@ -97,4 +105,6 @@ pub struct LocalUserUpdateForm { pub show_new_post_notifs: Option, pub email_verified: Option, pub accepted_application: Option, + pub totp_secret: Option>, + pub totp_url: Option>, } diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index 22f62a881e..e14c433aa7 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -288,6 +288,8 @@ mod tests { show_new_post_notifs: inserted_sara_local_user.show_new_post_notifs, email_verified: inserted_sara_local_user.email_verified, accepted_application: inserted_sara_local_user.accepted_application, + totp_secret: inserted_sara_local_user.totp_secret, + totp_url: inserted_sara_local_user.totp_url, }, creator: PersonSafe { id: inserted_sara_person.id, diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 21f92b117a..7a6ecdfd43 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -46,6 +46,7 @@ smart-default = "0.6.0" jsonwebtoken = "8.1.1" lettre = "0.10.1" comrak = { version = "0.14.0", default-features = false } +totp-rs = { version = "4.2.0", features = ["gen_secret", "otpauth"] } [build-dependencies] rosetta-build = "0.1.2" diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index 43f3cb35fa..c837e456b6 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -1,6 +1,8 @@ +use crate::error::LemmyError; use itertools::Itertools; use once_cell::sync::Lazy; use regex::Regex; +use totp_rs::{Secret, TOTP}; use url::Url; static VALID_ACTOR_NAME_REGEX: Lazy = @@ -56,10 +58,58 @@ pub fn clean_url_params(url: &Url) -> Url { url_out } +pub fn check_totp_valid( + totp_secret: &Option, + totp_token: &Option, + site_name: &str, + username: &str, +) -> Result<(), LemmyError> { + // Check only if they have a totp_secret in the DB + if let Some(totp_secret) = totp_secret { + // Throw an error if their token is missing + let token = totp_token + .as_deref() + .ok_or_else(|| LemmyError::from_message("missing_totp_token"))?; + + let totp = build_totp(site_name, username, totp_secret)?; + + let check_passed = totp.check_current(token)?; + if !check_passed { + return Err(LemmyError::from_message("incorrect_totp token")); + } + } + + Ok(()) +} + +pub fn generate_totp_secret() -> String { + Secret::generate_secret().to_string() +} + +pub fn build_totp(site_name: &str, username: &str, secret: &str) -> Result { + let sec = Secret::Raw(secret.as_bytes().to_vec()); + let sec_bytes = sec + .to_bytes() + .map_err(|_| LemmyError::from_message("Couldnt parse totp secret"))?; + + TOTP::new( + totp_rs::Algorithm::SHA1, + 6, + 1, + 30, + sec_bytes, + Some(site_name.to_string()), + username.to_string(), + ) + .map_err(|_| LemmyError::from_message("Couldnt generate TOTP")) +} + #[cfg(test)] mod tests { + use super::build_totp; use crate::utils::validation::{ clean_url_params, + generate_totp_secret, is_valid_actor_name, is_valid_display_name, is_valid_matrix_id, @@ -128,4 +178,11 @@ mod tests { assert!(!is_valid_matrix_id(" @dess:matrix.org")); assert!(!is_valid_matrix_id("@dess:matrix.org t")); } + + #[test] + fn test_build_totp() { + let generated_secret = generate_totp_secret(); + let totp = build_totp("lemmy", "my_name", &generated_secret); + assert!(totp.is_ok()); + } } diff --git a/migrations/2023-02-16-194139_add_totp_secret/down.sql b/migrations/2023-02-16-194139_add_totp_secret/down.sql new file mode 100644 index 0000000000..64c505691a --- /dev/null +++ b/migrations/2023-02-16-194139_add_totp_secret/down.sql @@ -0,0 +1,2 @@ +alter table local_user drop column totp_secret; +alter table local_user drop column totp_url; diff --git a/migrations/2023-02-16-194139_add_totp_secret/up.sql b/migrations/2023-02-16-194139_add_totp_secret/up.sql new file mode 100644 index 0000000000..c7ae8787c4 --- /dev/null +++ b/migrations/2023-02-16-194139_add_totp_secret/up.sql @@ -0,0 +1,2 @@ +alter table local_user add column totp_secret text; +alter table local_user add column totp_url text; From 20d24e7a5ff5d7d6da0e67983bfa08aaaec11e9d Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 24 Feb 2023 09:03:29 -0500 Subject: [PATCH 4/5] Changed name to totp_2fa for clarity. --- crates/api/src/local_user/login.rs | 8 ++++---- crates/api/src/local_user/save_settings.rs | 14 +++++++------- crates/api_common/src/person.rs | 4 ++-- crates/db_schema/src/impls/local_user.rs | 12 ++++++------ crates/db_schema/src/schema.rs | 4 ++-- crates/db_schema/src/source/local_user.rs | 16 ++++++++-------- .../src/registration_application_view.rs | 4 ++-- crates/utils/src/utils/validation.rs | 16 ++++++++-------- .../2023-02-16-194139_add_totp_secret/down.sql | 4 ++-- .../2023-02-16-194139_add_totp_secret/up.sql | 4 ++-- 10 files changed, 43 insertions(+), 43 deletions(-) diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs index fe9be6fbf7..25323c4530 100644 --- a/crates/api/src/local_user/login.rs +++ b/crates/api/src/local_user/login.rs @@ -10,7 +10,7 @@ use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::{ claims::Claims, error::LemmyError, - utils::validation::check_totp_valid, + utils::validation::check_totp_2fa_valid, ConnectionId, }; @@ -57,9 +57,9 @@ impl Perform for Login { check_registration_application(&local_user_view, &site_view.local_site, context.pool()).await?; // Check the totp - check_totp_valid( - &local_user_view.local_user.totp_secret, - &data.totp_token, + check_totp_2fa_valid( + &local_user_view.local_user.totp_2fa_secret, + &data.totp_2fa_token, &site_view.site.name, &local_user_view.person.name, )?; diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs index 3fb9244373..e3c95a3d3f 100644 --- a/crates/api/src/local_user/save_settings.rs +++ b/crates/api/src/local_user/save_settings.rs @@ -19,8 +19,8 @@ use lemmy_utils::{ claims::Claims, error::LemmyError, utils::validation::{ - build_totp, - generate_totp_secret, + build_totp_2fa, + generate_totp_2fa_secret, is_valid_display_name, is_valid_matrix_id, }, @@ -110,11 +110,11 @@ impl Perform for SaveUserSettings { } // If generate_totp is Some(false), this will clear it out from the database. - let (totp_secret, totp_url) = if let Some(generate) = data.generate_totp { + let (totp_2fa_secret, totp_2fa_url) = if let Some(generate) = data.generate_totp_2fa { if generate { - let secret = generate_totp_secret(); + let secret = generate_totp_2fa_secret(); let url = - build_totp(&site_view.site.name, &local_user_view.person.name, &secret)?.get_url(); + build_totp_2fa(&site_view.site.name, &local_user_view.person.name, &secret)?.get_url(); (Some(Some(secret)), Some(Some(url))) } else { (Some(None), Some(None)) @@ -136,8 +136,8 @@ impl Perform for SaveUserSettings { .default_listing_type(default_listing_type) .theme(data.theme.clone()) .interface_language(data.interface_language.clone()) - .totp_secret(totp_secret) - .totp_url(totp_url) + .totp_2fa_secret(totp_2fa_secret) + .totp_2fa_url(totp_2fa_url) .build(); let local_user_res = LocalUser::update(context.pool(), local_user_id, &local_user_form).await; diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index c1ca712b05..3ff7ab1623 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize}; pub struct Login { pub username_or_email: Sensitive, pub password: Sensitive, - pub totp_token: Option, + pub totp_2fa_token: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Default)] @@ -72,7 +72,7 @@ pub struct SaveUserSettings { pub show_new_post_notifs: Option, pub discussion_languages: Option>, /// None leaves it as is, true will generate or regenerate it, false clears it out - pub generate_totp: Option, + pub generate_totp_2fa: Option, pub auth: Sensitive, } diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 8275d22684..f5f02934f0 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -37,8 +37,8 @@ mod safe_settings_type { show_read_posts, show_scores, theme, - totp_secret, - totp_url, + totp_2fa_secret, + totp_2fa_url, validator_time, }, source::local_user::LocalUser, @@ -63,8 +63,8 @@ mod safe_settings_type { show_new_post_notifs, email_verified, accepted_application, - totp_secret, - totp_url, + totp_2fa_secret, + totp_2fa_url, ); impl ToSafeSettings for LocalUser { @@ -90,8 +90,8 @@ mod safe_settings_type { show_new_post_notifs, email_verified, accepted_application, - totp_secret, - totp_url, + totp_2fa_secret, + totp_2fa_url, ) } } diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index d9889e4696..870754a299 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -170,8 +170,8 @@ table! { show_new_post_notifs -> Bool, email_verified -> Bool, accepted_application -> Bool, - totp_secret -> Nullable, - totp_url -> Nullable, + totp_2fa_secret -> Nullable, + totp_2fa_url -> Nullable, } } diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 911469902b..6bdf2335f0 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -27,8 +27,8 @@ pub struct LocalUser { pub email_verified: bool, pub accepted_application: bool, #[serde(skip)] - pub totp_secret: Option, - pub totp_url: Option, + pub totp_2fa_secret: Option, + pub totp_2fa_url: Option, } /// A local user view that removes password encrypted @@ -54,8 +54,8 @@ pub struct LocalUserSettings { pub email_verified: bool, pub accepted_application: bool, #[serde(skip)] - pub totp_secret: Option, - pub totp_url: Option, + pub totp_2fa_secret: Option, + pub totp_2fa_url: Option, } #[derive(Clone, TypedBuilder)] @@ -81,8 +81,8 @@ pub struct LocalUserInsertForm { pub show_new_post_notifs: Option, pub email_verified: Option, pub accepted_application: Option, - pub totp_secret: Option>, - pub totp_url: Option>, + pub totp_2fa_secret: Option>, + pub totp_2fa_url: Option>, } #[derive(Clone, TypedBuilder)] @@ -105,6 +105,6 @@ pub struct LocalUserUpdateForm { pub show_new_post_notifs: Option, pub email_verified: Option, pub accepted_application: Option, - pub totp_secret: Option>, - pub totp_url: Option>, + pub totp_2fa_secret: Option>, + pub totp_2fa_url: Option>, } diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index e14c433aa7..873a21be00 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -288,8 +288,8 @@ mod tests { show_new_post_notifs: inserted_sara_local_user.show_new_post_notifs, email_verified: inserted_sara_local_user.email_verified, accepted_application: inserted_sara_local_user.accepted_application, - totp_secret: inserted_sara_local_user.totp_secret, - totp_url: inserted_sara_local_user.totp_url, + totp_2fa_secret: inserted_sara_local_user.totp_2fa_secret, + totp_2fa_url: inserted_sara_local_user.totp_2fa_url, }, creator: PersonSafe { id: inserted_sara_person.id, diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index c837e456b6..a5db6a2bbd 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -58,7 +58,7 @@ pub fn clean_url_params(url: &Url) -> Url { url_out } -pub fn check_totp_valid( +pub fn check_totp_2fa_valid( totp_secret: &Option, totp_token: &Option, site_name: &str, @@ -71,7 +71,7 @@ pub fn check_totp_valid( .as_deref() .ok_or_else(|| LemmyError::from_message("missing_totp_token"))?; - let totp = build_totp(site_name, username, totp_secret)?; + let totp = build_totp_2fa(site_name, username, totp_secret)?; let check_passed = totp.check_current(token)?; if !check_passed { @@ -82,11 +82,11 @@ pub fn check_totp_valid( Ok(()) } -pub fn generate_totp_secret() -> String { +pub fn generate_totp_2fa_secret() -> String { Secret::generate_secret().to_string() } -pub fn build_totp(site_name: &str, username: &str, secret: &str) -> Result { +pub fn build_totp_2fa(site_name: &str, username: &str, secret: &str) -> Result { let sec = Secret::Raw(secret.as_bytes().to_vec()); let sec_bytes = sec .to_bytes() @@ -106,10 +106,10 @@ pub fn build_totp(site_name: &str, username: &str, secret: &str) -> Result Date: Sun, 26 Feb 2023 09:31:44 -0500 Subject: [PATCH 5/5] Switch to sha256 for totp. --- crates/utils/src/utils/validation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index a5db6a2bbd..37838866d8 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -93,7 +93,7 @@ pub fn build_totp_2fa(site_name: &str, username: &str, secret: &str) -> Result Result