mirror of
https://github.com/bitwarden/server
synced 2025-12-24 20:23:21 +00:00
Merge branch 'SM-1571-DisableSMAdsForUsers' of https://github.com/bitwarden/server into SM-1571-DisableSMAdsForUsers
This commit is contained in:
28
.github/workflows/_move_edd_db_scripts.yml
vendored
28
.github/workflows/_move_edd_db_scripts.yml
vendored
@@ -41,18 +41,19 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Get script prefix
|
||||
id: prefix
|
||||
run: echo "prefix=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
run: echo "prefix=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check if any files in DB transition or finalization directories
|
||||
id: check-script-existence
|
||||
run: |
|
||||
if [ -f util/Migrator/DbScripts_transition/* -o -f util/Migrator/DbScripts_finalization/* ]; then
|
||||
echo "copy_edd_scripts=true" >> $GITHUB_OUTPUT
|
||||
echo "copy_edd_scripts=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "copy_edd_scripts=false" >> $GITHUB_OUTPUT
|
||||
echo "copy_edd_scripts=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
move-scripts:
|
||||
@@ -70,17 +71,18 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Generate branch name
|
||||
id: branch_name
|
||||
env:
|
||||
PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }}
|
||||
run: echo "branch_name=move_edd_db_scripts_$PREFIX" >> $GITHUB_OUTPUT
|
||||
run: echo "branch_name=move_edd_db_scripts_$PREFIX" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: "Create branch"
|
||||
env:
|
||||
BRANCH: ${{ steps.branch_name.outputs.branch_name }}
|
||||
run: git switch -c $BRANCH
|
||||
run: git switch -c "$BRANCH"
|
||||
|
||||
- name: Move scripts and finalization database schema
|
||||
id: move-files
|
||||
@@ -120,7 +122,7 @@ jobs:
|
||||
|
||||
# sync finalization schema back to dbo, maintaining structure
|
||||
rsync -r "$src_dir/" "$dest_dir/"
|
||||
rm -rf $src_dir/*
|
||||
rm -rf "${src_dir}"/*
|
||||
|
||||
# Replace any finalization references due to the move
|
||||
find ./src/Sql/dbo -name "*.sql" -type f -exec sed -i \
|
||||
@@ -131,7 +133,7 @@ jobs:
|
||||
moved_files="$moved_files \n $file"
|
||||
done
|
||||
|
||||
echo "moved_files=$moved_files" >> $GITHUB_OUTPUT
|
||||
echo "moved_files=$moved_files" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@@ -162,18 +164,20 @@ jobs:
|
||||
|
||||
- name: Commit and push changes
|
||||
id: commit
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.branch_name.outputs.branch_name }}
|
||||
run: |
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
git config --local user.name "bitwarden-devops-bot"
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git add .
|
||||
git commit -m "Move EDD database scripts" -a
|
||||
git push -u origin ${{ steps.branch_name.outputs.branch_name }}
|
||||
echo "pr_needed=true" >> $GITHUB_OUTPUT
|
||||
git push -u origin "${BRANCH_NAME}"
|
||||
echo "pr_needed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No changes to commit!";
|
||||
echo "pr_needed=false" >> $GITHUB_OUTPUT
|
||||
echo "### :mega: No changes to commit! PR was ommited." >> $GITHUB_STEP_SUMMARY
|
||||
echo "pr_needed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "### :mega: No changes to commit! PR was ommited." >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Create PR for ${{ steps.branch_name.outputs.branch_name }}
|
||||
@@ -195,7 +199,7 @@ jobs:
|
||||
Files moved:
|
||||
$(echo -e "$MOVED_FILES")
|
||||
")
|
||||
echo "pr_url=${PR_URL}" >> $GITHUB_OUTPUT
|
||||
echo "pr_url=${PR_URL}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Notify Slack about creation of PR
|
||||
if: ${{ steps.commit.outputs.pr_needed == 'true' }}
|
||||
|
||||
59
.github/workflows/build.yml
vendored
59
.github/workflows/build.yml
vendored
@@ -28,6 +28,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
@@ -97,23 +98,24 @@ jobs:
|
||||
id: check-secrets
|
||||
run: |
|
||||
has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }}
|
||||
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
|
||||
echo "has_secrets=$has_secrets" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check branch to publish
|
||||
env:
|
||||
PUBLISH_BRANCHES: "main,rc,hotfix-rc"
|
||||
id: publish-branch-check
|
||||
run: |
|
||||
IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES
|
||||
IFS="," read -a publish_branches <<< "$PUBLISH_BRANCHES"
|
||||
if [[ " ${publish_branches[*]} " =~ " ${GITHUB_REF:11} " ]]; then
|
||||
echo "is_publish_branch=true" >> $GITHUB_ENV
|
||||
echo "is_publish_branch=true" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "is_publish_branch=false" >> $GITHUB_ENV
|
||||
echo "is_publish_branch=false" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Set up .NET
|
||||
@@ -209,8 +211,8 @@ jobs:
|
||||
IMAGE_TAG=dev
|
||||
fi
|
||||
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
echo "### :mega: Docker Image Tag: $IMAGE_TAG" >> $GITHUB_STEP_SUMMARY
|
||||
echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "### :mega: Docker Image Tag: $IMAGE_TAG" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Set up project name
|
||||
id: setup
|
||||
@@ -218,7 +220,7 @@ jobs:
|
||||
PROJECT_NAME=$(echo "${{ matrix.project_name }}" | awk '{print tolower($0)}')
|
||||
echo "Matrix name: ${{ matrix.project_name }}"
|
||||
echo "PROJECT_NAME: $PROJECT_NAME"
|
||||
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
|
||||
echo "project_name=$PROJECT_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate image tags(s)
|
||||
id: image-tags
|
||||
@@ -228,12 +230,12 @@ jobs:
|
||||
SHA: ${{ github.sha }}
|
||||
run: |
|
||||
TAGS="${_AZ_REGISTRY}/${PROJECT_NAME}:${IMAGE_TAG}"
|
||||
echo "primary_tag=$TAGS" >> $GITHUB_OUTPUT
|
||||
echo "primary_tag=$TAGS" >> "$GITHUB_OUTPUT"
|
||||
if [[ "${IMAGE_TAG}" == "dev" ]]; then
|
||||
SHORT_SHA=$(git rev-parse --short ${SHA})
|
||||
SHORT_SHA=$(git rev-parse --short "${SHA}")
|
||||
TAGS=$TAGS",${_AZ_REGISTRY}/${PROJECT_NAME}:dev-${SHORT_SHA}"
|
||||
fi
|
||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build Docker image
|
||||
id: build-artifacts
|
||||
@@ -260,12 +262,13 @@ jobs:
|
||||
DIGEST: ${{ steps.build-artifacts.outputs.digest }}
|
||||
TAGS: ${{ steps.image-tags.outputs.tags }}
|
||||
run: |
|
||||
IFS="," read -a tags <<< "${TAGS}"
|
||||
images=""
|
||||
for tag in "${tags[@]}"; do
|
||||
images+="${tag}@${DIGEST} "
|
||||
IFS=',' read -r -a tags_array <<< "${TAGS}"
|
||||
images=()
|
||||
for tag in "${tags_array[@]}"; do
|
||||
images+=("${tag}@${DIGEST}")
|
||||
done
|
||||
cosign sign --yes ${images}
|
||||
cosign sign --yes ${images[@]}
|
||||
echo "images=${images[*]}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Scan Docker image
|
||||
id: container-scan
|
||||
@@ -297,6 +300,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
@@ -309,7 +313,7 @@ jobs:
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Log in to ACR - production subscription
|
||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||
run: az acr login -n "$_AZ_REGISTRY" --only-show-errors
|
||||
|
||||
- name: Make Docker stubs
|
||||
if: |
|
||||
@@ -332,26 +336,26 @@ jobs:
|
||||
STUB_OUTPUT=$(pwd)/docker-stub
|
||||
|
||||
# Run setup
|
||||
docker run -i --rm --name setup -v $STUB_OUTPUT/US:/bitwarden $SETUP_IMAGE \
|
||||
docker run -i --rm --name setup -v "$STUB_OUTPUT/US:/bitwarden" "$SETUP_IMAGE" \
|
||||
/app/Setup -stub 1 -install 1 -domain bitwarden.example.com -os lin -cloud-region US
|
||||
docker run -i --rm --name setup -v $STUB_OUTPUT/EU:/bitwarden $SETUP_IMAGE \
|
||||
docker run -i --rm --name setup -v "$STUB_OUTPUT/EU:/bitwarden" "$SETUP_IMAGE" \
|
||||
/app/Setup -stub 1 -install 1 -domain bitwarden.example.com -os lin -cloud-region EU
|
||||
|
||||
sudo chown -R $(whoami):$(whoami) $STUB_OUTPUT
|
||||
sudo chown -R "$(whoami):$(whoami)" "$STUB_OUTPUT"
|
||||
|
||||
# Remove extra directories and files
|
||||
rm -rf $STUB_OUTPUT/US/letsencrypt
|
||||
rm -rf $STUB_OUTPUT/EU/letsencrypt
|
||||
rm $STUB_OUTPUT/US/env/uid.env $STUB_OUTPUT/US/config.yml
|
||||
rm $STUB_OUTPUT/EU/env/uid.env $STUB_OUTPUT/EU/config.yml
|
||||
rm -rf "$STUB_OUTPUT/US/letsencrypt"
|
||||
rm -rf "$STUB_OUTPUT/EU/letsencrypt"
|
||||
rm "$STUB_OUTPUT/US/env/uid.env" "$STUB_OUTPUT/US/config.yml"
|
||||
rm "$STUB_OUTPUT/EU/env/uid.env" "$STUB_OUTPUT/EU/config.yml"
|
||||
|
||||
# Create uid environment files
|
||||
touch $STUB_OUTPUT/US/env/uid.env
|
||||
touch $STUB_OUTPUT/EU/env/uid.env
|
||||
touch "$STUB_OUTPUT/US/env/uid.env"
|
||||
touch "$STUB_OUTPUT/EU/env/uid.env"
|
||||
|
||||
# Zip up the Docker stub files
|
||||
cd docker-stub/US; zip -r ../../docker-stub-US.zip *; cd ../..
|
||||
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../..
|
||||
cd docker-stub/US; zip -r ../../docker-stub-US.zip ./*; cd ../..
|
||||
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip ./*; cd ../..
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
@@ -423,6 +427,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
|
||||
12
.github/workflows/cleanup-after-pr.yml
vendored
12
.github/workflows/cleanup-after-pr.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Log in to Azure ACR
|
||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||
run: az acr login -n "$_AZ_REGISTRY" --only-show-errors
|
||||
|
||||
########## Remove Docker images ##########
|
||||
- name: Remove the Docker image from ACR
|
||||
@@ -45,20 +45,20 @@ jobs:
|
||||
- Setup
|
||||
- Sso
|
||||
run: |
|
||||
for SERVICE in $(echo "${{ env.SERVICES }}" | yq e ".services[]" - )
|
||||
for SERVICE in $(echo "${SERVICES}" | yq e ".services[]" - )
|
||||
do
|
||||
SERVICE_NAME=$(echo $SERVICE | awk '{print tolower($0)}')
|
||||
SERVICE_NAME=$(echo "$SERVICE" | awk '{print tolower($0)}')
|
||||
IMAGE_TAG=$(echo "${REF}" | sed "s#/#-#g") # slash safe branch name
|
||||
|
||||
echo "[*] Checking if remote exists: $_AZ_REGISTRY/$SERVICE_NAME:$IMAGE_TAG"
|
||||
TAG_EXISTS=$(
|
||||
az acr repository show-tags --name $_AZ_REGISTRY --repository $SERVICE_NAME \
|
||||
| jq --arg $TAG "$IMAGE_TAG" -e '. | any(. == "$TAG")'
|
||||
az acr repository show-tags --name "$_AZ_REGISTRY" --repository "$SERVICE_NAME" \
|
||||
| jq --arg TAG "$IMAGE_TAG" -e '. | any(. == $TAG)'
|
||||
)
|
||||
|
||||
if [[ "$TAG_EXISTS" == "true" ]]; then
|
||||
echo "[*] Tag exists. Removing tag"
|
||||
az acr repository delete --name $_AZ_REGISTRY --image $SERVICE_NAME:$IMAGE_TAG --yes
|
||||
az acr repository delete --name "$_AZ_REGISTRY" --image "$SERVICE_NAME:$IMAGE_TAG" --yes
|
||||
else
|
||||
echo "[*] Tag does not exist. No action needed"
|
||||
fi
|
||||
|
||||
14
.github/workflows/cleanup-rc-branch.yml
vendored
14
.github/workflows/cleanup-rc-branch.yml
vendored
@@ -35,6 +35,8 @@ jobs:
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if a RC branch exists
|
||||
id: branch-check
|
||||
@@ -43,11 +45,11 @@ jobs:
|
||||
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
|
||||
|
||||
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
|
||||
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
|
||||
echo "hotfix-rc branch exists." | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
echo "name=hotfix-rc" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${rc_branch_check}" -gt 0 ]]; then
|
||||
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "name=rc" >> $GITHUB_OUTPUT
|
||||
echo "rc branch exists." | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
echo "name=rc" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Delete RC branch
|
||||
@@ -55,6 +57,6 @@ jobs:
|
||||
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
|
||||
run: |
|
||||
if ! [[ -z "$BRANCH_NAME" ]]; then
|
||||
git push --quiet origin --delete $BRANCH_NAME
|
||||
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
|
||||
git push --quiet origin --delete "$BRANCH_NAME"
|
||||
echo "Deleted $BRANCH_NAME branch." | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
10
.github/workflows/code-references.yml
vendored
10
.github/workflows/code-references.yml
vendored
@@ -19,9 +19,9 @@ jobs:
|
||||
id: check-secret-access
|
||||
run: |
|
||||
if [ "${{ secrets.AZURE_CLIENT_ID }}" != '' ]; then
|
||||
echo "available=true" >> $GITHUB_OUTPUT;
|
||||
echo "available=true" >> "$GITHUB_OUTPUT";
|
||||
else
|
||||
echo "available=false" >> $GITHUB_OUTPUT;
|
||||
echo "available=false" >> "$GITHUB_OUTPUT";
|
||||
fi
|
||||
|
||||
refs:
|
||||
@@ -37,6 +37,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@@ -65,14 +67,14 @@ jobs:
|
||||
|
||||
- name: Add label
|
||||
if: steps.collect.outputs.any-changed == 'true'
|
||||
run: gh pr edit $PR_NUMBER --add-label feature-flag
|
||||
run: gh pr edit "$PR_NUMBER" --add-label feature-flag
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
- name: Remove label
|
||||
if: steps.collect.outputs.any-changed == 'false'
|
||||
run: gh pr edit $PR_NUMBER --remove-label feature-flag
|
||||
run: gh pr edit "$PR_NUMBER" --remove-label feature-flag
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
2
.github/workflows/enforce-labels.yml
vendored
2
.github/workflows/enforce-labels.yml
vendored
@@ -17,5 +17,5 @@ jobs:
|
||||
- name: Check for label
|
||||
run: |
|
||||
echo "PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged"
|
||||
echo "### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
|
||||
4
.github/workflows/load-test.yml
vendored
4
.github/workflows/load-test.yml
vendored
@@ -63,13 +63,15 @@ jobs:
|
||||
|
||||
# Datadog agent for collecting OTEL metrics from k6
|
||||
- name: Start Datadog agent
|
||||
env:
|
||||
DD_API_KEY: ${{ steps.get-kv-secrets.outputs.DD-API-KEY }}
|
||||
run: |
|
||||
docker run --detach \
|
||||
--name datadog-agent \
|
||||
-p 4317:4317 \
|
||||
-p 5555:5555 \
|
||||
-e DD_SITE=us3.datadoghq.com \
|
||||
-e DD_API_KEY=${{ steps.get-kv-secrets.outputs.DD-API-KEY }} \
|
||||
-e DD_API_KEY="${DD_API_KEY}" \
|
||||
-e DD_DOGSTATSD_NON_LOCAL_TRAFFIC=1 \
|
||||
-e DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT=0.0.0.0:4317 \
|
||||
-e DD_HEALTH_PORT=5555 \
|
||||
|
||||
5
.github/workflows/protect-files.yml
vendored
5
.github/workflows/protect-files.yml
vendored
@@ -34,6 +34,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for file changes
|
||||
id: check-changes
|
||||
@@ -43,9 +44,9 @@ jobs:
|
||||
for file in $MODIFIED_FILES
|
||||
do
|
||||
if [[ $file == *"${{ matrix.path }}"* ]]; then
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
echo "changes_detected=true" >> "$GITHUB_OUTPUT"
|
||||
break
|
||||
else echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
else echo "changes_detected=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
37
.github/workflows/publish.yml
vendored
37
.github/workflows/publish.yml
vendored
@@ -36,21 +36,23 @@ jobs:
|
||||
steps:
|
||||
- name: Version output
|
||||
id: version-output
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
if [[ "${{ inputs.version }}" == "latest" || "${{ inputs.version }}" == "" ]]; then
|
||||
if [[ "${INPUT_VERSION}" == "latest" || "${INPUT_VERSION}" == "" ]]; then
|
||||
VERSION=$(curl "https://api.github.com/repos/bitwarden/server/releases" | jq -c '.[] | select(.tag_name) | .tag_name' | head -1 | grep -ohE '20[0-9]{2}\.([1-9]|1[0-2])\.[0-9]+')
|
||||
echo "Latest Released Version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Release Version: ${{ inputs.version }}"
|
||||
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
|
||||
echo "Release Version: ${INPUT_VERSION}"
|
||||
echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Get branch name
|
||||
id: branch
|
||||
run: |
|
||||
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
BRANCH_NAME=$(basename "${GITHUB_REF}")
|
||||
echo "branch-name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create GitHub deployment
|
||||
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
|
||||
@@ -105,6 +107,9 @@ jobs:
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up project name
|
||||
id: setup
|
||||
@@ -112,7 +117,7 @@ jobs:
|
||||
PROJECT_NAME=$(echo "${{ matrix.project_name }}" | awk '{print tolower($0)}')
|
||||
echo "Matrix name: ${{ matrix.project_name }}"
|
||||
echo "PROJECT_NAME: $PROJECT_NAME"
|
||||
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
|
||||
echo "project_name=$PROJECT_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
########## ACR PROD ##########
|
||||
- name: Log in to Azure
|
||||
@@ -123,16 +128,16 @@ jobs:
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Log in to Azure ACR
|
||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||
run: az acr login -n "$_AZ_REGISTRY" --only-show-errors
|
||||
|
||||
- name: Pull latest project image
|
||||
env:
|
||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||
run: |
|
||||
if [[ "${{ inputs.publish_type }}" == "Dry Run" ]]; then
|
||||
docker pull $_AZ_REGISTRY/$PROJECT_NAME:latest
|
||||
docker pull "$_AZ_REGISTRY/$PROJECT_NAME:latest"
|
||||
else
|
||||
docker pull $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME
|
||||
docker pull "$_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME"
|
||||
fi
|
||||
|
||||
- name: Tag version and latest
|
||||
@@ -140,10 +145,10 @@ jobs:
|
||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||
run: |
|
||||
if [[ "${{ inputs.publish_type }}" == "Dry Run" ]]; then
|
||||
docker tag $_AZ_REGISTRY/$PROJECT_NAME:latest $_AZ_REGISTRY/$PROJECT_NAME:dryrun
|
||||
docker tag "$_AZ_REGISTRY/$PROJECT_NAME:latest" "$_AZ_REGISTRY/$PROJECT_NAME:dryrun"
|
||||
else
|
||||
docker tag $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION
|
||||
docker tag $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $_AZ_REGISTRY/$PROJECT_NAME:latest
|
||||
docker tag "$_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME" "$_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION"
|
||||
docker tag "$_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME" "$_AZ_REGISTRY/$PROJECT_NAME:latest"
|
||||
fi
|
||||
|
||||
- name: Push version and latest image
|
||||
@@ -151,10 +156,10 @@ jobs:
|
||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||
run: |
|
||||
if [[ "${{ inputs.publish_type }}" == "Dry Run" ]]; then
|
||||
docker push $_AZ_REGISTRY/$PROJECT_NAME:dryrun
|
||||
docker push "$_AZ_REGISTRY/$PROJECT_NAME:dryrun"
|
||||
else
|
||||
docker push $_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION
|
||||
docker push $_AZ_REGISTRY/$PROJECT_NAME:latest
|
||||
docker push "$_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION"
|
||||
docker push "$_AZ_REGISTRY/$PROJECT_NAME:latest"
|
||||
fi
|
||||
|
||||
- name: Log out of Docker
|
||||
|
||||
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@@ -40,6 +40,9 @@ jobs:
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check release version
|
||||
id: version
|
||||
@@ -52,8 +55,8 @@ jobs:
|
||||
- name: Get branch name
|
||||
id: branch
|
||||
run: |
|
||||
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
BRANCH_NAME=$(basename "${GITHUB_REF}")
|
||||
echo "branch-name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
release:
|
||||
name: Create GitHub release
|
||||
|
||||
37
.github/workflows/repository-management.yml
vendored
37
.github/workflows/repository-management.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
BRANCH="hotfix-rc"
|
||||
fi
|
||||
|
||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
bump_version:
|
||||
name: Bump Version
|
||||
@@ -95,6 +95,7 @@ jobs:
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
persist-credentials: true
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
@@ -110,7 +111,7 @@ jobs:
|
||||
id: current-version
|
||||
run: |
|
||||
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
||||
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify input version
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
@@ -120,16 +121,15 @@ jobs:
|
||||
run: |
|
||||
# Error if version has not changed.
|
||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||
echo "Specified override version is the same as the current version." >> $GITHUB_STEP_SUMMARY
|
||||
echo "Specified override version is the same as the current version." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if version is newer.
|
||||
printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V
|
||||
if [ $? -eq 0 ]; then
|
||||
if printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V; then
|
||||
echo "Version is newer than the current version."
|
||||
else
|
||||
echo "Version is older than the current version." >> $GITHUB_STEP_SUMMARY
|
||||
echo "Version is older than the current version." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -160,15 +160,20 @@ jobs:
|
||||
id: set-final-version-output
|
||||
env:
|
||||
VERSION: ${{ inputs.version_number_override }}
|
||||
BUMP_VERSION_OVERRIDE_OUTCOME: ${{ steps.bump-version-override.outcome }}
|
||||
BUMP_VERSION_AUTOMATIC_OUTCOME: ${{ steps.bump-version-automatic.outcome }}
|
||||
CALCULATE_NEXT_VERSION: ${{ steps.calculate-next-version.outputs.version }}
|
||||
run: |
|
||||
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
|
||||
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
if [[ "${BUMP_VERSION_OVERRIDE_OUTCOME}" = "success" ]]; then
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${BUMP_VERSION_AUTOMATIC_OUTCOME}" = "success" ]]; then
|
||||
echo "version=${CALCULATE_NEXT_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Commit files
|
||||
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
|
||||
env:
|
||||
FINAL_VERSION: ${{ steps.set-final-version-output.outputs.version }}
|
||||
run: git commit -m "Bumped version to $FINAL_VERSION" -a
|
||||
|
||||
- name: Push changes
|
||||
run: git push
|
||||
@@ -213,13 +218,15 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.target_ref }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
persist-credentials: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
|
||||
env:
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
|
||||
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
|
||||
if [[ $(git ls-remote --heads origin "$BRANCH_NAME") ]]; then
|
||||
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -227,8 +234,8 @@ jobs:
|
||||
env:
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
git switch --quiet --create $BRANCH_NAME
|
||||
git push --quiet --set-upstream origin $BRANCH_NAME
|
||||
git switch --quiet --create "$BRANCH_NAME"
|
||||
git push --quiet --set-upstream origin "$BRANCH_NAME"
|
||||
|
||||
move_edd_db_scripts:
|
||||
name: Move EDD database scripts
|
||||
|
||||
12
.github/workflows/review-code.yml
vendored
12
.github/workflows/review-code.yml
vendored
@@ -26,14 +26,14 @@ jobs:
|
||||
id: check_changes
|
||||
run: |
|
||||
# Ensure we have the base branch
|
||||
git fetch origin ${{ github.base_ref }}
|
||||
git fetch origin "${GITHUB_BASE_REF}"
|
||||
|
||||
echo "Comparing changes between origin/${{ github.base_ref }} and HEAD"
|
||||
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
||||
echo "Comparing changes between origin/${GITHUB_BASE_REF} and HEAD"
|
||||
CHANGED_FILES=$(git diff --name-only "origin/${GITHUB_BASE_REF}...HEAD")
|
||||
|
||||
if [ -z "$CHANGED_FILES" ]; then
|
||||
echo "Zero files changed"
|
||||
echo "vault_team_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "vault_team_changes=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
if [ -z "$VAULT_PATTERNS" ]; then
|
||||
echo "⚠️ No patterns found for @bitwarden/team-vault-dev in CODEOWNERS"
|
||||
echo "vault_team_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "vault_team_changes=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
echo "vault_team_changes=$vault_team_changes" >> $GITHUB_OUTPUT
|
||||
echo "vault_team_changes=$vault_team_changes" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ "$vault_team_changes" = "true" ]; then
|
||||
echo ""
|
||||
|
||||
14
.github/workflows/test-database.yml
vendored
14
.github/workflows/test-database.yml
vendored
@@ -45,6 +45,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
@@ -139,26 +141,26 @@ jobs:
|
||||
|
||||
- name: Print MySQL Logs
|
||||
if: failure()
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=mysql")'
|
||||
run: 'docker logs "$(docker ps --quiet --filter "name=mysql")"'
|
||||
|
||||
- name: Print MariaDB Logs
|
||||
if: failure()
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=mariadb")'
|
||||
run: 'docker logs "$(docker ps --quiet --filter "name=mariadb")"'
|
||||
|
||||
- name: Print Postgres Logs
|
||||
if: failure()
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=postgres")'
|
||||
run: 'docker logs "$(docker ps --quiet --filter "name=postgres")"'
|
||||
|
||||
- name: Print MSSQL Logs
|
||||
if: failure()
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=mssql")'
|
||||
run: 'docker logs "$(docker ps --quiet --filter "name=mssql")"'
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
path: "**/*-test-results.trx"
|
||||
path: "./**/*-test-results.trx"
|
||||
reporter: dotnet-trx
|
||||
fail-on-error: true
|
||||
|
||||
@@ -177,6 +179,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -28,6 +28,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
|
||||
@@ -3,7 +3,7 @@ using Bit.Core.Billing.Pricing;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
namespace Bit.Api.Billing.Controllers;
|
||||
|
||||
[Route("plans")]
|
||||
[Authorize("Web")]
|
||||
@@ -18,4 +18,11 @@ public class PlansController(
|
||||
var responses = plans.Select(plan => new PlanResponseModel(plan));
|
||||
return new ListResponseModel<PlanResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("premium")]
|
||||
public async Task<IResult> GetPremiumPlanAsync()
|
||||
{
|
||||
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
|
||||
return TypedResults.Ok(premiumPlan);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -50,7 +51,8 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
ISubscriberService subscriberService,
|
||||
IUserService userService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
ILogger<CreatePremiumCloudHostedSubscriptionCommand> logger)
|
||||
ILogger<CreatePremiumCloudHostedSubscriptionCommand> logger,
|
||||
IPricingClient pricingClient)
|
||||
: BaseBillingCommand<CreatePremiumCloudHostedSubscriptionCommand>(logger), ICreatePremiumCloudHostedSubscriptionCommand
|
||||
{
|
||||
private static readonly List<string> _expand = ["tax"];
|
||||
@@ -255,11 +257,13 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
Customer customer,
|
||||
int? storage)
|
||||
{
|
||||
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Price = StripeConstants.Prices.PremiumAnnually,
|
||||
Price = premiumPlan.Seat.StripePriceId,
|
||||
Quantity = 1
|
||||
}
|
||||
};
|
||||
@@ -268,7 +272,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = StripeConstants.Prices.StoragePlanPersonal,
|
||||
Price = premiumPlan.Storage.StripePriceId,
|
||||
Quantity = storage
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Premium.Commands;
|
||||
|
||||
using static StripeConstants;
|
||||
|
||||
public interface IPreviewPremiumTaxCommand
|
||||
{
|
||||
Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(
|
||||
@@ -18,6 +16,7 @@ public interface IPreviewPremiumTaxCommand
|
||||
|
||||
public class PreviewPremiumTaxCommand(
|
||||
ILogger<PreviewPremiumTaxCommand> logger,
|
||||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter) : BaseBillingCommand<PreviewPremiumTaxCommand>(logger), IPreviewPremiumTaxCommand
|
||||
{
|
||||
public Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(
|
||||
@@ -25,6 +24,8 @@ public class PreviewPremiumTaxCommand(
|
||||
BillingAddress billingAddress)
|
||||
=> HandleAsync<(decimal, decimal)>(async () =>
|
||||
{
|
||||
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var options = new InvoiceCreatePreviewOptions
|
||||
{
|
||||
AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true },
|
||||
@@ -41,7 +42,7 @@ public class PreviewPremiumTaxCommand(
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new InvoiceSubscriptionDetailsItemOptions { Price = Prices.PremiumAnnually, Quantity = 1 }
|
||||
new InvoiceSubscriptionDetailsItemOptions { Price = premiumPlan.Seat.StripePriceId, Quantity = 1 }
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -50,7 +51,7 @@ public class PreviewPremiumTaxCommand(
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Price = Prices.StoragePlanPersonal,
|
||||
Price = premiumPlan.Storage.StripePriceId,
|
||||
Quantity = additionalStorage
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Billing.Pricing;
|
||||
|
||||
using OrganizationPlan = Plan;
|
||||
using PremiumPlan = Premium.Plan;
|
||||
|
||||
public interface IPricingClient
|
||||
{
|
||||
// TODO: Rename with Organization focus.
|
||||
/// <summary>
|
||||
/// Retrieve a Bitwarden plan by its <paramref name="planType"/>. If the feature flag 'use-pricing-service' is enabled,
|
||||
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
|
||||
@@ -16,8 +18,9 @@ public interface IPricingClient
|
||||
/// <param name="planType">The type of plan to retrieve.</param>
|
||||
/// <returns>A Bitwarden <see cref="Plan"/> record or null in the case the plan could not be found or the method was executed from a self-hosted instance.</returns>
|
||||
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
|
||||
Task<Plan?> GetPlan(PlanType planType);
|
||||
Task<OrganizationPlan?> GetPlan(PlanType planType);
|
||||
|
||||
// TODO: Rename with Organization focus.
|
||||
/// <summary>
|
||||
/// Retrieve a Bitwarden plan by its <paramref name="planType"/>. If the feature flag 'use-pricing-service' is enabled,
|
||||
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
|
||||
@@ -26,13 +29,17 @@ public interface IPricingClient
|
||||
/// <returns>A Bitwarden <see cref="Plan"/> record.</returns>
|
||||
/// <exception cref="NotFoundException">Thrown when the <see cref="Plan"/> for the provided <paramref name="planType"/> could not be found or the method was executed from a self-hosted instance.</exception>
|
||||
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
|
||||
Task<Plan> GetPlanOrThrow(PlanType planType);
|
||||
Task<OrganizationPlan> GetPlanOrThrow(PlanType planType);
|
||||
|
||||
// TODO: Rename with Organization focus.
|
||||
/// <summary>
|
||||
/// Retrieve all the Bitwarden plans. If the feature flag 'use-pricing-service' is enabled,
|
||||
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
|
||||
/// </summary>
|
||||
/// <returns>A list of Bitwarden <see cref="Plan"/> records or an empty list in the case the method is executed from a self-hosted instance.</returns>
|
||||
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
|
||||
Task<List<Plan>> ListPlans();
|
||||
Task<List<OrganizationPlan>> ListPlans();
|
||||
|
||||
Task<PremiumPlan> GetAvailablePremiumPlan();
|
||||
Task<List<PremiumPlan>> ListPremiumPlans();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.Billing.Pricing.Models;
|
||||
namespace Bit.Core.Billing.Pricing.Organizations;
|
||||
|
||||
public class Feature
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.Billing.Pricing.Models;
|
||||
namespace Bit.Core.Billing.Pricing.Organizations;
|
||||
|
||||
public class Plan
|
||||
{
|
||||
@@ -1,8 +1,6 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing.Models;
|
||||
using Plan = Bit.Core.Billing.Pricing.Models.Plan;
|
||||
|
||||
namespace Bit.Core.Billing.Pricing;
|
||||
namespace Bit.Core.Billing.Pricing.Organizations;
|
||||
|
||||
public record PlanAdapter : Core.Models.StaticStore.Plan
|
||||
{
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using OneOf;
|
||||
|
||||
namespace Bit.Core.Billing.Pricing.Models;
|
||||
namespace Bit.Core.Billing.Pricing.Organizations;
|
||||
|
||||
[JsonConverter(typeof(PurchasableJsonConverter))]
|
||||
public class Purchasable(OneOf<Free, Packaged, Scalable> input) : OneOfBase<Free, Packaged, Scalable>(input)
|
||||
10
src/Core/Billing/Pricing/Premium/Plan.cs
Normal file
10
src/Core/Billing/Pricing/Premium/Plan.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Bit.Core.Billing.Pricing.Premium;
|
||||
|
||||
public class Plan
|
||||
{
|
||||
public string Name { get; init; } = null!;
|
||||
public int? LegacyYear { get; init; }
|
||||
public bool Available { get; init; }
|
||||
public Purchasable Seat { get; init; } = null!;
|
||||
public Purchasable Storage { get; init; } = null!;
|
||||
}
|
||||
7
src/Core/Billing/Pricing/Premium/Purchasable.cs
Normal file
7
src/Core/Billing/Pricing/Premium/Purchasable.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Billing.Pricing.Premium;
|
||||
|
||||
public class Purchasable
|
||||
{
|
||||
public string StripePriceId { get; init; } = null!;
|
||||
public decimal Price { get; init; }
|
||||
}
|
||||
@@ -1,24 +1,27 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing.Organizations;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Plan = Bit.Core.Models.StaticStore.Plan;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Billing.Pricing;
|
||||
|
||||
using OrganizationPlan = Bit.Core.Models.StaticStore.Plan;
|
||||
using PremiumPlan = Premium.Plan;
|
||||
using Purchasable = Premium.Purchasable;
|
||||
|
||||
public class PricingClient(
|
||||
IFeatureService featureService,
|
||||
GlobalSettings globalSettings,
|
||||
HttpClient httpClient,
|
||||
ILogger<PricingClient> logger) : IPricingClient
|
||||
{
|
||||
public async Task<Plan?> GetPlan(PlanType planType)
|
||||
public async Task<OrganizationPlan?> GetPlan(PlanType planType)
|
||||
{
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
@@ -40,16 +43,14 @@ public class PricingClient(
|
||||
return null;
|
||||
}
|
||||
|
||||
var response = await httpClient.GetAsync($"plans/lookup/{lookupKey}");
|
||||
var response = await httpClient.GetAsync($"plans/organization/{lookupKey}");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var plan = await response.Content.ReadFromJsonAsync<Models.Plan>();
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
|
||||
}
|
||||
return new PlanAdapter(plan);
|
||||
var plan = await response.Content.ReadFromJsonAsync<Plan>();
|
||||
return plan == null
|
||||
? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null")
|
||||
: new PlanAdapter(plan);
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
@@ -62,19 +63,14 @@ public class PricingClient(
|
||||
message: $"Request to the Pricing Service failed with status code {response.StatusCode}");
|
||||
}
|
||||
|
||||
public async Task<Plan> GetPlanOrThrow(PlanType planType)
|
||||
public async Task<OrganizationPlan> GetPlanOrThrow(PlanType planType)
|
||||
{
|
||||
var plan = await GetPlan(planType);
|
||||
|
||||
if (plan == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return plan;
|
||||
return plan ?? throw new NotFoundException($"Could not find plan for type {planType}");
|
||||
}
|
||||
|
||||
public async Task<List<Plan>> ListPlans()
|
||||
public async Task<List<OrganizationPlan>> ListPlans()
|
||||
{
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
@@ -88,16 +84,51 @@ public class PricingClient(
|
||||
return StaticStore.Plans.ToList();
|
||||
}
|
||||
|
||||
var response = await httpClient.GetAsync("plans");
|
||||
var response = await httpClient.GetAsync("plans/organization");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var plans = await response.Content.ReadFromJsonAsync<List<Models.Plan>>();
|
||||
if (plans == null)
|
||||
{
|
||||
throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
|
||||
}
|
||||
return plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList();
|
||||
var plans = await response.Content.ReadFromJsonAsync<List<Plan>>();
|
||||
return plans == null
|
||||
? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null")
|
||||
: plans.Select(OrganizationPlan (plan) => new PlanAdapter(plan)).ToList();
|
||||
}
|
||||
|
||||
throw new BillingException(
|
||||
message: $"Request to the Pricing Service failed with status {response.StatusCode}");
|
||||
}
|
||||
|
||||
public async Task<PremiumPlan> GetAvailablePremiumPlan()
|
||||
{
|
||||
var premiumPlans = await ListPremiumPlans();
|
||||
|
||||
var availablePlan = premiumPlans.FirstOrDefault(premiumPlan => premiumPlan.Available);
|
||||
|
||||
return availablePlan ?? throw new NotFoundException("Could not find available premium plan");
|
||||
}
|
||||
|
||||
public async Task<List<PremiumPlan>> ListPremiumPlans()
|
||||
{
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
|
||||
var fetchPremiumPriceFromPricingService =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM26793_FetchPremiumPriceFromPricingService);
|
||||
|
||||
if (!usePricingService || !fetchPremiumPriceFromPricingService)
|
||||
{
|
||||
return [CurrentPremiumPlan];
|
||||
}
|
||||
|
||||
var response = await httpClient.GetAsync("plans/premium");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var plans = await response.Content.ReadFromJsonAsync<List<PremiumPlan>>();
|
||||
return plans ?? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
|
||||
}
|
||||
|
||||
throw new BillingException(
|
||||
@@ -130,4 +161,13 @@ public class PricingClient(
|
||||
PlanType.TeamsStarter2023 => "teams-starter-2023",
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static PremiumPlan CurrentPremiumPlan => new()
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new Purchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
|
||||
Storage = new Purchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -30,7 +31,8 @@ public class PremiumUserBillingService(
|
||||
ISetupIntentCache setupIntentCache,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService,
|
||||
IUserRepository userRepository) : IPremiumUserBillingService
|
||||
IUserRepository userRepository,
|
||||
IPricingClient pricingClient) : IPremiumUserBillingService
|
||||
{
|
||||
public async Task Credit(User user, decimal amount)
|
||||
{
|
||||
@@ -301,11 +303,13 @@ public class PremiumUserBillingService(
|
||||
Customer customer,
|
||||
int? storage)
|
||||
{
|
||||
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Price = StripeConstants.Prices.PremiumAnnually,
|
||||
Price = premiumPlan.Seat.StripePriceId,
|
||||
Quantity = 1
|
||||
}
|
||||
};
|
||||
@@ -314,7 +318,7 @@ public class PremiumUserBillingService(
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = StripeConstants.Prices.StoragePlanPersonal,
|
||||
Price = premiumPlan.Storage.StripePriceId,
|
||||
Quantity = storage
|
||||
});
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ public static class FeatureFlagKeys
|
||||
public const string DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods";
|
||||
public const string PM23174ManageAccountRecoveryPermissionDrivesTheNeedToSetMasterPassword =
|
||||
"pm-23174-manage-account-recovery-permission-drives-the-need-to-set-master-password";
|
||||
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
|
||||
|
||||
/* Autofill Team */
|
||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||
@@ -185,6 +186,7 @@ public static class FeatureFlagKeys
|
||||
public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button";
|
||||
public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog";
|
||||
public const string PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page";
|
||||
public const string PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service";
|
||||
|
||||
/* Key Management Team */
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
|
||||
@@ -0,0 +1,675 @@
|
||||
<!doctype html>
|
||||
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<title></title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a { padding:0; }
|
||||
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
||||
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
||||
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
||||
p { display:block;margin:13px 0; }
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
|
||||
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||
.mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
.mj-column-per-90 { width:90% !important; max-width: 90%; }
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||
.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
.moz-text-html .mj-column-per-90 { width:90% !important; max-width: 90%; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
<style type="text/css">
|
||||
|
||||
@media only screen and (max-width:479px) {
|
||||
table.mj-full-width-mobile { width: 100% !important; }
|
||||
td.mj-full-width-mobile { width: auto !important; }
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style type="text/css">
|
||||
.border-fix > table {
|
||||
border-collapse: separate !important;
|
||||
}
|
||||
.border-fix > table > tbody > tr > td {
|
||||
border-radius: 3px;
|
||||
}
|
||||
@media only screen and
|
||||
(max-width: 480px) { .hide-small-img { display: none !important; } .send-bubble { padding-left: 20px; padding-right: 20px; width: 90% !important; } }
|
||||
|
||||
</style>
|
||||
<!-- Responsive icon visibility -->
|
||||
</head>
|
||||
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
||||
|
||||
|
||||
<div class="border-fix" style="background-color:#e6e9ef;" lang="und" dir="auto">
|
||||
<!-- Blue Header Section -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="border-fix-outlook" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div class="border-fix" style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><![endif]-->
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#175ddc" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;border-radius:4px 4px 0px 0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:434px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:150px;">
|
||||
|
||||
<img alt src="https://bitwarden.com/images/logo-horizontal-white.png" style="border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;" width="150" height="30">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;"><h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
|
||||
Verify your email to access this Bitwarden Send
|
||||
</h1></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td><td class="" style="vertical-align:bottom;width:186px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:bottom;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center" class="hide-small-img" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:140px;">
|
||||
|
||||
<img alt src="https://assets.bitwarden.com/email/v1/spot-secure-send-round.png" style="border:0;display:block;outline:none;text-decoration:none;height:140px;width:100%;font-size:16px;" width="140" height="140">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Main Content -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:620px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align:top;padding:0px;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;">Your verification code is:</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:32px;line-height:1;text-align:left;color:#1B2029;"><b>{{Token}}</b></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="font-size:0px;word-break:break-word;">
|
||||
|
||||
<div style="height:20px;line-height:20px;"> </div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;">This code expires in {{Expiry}} minutes. After that, you'll need to
|
||||
verify your email again.</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px 0px 20px 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="send-bubble-outlook" style="vertical-align:top;width:558px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-90 mj-outlook-group-fix send-bubble" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align:top;padding:0px;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color:#DBE5F6;border-radius:16px;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;"><p>
|
||||
Bitwarden Send transmits sensitive, temporary information to
|
||||
others easily and securely. Learn more about
|
||||
<a href="https://bitwarden.com/help/send" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">Bitwarden Send</a>
|
||||
or
|
||||
<a href="https://bitwarden.com/signup" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">sign up</a>
|
||||
to try it today.
|
||||
</p></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Learn More Section -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#f6f6f6" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#f6f6f6;background-color:#f6f6f6;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f6f6f6;background-color:#f6f6f6;width:100%;border-radius:0px 0px 4px 4px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:406px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;"><h3 style="font-size: 20px; margin: 0; line-height: 28px">
|
||||
Learn more about Bitwarden
|
||||
</h3>
|
||||
Find user guides, product documentation, and videos on the
|
||||
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:174px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center" class="hide-small-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:94px;">
|
||||
|
||||
<img alt src="https://assets.bitwarden.com/email/v1/spot-community.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;" width="94" height="auto">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:660px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:0;word-break:break-word;">
|
||||
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://x.com/bitwarden" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-x.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-reddit.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://community.bitwarden.com/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-discourse.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://github.com/bitwarden" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-github.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-youtube.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-linkedin.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<a href="https://www.facebook.com/bitwarden/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-facebook.png" style="border-radius:3px;display:block;" width="30">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px">
|
||||
© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
|
||||
Barbara, CA, USA
|
||||
</p>
|
||||
<p style="margin-top: 5px">
|
||||
Always confirm you are on a trusted Bitwarden domain before logging
|
||||
in:<br>
|
||||
<a href="https://bitwarden.com/">bitwarden.com</a> |
|
||||
<a href="https://bitwarden.com/help/emails-from-bitwarden/">Learn why we include this</a>
|
||||
</p></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Verify your email to access this Bitwarden Send.
|
||||
|
||||
Your verification code is: {{Token}}
|
||||
|
||||
This code can only be used once and expires in {{Expiry}} minutes. After that you'll need to verify your email again.
|
||||
|
||||
Bitwarden Send transmits sensitive, temporary information to others easily and securely. Learn more about Bitwarden Send or sign up to try it today.
|
||||
{{/BasicTextLayout}}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"packages": [
|
||||
"components/hero"
|
||||
"components/mj-bw-hero"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,38 +2,38 @@
|
||||
<mj-column>
|
||||
<mj-social icon-size="30px" inner-padding="10px" padding="0">
|
||||
<mj-social-element
|
||||
href="https://twitter.com/bitwarden"
|
||||
src="https://bitwarden.com/images/mail-twitter.png"
|
||||
href="https://x.com/bitwarden"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-x.png"
|
||||
></mj-social-element>
|
||||
|
||||
<mj-social-element
|
||||
href="https://www.reddit.com/r/Bitwarden/"
|
||||
src="https://bitwarden.com/images/mail-reddit.png"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-reddit.png"
|
||||
></mj-social-element>
|
||||
|
||||
<mj-social-element
|
||||
href="https://community.bitwarden.com/"
|
||||
src="https://bitwarden.com/images/mail-discourse.png"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-discourse.png"
|
||||
></mj-social-element>
|
||||
|
||||
<mj-social-element
|
||||
href="https://github.com/bitwarden"
|
||||
src="https://bitwarden.com/images/mail-github.png"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-github.png"
|
||||
></mj-social-element>
|
||||
|
||||
<mj-social-element
|
||||
href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw"
|
||||
src="https://bitwarden.com/images/mail-youtube.png"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-youtube.png"
|
||||
></mj-social-element>
|
||||
|
||||
<mj-social-element
|
||||
href="https://www.linkedin.com/company/bitwarden1/"
|
||||
src="https://bitwarden.com/images/mail-linkedin.png"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-linkedin.png"
|
||||
></mj-social-element>
|
||||
|
||||
<mj-social-element
|
||||
href="https://www.facebook.com/bitwarden/"
|
||||
src="https://bitwarden.com/images/mail-facebook.png"
|
||||
src="https://assets.bitwarden.com/email/v1/mail-facebook.png"
|
||||
></mj-social-element>
|
||||
</mj-social>
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
<p style="margin-top: 5px">
|
||||
Always confirm you are on a trusted Bitwarden domain before logging
|
||||
in:<br />
|
||||
<a href="#">bitwarden.com</a> |
|
||||
<a href="#">Learn why we include this</a>
|
||||
<a href="https://bitwarden.com/">bitwarden.com</a> |
|
||||
<a href="https://bitwarden.com/help/emails-from-bitwarden/">Learn why we include this</a>
|
||||
</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
font-size="16px"
|
||||
/>
|
||||
<mj-button background-color="#175ddc" />
|
||||
<mj-text color="#333" />
|
||||
<mj-text color="#1B2029" />
|
||||
<mj-body background-color="#e6e9ef" width="660px" />
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
@@ -22,3 +22,9 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
</mj-style>
|
||||
|
||||
<!-- Responsive icon visibility -->
|
||||
<mj-style>
|
||||
@media only screen and
|
||||
(max-width: 480px) { .hide-small-img { display: none !important; } .send-bubble { padding-left: 20px; padding-right: 20px; width: 90% !important; } }
|
||||
</mj-style>
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
const { BodyComponent } = require("mjml-core");
|
||||
class MjBwHero extends BodyComponent {
|
||||
static dependencies = {
|
||||
// Tell the validator which tags are allowed as our component's parent
|
||||
"mj-column": ["mj-bw-hero"],
|
||||
"mj-wrapper": ["mj-bw-hero"],
|
||||
// Tell the validator which tags are allowed as our component's children
|
||||
"mj-bw-hero": [],
|
||||
};
|
||||
|
||||
static allowedAttributes = {
|
||||
"img-src": "string",
|
||||
title: "string",
|
||||
"button-text": "string",
|
||||
"button-url": "string",
|
||||
};
|
||||
|
||||
static defaultAttributes = {};
|
||||
|
||||
render() {
|
||||
return this.renderMJML(`
|
||||
<mj-section
|
||||
full-width="full-width"
|
||||
background-color="#175ddc"
|
||||
border-radius="4px 4px 0 0"
|
||||
>
|
||||
<mj-column width="70%">
|
||||
<mj-image
|
||||
align="left"
|
||||
src="https://bitwarden.com/images/logo-horizontal-white.png"
|
||||
width="150px"
|
||||
height="30px"
|
||||
></mj-image>
|
||||
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
||||
<h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
|
||||
${this.getAttribute("title")}
|
||||
</h1>
|
||||
</mj-text>
|
||||
<mj-button
|
||||
href="${this.getAttribute("button-url")}"
|
||||
background-color="#fff"
|
||||
color="#1A41AC"
|
||||
border-radius="20px"
|
||||
align="left"
|
||||
>
|
||||
${this.getAttribute("button-text")}
|
||||
</mj-button
|
||||
>
|
||||
</mj-column>
|
||||
<mj-column width="30%" vertical-align="bottom">
|
||||
<mj-image
|
||||
src="${this.getAttribute("img-src")}"
|
||||
alt=""
|
||||
width="140px"
|
||||
height="140px"
|
||||
padding="0"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MjBwHero;
|
||||
@@ -0,0 +1,18 @@
|
||||
<mj-section border-radius="0px 0px 4px 4px" background-color="#f6f6f6" padding="5px 20px 10px 20px">
|
||||
<mj-column width="70%">
|
||||
<mj-text line-height="24px">
|
||||
<h3 style="font-size: 20px; margin: 0; line-height: 28px">
|
||||
Learn more about Bitwarden
|
||||
</h3>
|
||||
Find user guides, product documentation, and videos on the
|
||||
<a href="https://bitwarden.com/help/" class="link"> Bitwarden Help Center</a>.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="30%">
|
||||
<mj-image
|
||||
src="https://assets.bitwarden.com/email/v1/spot-community.png"
|
||||
css-class="hide-small-img"
|
||||
width="94px"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
100
src/Core/MailTemplates/Mjml/components/mj-bw-hero.js
Normal file
100
src/Core/MailTemplates/Mjml/components/mj-bw-hero.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const { BodyComponent } = require("mjml-core");
|
||||
class MjBwHero extends BodyComponent {
|
||||
static dependencies = {
|
||||
// Tell the validator which tags are allowed as our component's parent
|
||||
"mj-column": ["mj-bw-hero"],
|
||||
"mj-wrapper": ["mj-bw-hero"],
|
||||
// Tell the validator which tags are allowed as our component's children
|
||||
"mj-bw-hero": [],
|
||||
};
|
||||
|
||||
static allowedAttributes = {
|
||||
"img-src": "string", // REQUIRED: Source for the image displayed in the right-hand side of the blue header area
|
||||
title: "string", // REQUIRED: large text stating primary purpose of the email
|
||||
"button-text": "string", // OPTIONAL: text to display in the button
|
||||
"button-url": "string", // OPTIONAL: URL to navigate to when the button is clicked
|
||||
"sub-title": "string", // OPTIONAL: smaller text providing additional context for the title
|
||||
};
|
||||
|
||||
static defaultAttributes = {};
|
||||
|
||||
render() {
|
||||
if (this.getAttribute("button-text") && this.getAttribute("button-url")) {
|
||||
return this.renderMJML(`
|
||||
<mj-section
|
||||
full-width="full-width"
|
||||
background-color="#175ddc"
|
||||
border-radius="4px 4px 0px 0px"
|
||||
>
|
||||
<mj-column width="70%">
|
||||
<mj-image
|
||||
align="left"
|
||||
src="https://bitwarden.com/images/logo-horizontal-white.png"
|
||||
width="150px"
|
||||
height="30px"
|
||||
></mj-image>
|
||||
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
||||
<h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
|
||||
${this.getAttribute("title")}
|
||||
</h1>
|
||||
</mj-text>
|
||||
<mj-button
|
||||
href="${this.getAttribute("button-url")}"
|
||||
background-color="#fff"
|
||||
color="#1A41AC"
|
||||
border-radius="20px"
|
||||
align="left"
|
||||
>
|
||||
${this.getAttribute("button-text")}
|
||||
</mj-button
|
||||
>
|
||||
</mj-column>
|
||||
<mj-column width="30%" vertical-align="bottom">
|
||||
<mj-image
|
||||
src="${this.getAttribute("img-src")}"
|
||||
alt=""
|
||||
width="140px"
|
||||
height="140px"
|
||||
padding="0px"
|
||||
css-class="hide-small-img"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
`);
|
||||
} else {
|
||||
return this.renderMJML(`
|
||||
<mj-section
|
||||
full-width="full-width"
|
||||
background-color="#175ddc"
|
||||
border-radius="4px 4px 0px 0px"
|
||||
>
|
||||
<mj-column width="70%">
|
||||
<mj-image
|
||||
align="left"
|
||||
src="https://bitwarden.com/images/logo-horizontal-white.png"
|
||||
width="150px"
|
||||
height="30px"
|
||||
></mj-image>
|
||||
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
||||
<h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
|
||||
${this.getAttribute("title")}
|
||||
</h1>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="30%" vertical-align="bottom">
|
||||
<mj-image
|
||||
src="${this.getAttribute("img-src")}"
|
||||
alt=""
|
||||
width="140px"
|
||||
height="140px"
|
||||
padding="0px"
|
||||
css-class="hide-small-img"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MjBwHero;
|
||||
64
src/Core/MailTemplates/Mjml/emails/Auth/send-email-otp.mjml
Normal file
64
src/Core/MailTemplates/Mjml/emails/Auth/send-email-otp.mjml
Normal file
@@ -0,0 +1,64 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-include path="../../components/head.mjml" />
|
||||
<mj-style> </mj-style>
|
||||
</mj-head>
|
||||
|
||||
<mj-body css-class="border-fix">
|
||||
<!-- Blue Header Section -->
|
||||
<mj-wrapper css-class="border-fix" padding="20px 20px 10px 20px">
|
||||
<mj-bw-hero
|
||||
img-src="https://assets.bitwarden.com/email/v1/spot-secure-send-round.png"
|
||||
title="Verify your email to access this Bitwarden Send"
|
||||
/>
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Main Content -->
|
||||
<mj-wrapper padding="5px 20px 10px 20px">
|
||||
<mj-section background-color="#fff">
|
||||
<mj-column padding="0px">
|
||||
<mj-text> Your verification code is: </mj-text>
|
||||
<mj-text font-size="32px"> <b>{{Token}}</b> </mj-text>
|
||||
<mj-spacer height="20px" />
|
||||
<mj-text>
|
||||
This code expires in {{Expiry}} minutes. After that, you'll need to
|
||||
verify your email again.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section
|
||||
background-color="#fff"
|
||||
padding="0px 0px 20px 0px"
|
||||
>
|
||||
<mj-column
|
||||
css-class="send-bubble"
|
||||
width="90%"
|
||||
inner-background-color="#DBE5F6"
|
||||
inner-border-radius="16px"
|
||||
padding="0px"
|
||||
>
|
||||
<mj-text>
|
||||
<p>
|
||||
Bitwarden Send transmits sensitive, temporary information to
|
||||
others easily and securely. Learn more about
|
||||
<a href="https://bitwarden.com/help/send" class="link"
|
||||
>Bitwarden Send</a
|
||||
>
|
||||
or
|
||||
<a href="https://bitwarden.com/signup" class="link">sign up</a>
|
||||
to try it today.
|
||||
</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Learn More Section -->
|
||||
<mj-wrapper padding="5px 20px 10px 20px">
|
||||
<mj-include path="../../components/learn-more-footer.mjml" />
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Footer -->
|
||||
<mj-include path="../../components/footer.mjml" />
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -1,10 +1,10 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-include path="../components/head.mjml" />
|
||||
<mj-include path="../../components/head.mjml" />
|
||||
</mj-head>
|
||||
|
||||
<mj-body background-color="#f6f6f6">
|
||||
<mj-include path="../components/logo.mjml" />
|
||||
<mj-include path="../../components/logo.mjml" />
|
||||
|
||||
<mj-wrapper
|
||||
background-color="#fff"
|
||||
@@ -24,6 +24,6 @@
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<mj-include path="../components/footer.mjml" />
|
||||
<mj-include path="../../components/footer.mjml" />
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -22,26 +22,7 @@
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section background-color="#fbfbfb">
|
||||
<mj-column width="70%">
|
||||
<mj-text line-height="24px">
|
||||
<h3 style="font-size: 20px; margin: 0; line-height: 28px">
|
||||
We’re here for you!
|
||||
</h3>
|
||||
If you have any questions, search the Bitwarden
|
||||
<a href="#" class="link">Help</a>
|
||||
site or
|
||||
<a href="#" class="link">contact us</a>.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="30%">
|
||||
<mj-image
|
||||
src="https://assets.bitwarden.com/email/v1/chat.png"
|
||||
height="77px"
|
||||
width="94px"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-include path="../components/learn-more-footer.mjml" />
|
||||
</mj-wrapper>
|
||||
|
||||
<mj-include path="../components/footer.mjml" />
|
||||
|
||||
@@ -9,4 +9,5 @@ public class DefaultEmailOtpViewModel : BaseMailModel
|
||||
public string? TheDate { get; set; }
|
||||
public string? TheTime { get; set; }
|
||||
public string? TimeZone { get; set; }
|
||||
public string? Expiry { get; set; }
|
||||
}
|
||||
|
||||
@@ -226,7 +226,11 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
// Check minimum seats currently in use by the organization
|
||||
if (organization.SmSeats.Value > update.SmSeats.Value)
|
||||
{
|
||||
// Retrieve the number of currently occupied Secrets Manager seats for the organization.
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
|
||||
|
||||
// Check if the occupied number of seats exceeds the updated seat count.
|
||||
// If so, throw an exception indicating that the subscription cannot be decreased below the current usage.
|
||||
if (occupiedSeats > update.SmSeats.Value)
|
||||
{
|
||||
throw new BadRequestException($"{occupiedSeats} users are currently occupying Secrets Manager seats. " +
|
||||
@@ -412,7 +416,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests the number of Secret Manager seats and service accounts are currently used by the organization
|
||||
/// Requests the number of Secret Manager seats and service accounts currently used by the organization
|
||||
/// </summary>
|
||||
/// <param name="organizationId"> The id of the organization</param>
|
||||
/// <returns > A tuple containing the occupied seats and the occupied service account counts</returns>
|
||||
|
||||
@@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
@@ -31,6 +32,16 @@ public interface IMailService
|
||||
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
||||
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose);
|
||||
Task SendSendEmailOtpEmailAsync(string email, string token, string subject);
|
||||
/// <summary>
|
||||
/// <see cref="DefaultOtpTokenProviderOptions"/> has a default expiry of 5 minutes so we set the expiry to that value int he view model.
|
||||
/// Sends OTP code token to the specified email address.
|
||||
/// will replace <see cref="SendSendEmailOtpEmailAsync"/> when MJML templates are fully accepted.
|
||||
/// </summary>
|
||||
/// <param name="email">Email address to send the OTP to</param>
|
||||
/// <param name="token">Otp code token</param>
|
||||
/// <param name="subject">subject line of the email</param>
|
||||
/// <returns>Task</returns>
|
||||
Task SendSendEmailOtpEmailv2Async(string email, string token, string subject);
|
||||
Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType type, DateTime utcNow, string ip);
|
||||
Task SendNoMasterPasswordHintEmailAsync(string email);
|
||||
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
||||
|
||||
@@ -224,6 +224,27 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendSendEmailOtpEmailv2Async(string email, string token, string subject)
|
||||
{
|
||||
var message = CreateDefaultMessage(subject, email);
|
||||
var requestDateTime = DateTime.UtcNow;
|
||||
var model = new DefaultEmailOtpViewModel
|
||||
{
|
||||
Token = token,
|
||||
Expiry = "5", // This should be configured through the OTPDefaultTokenProviderOptions but for now we will hardcode it to 5 minutes.
|
||||
TheDate = requestDateTime.ToLongDateString(),
|
||||
TheTime = requestDateTime.ToShortTimeString(),
|
||||
TimeZone = _utcTimeZoneDisplay,
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName,
|
||||
};
|
||||
await AddMessageContentAsync(message, "Auth.SendAccessEmailOtpEmailv2", model);
|
||||
message.MetaData.Add("SendGridBypassListManagement", true);
|
||||
// TODO - PM-25380 change to string constant
|
||||
message.Category = "SendEmailOtp";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
|
||||
{
|
||||
// Check if we've sent this email within the last hour
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Billing.Tax.Responses;
|
||||
@@ -896,11 +897,14 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete($"Use {nameof(PreviewPremiumTaxCommand)} instead.")]
|
||||
public async Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(
|
||||
PreviewIndividualInvoiceRequestBody parameters,
|
||||
string gatewayCustomerId,
|
||||
string gatewaySubscriptionId)
|
||||
{
|
||||
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var options = new InvoiceCreatePreviewOptions
|
||||
{
|
||||
AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true, },
|
||||
@@ -909,8 +913,17 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = StripeConstants.Prices.PremiumAnnually },
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.AdditionalStorage, Plan = StripeConstants.Prices.StoragePlanPersonal }
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = 1,
|
||||
Plan = premiumPlan.Seat.StripePriceId
|
||||
},
|
||||
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.PasswordManager.AdditionalStorage,
|
||||
Plan = premiumPlan.Storage.StripePriceId
|
||||
}
|
||||
]
|
||||
},
|
||||
CustomerDetails = new InvoiceCustomerDetailsOptions
|
||||
@@ -1028,7 +1041,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new()
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.PasswordManager.AdditionalStorage,
|
||||
Plan = plan.PasswordManager.StripeStoragePlanId
|
||||
@@ -1049,7 +1062,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(parameters.PasswordManager.SponsoredPlan.Value);
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new() { Quantity = 1, Plan = sponsoredPlan.StripePlanId }
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = sponsoredPlan.StripePlanId }
|
||||
);
|
||||
}
|
||||
else
|
||||
@@ -1057,13 +1070,13 @@ public class StripePaymentService : IPaymentService
|
||||
if (plan.PasswordManager.HasAdditionalSeatsOption)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new() { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId }
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new() { Quantity = 1, Plan = plan.PasswordManager.StripePlanId }
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = plan.PasswordManager.StripePlanId }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1071,7 +1084,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
if (plan.SecretsManager.HasAdditionalSeatsOption)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new()
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.SecretsManager?.Seats ?? 0,
|
||||
Plan = plan.SecretsManager.StripeSeatPlanId
|
||||
@@ -1080,7 +1093,7 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
if (plan.SecretsManager.HasAdditionalServiceAccountOption)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new()
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.SecretsManager?.AdditionalMachineAccounts ?? 0,
|
||||
Plan = plan.SecretsManager.StripeServiceAccountPlanId
|
||||
|
||||
@@ -14,10 +14,10 @@ using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
@@ -72,6 +72,7 @@ public class UserService : UserManager<User>, IUserService
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IDistributedCache _distributedCache;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public UserService(
|
||||
IUserRepository userRepository,
|
||||
@@ -106,7 +107,8 @@ public class UserService : UserManager<User>, IUserService
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IDistributedCache distributedCache,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IPricingClient pricingClient)
|
||||
: base(
|
||||
store,
|
||||
optionsAccessor,
|
||||
@@ -146,6 +148,7 @@ public class UserService : UserManager<User>, IUserService
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_distributedCache = distributedCache;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
||||
@@ -972,8 +975,9 @@ public class UserService : UserManager<User>, IUserService
|
||||
throw new BadRequestException("Not a premium user.");
|
||||
}
|
||||
|
||||
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb,
|
||||
StripeConstants.Prices.StoragePlanPersonal);
|
||||
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, premiumPlan.Storage.StripePriceId);
|
||||
await SaveUserAsync(user);
|
||||
return secret;
|
||||
}
|
||||
|
||||
@@ -98,6 +98,11 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendSendEmailOtpEmailv2Async(string email, string token, string subject)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Services;
|
||||
@@ -10,6 +11,7 @@ using Duende.IdentityServer.Validation;
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
|
||||
public class SendEmailOtpRequestValidator(
|
||||
IFeatureService featureService,
|
||||
IOtpTokenProvider<DefaultOtpTokenProviderOptions> otpTokenProvider,
|
||||
IMailService mailService) : ISendAuthenticationMethodValidator<EmailOtp>
|
||||
{
|
||||
@@ -60,11 +62,20 @@ public class SendEmailOtpRequestValidator(
|
||||
{
|
||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.OtpGenerationFailed);
|
||||
}
|
||||
|
||||
await mailService.SendSendEmailOtpEmailAsync(
|
||||
email,
|
||||
token,
|
||||
string.Format(SendAccessConstants.OtpEmail.Subject, token));
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.MJMLBasedEmailTemplates))
|
||||
{
|
||||
await mailService.SendSendEmailOtpEmailv2Async(
|
||||
email,
|
||||
token,
|
||||
string.Format(SendAccessConstants.OtpEmail.Subject, token));
|
||||
}
|
||||
else
|
||||
{
|
||||
await mailService.SendSendEmailOtpEmailAsync(
|
||||
email,
|
||||
token,
|
||||
string.Format(SendAccessConstants.OtpEmail.Subject, token));
|
||||
}
|
||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Platform.Push;
|
||||
@@ -14,6 +16,8 @@ using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
using Address = Stripe.Address;
|
||||
using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;
|
||||
using PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;
|
||||
using StripeCustomer = Stripe.Customer;
|
||||
using StripeSubscription = Stripe.Subscription;
|
||||
|
||||
@@ -28,6 +32,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
|
||||
private readonly IUserService _userService = Substitute.For<IUserService>();
|
||||
private readonly IPushNotificationService _pushNotificationService = Substitute.For<IPushNotificationService>();
|
||||
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
|
||||
private readonly CreatePremiumCloudHostedSubscriptionCommand _command;
|
||||
|
||||
public CreatePremiumCloudHostedSubscriptionCommandTests()
|
||||
@@ -36,6 +41,17 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
baseServiceUri.CloudRegion.Returns("US");
|
||||
_globalSettings.BaseServiceUri.Returns(baseServiceUri);
|
||||
|
||||
// Setup default premium plan with standard pricing
|
||||
var premiumPlan = new PremiumPlan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
|
||||
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal }
|
||||
};
|
||||
_pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);
|
||||
|
||||
_command = new CreatePremiumCloudHostedSubscriptionCommand(
|
||||
_braintreeGateway,
|
||||
_globalSettings,
|
||||
@@ -44,7 +60,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
_subscriberService,
|
||||
_userService,
|
||||
_pushNotificationService,
|
||||
Substitute.For<ILogger<CreatePremiumCloudHostedSubscriptionCommand>>());
|
||||
Substitute.For<ILogger<CreatePremiumCloudHostedSubscriptionCommand>>(),
|
||||
_pricingClient);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
||||
@@ -1,23 +1,38 @@
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
using static Bit.Core.Billing.Constants.StripeConstants;
|
||||
using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;
|
||||
using PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Premium.Commands;
|
||||
|
||||
public class PreviewPremiumTaxCommandTests
|
||||
{
|
||||
private readonly ILogger<PreviewPremiumTaxCommand> _logger = Substitute.For<ILogger<PreviewPremiumTaxCommand>>();
|
||||
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly PreviewPremiumTaxCommand _command;
|
||||
|
||||
public PreviewPremiumTaxCommandTests()
|
||||
{
|
||||
_command = new PreviewPremiumTaxCommand(_logger, _stripeAdapter);
|
||||
// Setup default premium plan with standard pricing
|
||||
var premiumPlan = new PremiumPlan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = Prices.PremiumAnnually },
|
||||
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }
|
||||
};
|
||||
_pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);
|
||||
|
||||
_command = new PreviewPremiumTaxCommand(_logger, _pricingClient, _stripeAdapter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -278,21 +278,27 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const int seatCount = 10;
|
||||
var existingSeatCount = 9;
|
||||
|
||||
// Make sure Password Manager seats is greater or equal to Secrets Manager seats
|
||||
organization.Seats = seatCount;
|
||||
const int initialSeatCount = 9;
|
||||
const int maxSeatCount = 20;
|
||||
// This represents the total number of users allowed in the organization.
|
||||
organization.Seats = maxSeatCount;
|
||||
// This represents the number of Secrets Manager users allowed in the organization.
|
||||
organization.SmSeats = initialSeatCount;
|
||||
// This represents the upper limit of Secrets Manager seats that can be automatically scaled.
|
||||
organization.MaxAutoscaleSmSeats = maxSeatCount;
|
||||
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
SmSeats = seatCount,
|
||||
MaxAutoscaleSmSeats = seatCount
|
||||
SmSeats = 8,
|
||||
MaxAutoscaleSmSeats = maxSeatCount
|
||||
};
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(existingSeatCount);
|
||||
.Returns(5);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateSubscriptionAsync(update);
|
||||
@@ -316,21 +322,29 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const int seatCount = 10;
|
||||
const int existingSeatCount = 10;
|
||||
var ownerDetailsList = new List<OrganizationUserUserDetails> { new() { Email = "owner@example.com" } };
|
||||
const int initialSeatCount = 5;
|
||||
const int maxSeatCount = 10;
|
||||
|
||||
// The amount of seats for users in an organization
|
||||
// This represents the total number of users allowed in the organization.
|
||||
organization.Seats = maxSeatCount;
|
||||
// This represents the number of Secrets Manager users allowed in the organization.
|
||||
organization.SmSeats = initialSeatCount;
|
||||
// This represents the upper limit of Secrets Manager seats that can be automatically scaled.
|
||||
organization.MaxAutoscaleSmSeats = maxSeatCount;
|
||||
|
||||
var ownerDetailsList = new List<OrganizationUserUserDetails> { new() { Email = "owner@example.com" } };
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
SmSeats = seatCount,
|
||||
MaxAutoscaleSmSeats = seatCount
|
||||
SmSeats = maxSeatCount,
|
||||
MaxAutoscaleSmSeats = maxSeatCount
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(existingSeatCount);
|
||||
.Returns(maxSeatCount);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByMinimumRoleAsync(organization.Id, OrganizationUserType.Owner)
|
||||
.Returns(ownerDetailsList);
|
||||
@@ -340,15 +354,14 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
|
||||
// Assert
|
||||
|
||||
// Currently being called once each for different validation methods
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(2)
|
||||
.Received(1)
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendSecretsManagerMaxSeatLimitReachedEmailAsync(Arg.Is(organization),
|
||||
Arg.Is(seatCount),
|
||||
Arg.Is(maxSeatCount),
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Contains(ownerDetailsList[0].Email)));
|
||||
}
|
||||
|
||||
|
||||
@@ -265,9 +265,10 @@ public class SendEmailOtpRequestValidatorTests
|
||||
// Arrange
|
||||
var otpTokenProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
|
||||
// Act
|
||||
var validator = new SendEmailOtpRequestValidator(otpTokenProvider, mailService);
|
||||
var validator = new SendEmailOtpRequestValidator(featureService, otpTokenProvider, mailService);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(validator);
|
||||
|
||||
Reference in New Issue
Block a user