mirror of
https://github.com/bitwarden/web
synced 2025-12-13 14:53:25 +00:00
Compare commits
10 Commits
mono-repo
...
update-sel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b295ce392 | ||
|
|
254f215efd | ||
|
|
74bd2a0884 | ||
|
|
c490b67f74 | ||
|
|
3fb6b36874 | ||
|
|
b9c31597a2 | ||
|
|
a6f41f9020 | ||
|
|
8add15eae9 | ||
|
|
9d2cfe4a3d | ||
|
|
dbd70f687d |
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
**/bin
|
||||||
|
**/obj
|
||||||
|
**/node_modules
|
||||||
@@ -218,7 +218,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo -e "\nBuilding Docker image"
|
echo -e "\nBuilding Docker image"
|
||||||
docker --version
|
docker --version
|
||||||
docker build -t bitwarden/web .
|
docker build -t bitwarden/web -f docker/Dockerfile .
|
||||||
|
|
||||||
- name: Tag rc branch
|
- name: Tag rc branch
|
||||||
if: github.ref == 'refs/heads/rc'
|
if: github.ref == 'refs/heads/rc'
|
||||||
@@ -340,7 +340,7 @@ jobs:
|
|||||||
|
|
||||||
echo -e "\nBuilding Docker image"
|
echo -e "\nBuilding Docker image"
|
||||||
docker --version
|
docker --version
|
||||||
docker build -t bitwardenqa.azurecr.io/web .
|
docker build -t bitwardenqa.azurecr.io/web -f docker/Dockerfile-QA .
|
||||||
|
|
||||||
- name: Get image tag
|
- name: Get image tag
|
||||||
id: image-tag
|
id: image-tag
|
||||||
@@ -19,8 +19,8 @@ jobs:
|
|||||||
name: Setup
|
name: Setup
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
outputs:
|
outputs:
|
||||||
release_version: ${{ steps.version.outputs.version }}
|
release_version: ${{ steps.version.outputs.package }}
|
||||||
tag_version: ${{ steps.version.outputs.version }}
|
tag_version: ${{ steps.version.outputs.tag }}
|
||||||
branch_name: ${{ steps.branch.outputs.branch_name }}
|
branch_name: ${{ steps.branch.outputs.branch_name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Branch check
|
- name: Branch check
|
||||||
@@ -38,11 +38,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Check Release Version
|
- name: Check Release Version
|
||||||
id: version
|
id: version
|
||||||
uses: bitwarden/gh-actions/release-version-check@ea9fab01d76940267b4147cc1c4542431246b9f6
|
run: |
|
||||||
with:
|
version=$( jq -r ".version" package.json)
|
||||||
release-type: ${{ github.event.inputs.release_type }}
|
previous_release_tag_version=$(
|
||||||
project-type: ts
|
curl -sL https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r ".tag_name"
|
||||||
file: package.json
|
)
|
||||||
|
|
||||||
|
if [ "v$version" == "$previous_release_tag_version" ] && \
|
||||||
|
[ "${{ github.event.inputs.release_type }}" == "Initial Release" ]; then
|
||||||
|
echo "[!] Already released v$version. Please bump version to continue"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "::set-output name=package::$version"
|
||||||
|
echo "::set-output name=tag::v$version"
|
||||||
|
|
||||||
- name: Get branch name
|
- name: Get branch name
|
||||||
id: branch
|
id: branch
|
||||||
@@ -38,6 +38,12 @@ jobs:
|
|||||||
version: ${{ github.event.inputs.version_number }}
|
version: ${{ github.event.inputs.version_number }}
|
||||||
file_path: "./package-lock.json"
|
file_path: "./package-lock.json"
|
||||||
|
|
||||||
|
- name: Bump Version - csproj
|
||||||
|
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||||
|
with:
|
||||||
|
version: ${{ github.event.inputs.version_number }}
|
||||||
|
file_path: "./dotnet-src/Web/Web.csproj"
|
||||||
|
|
||||||
- name: Commit files
|
- name: Commit files
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
12
apps/web/.gitignore → .gitignore
vendored
12
apps/web/.gitignore → .gitignore
vendored
@@ -13,3 +13,15 @@ dist/
|
|||||||
build/
|
build/
|
||||||
!dev-server.shared.pem
|
!dev-server.shared.pem
|
||||||
config/local.json
|
config/local.json
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
build/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "jslib"]
|
||||||
|
path = jslib
|
||||||
|
url = https://github.com/bitwarden/jslib.git
|
||||||
|
branch = master
|
||||||
|
|||||||
0
apps/web/.husky/pre-commit → .husky/pre-commit
Normal file → Executable file
0
apps/web/.husky/pre-commit → .husky/pre-commit
Normal file → Executable file
@@ -2,7 +2,7 @@
|
|||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
|
|
||||||
#jslib
|
jslib
|
||||||
|
|
||||||
# External libraries / auto synced locales
|
# External libraries / auto synced locales
|
||||||
src/locales
|
src/locales
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
*
|
|
||||||
!build/*
|
|
||||||
!entrypoint.sh
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
FROM bitwarden/server
|
|
||||||
|
|
||||||
LABEL com.bitwarden.product="bitwarden"
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends \
|
|
||||||
gosu \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
ENV ASPNETCORE_URLS http://+:5000
|
|
||||||
WORKDIR /app
|
|
||||||
EXPOSE 5000
|
|
||||||
COPY ./build .
|
|
||||||
COPY entrypoint.sh /
|
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
|
|
||||||
HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Setup
|
|
||||||
|
|
||||||
GROUPNAME="bitwarden"
|
|
||||||
USERNAME="bitwarden"
|
|
||||||
|
|
||||||
LUID=${LOCAL_UID:-0}
|
|
||||||
LGID=${LOCAL_GID:-0}
|
|
||||||
|
|
||||||
# Step down from host root to well-known nobody/nogroup user
|
|
||||||
|
|
||||||
if [ $LUID -eq 0 ]
|
|
||||||
then
|
|
||||||
LUID=65534
|
|
||||||
fi
|
|
||||||
if [ $LGID -eq 0 ]
|
|
||||||
then
|
|
||||||
LGID=65534
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create user and group
|
|
||||||
|
|
||||||
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
|
||||||
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
|
||||||
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
|
||||||
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
|
||||||
mkhomedir_helper $USERNAME
|
|
||||||
|
|
||||||
# The rest...
|
|
||||||
|
|
||||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
|
||||||
cp /etc/bitwarden/web/app-id.json /app/app-id.json
|
|
||||||
chown -R $USERNAME:$GROUPNAME /app
|
|
||||||
chown -R $USERNAME:$GROUPNAME /bitwarden_server
|
|
||||||
|
|
||||||
exec gosu $USERNAME:$GROUPNAME dotnet /bitwarden_server/Server.dll \
|
|
||||||
/contentRoot=/app /webRoot=. /serveUnknown=false /webVault=true
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
|
||||||
import { NgModule } from "@angular/core";
|
|
||||||
|
|
||||||
import { SharedModule } from "../../shared.module";
|
|
||||||
|
|
||||||
import { EntityUsersComponent } from "./entity-users.component";
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [SharedModule, ScrollingModule],
|
|
||||||
declarations: [EntityUsersComponent],
|
|
||||||
exports: [EntityUsersComponent],
|
|
||||||
})
|
|
||||||
export class OrganizationManageModule {}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<div
|
|
||||||
class="modal fade"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="enrollMasterPasswordResetTitle"
|
|
||||||
>
|
|
||||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
|
||||||
<form
|
|
||||||
class="modal-content"
|
|
||||||
#form
|
|
||||||
(ngSubmit)="submit()"
|
|
||||||
[appApiAction]="formPromise"
|
|
||||||
ngNativeValidate
|
|
||||||
>
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 class="modal-title" id="enrollMasterPasswordResetTitle">
|
|
||||||
{{ (isEnrolled ? "withdrawPasswordReset" : "enrollPasswordReset") | i18n }}
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="close"
|
|
||||||
data-dismiss="modal"
|
|
||||||
appA11yTitle="{{ 'close' | i18n }}"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<app-callout type="warning" *ngIf="!isEnrolled">
|
|
||||||
{{ "resetPasswordEnrollmentWarning" | i18n }}
|
|
||||||
</app-callout>
|
|
||||||
<app-user-verification [(ngModel)]="verification" name="secret"> </app-user-verification>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button bitButton buttonType="primary" type="submit" [disabled]="form.loading">
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
*ngIf="form.loading"
|
|
||||||
></i>
|
|
||||||
<span>
|
|
||||||
{{ "submit" | i18n }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
bitButton
|
|
||||||
buttonType="secondary"
|
|
||||||
type="button"
|
|
||||||
data-dismiss="modal"
|
|
||||||
appA11yTitle="{{ 'close' | i18n }}"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{{ "cancel" | i18n }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { Component } from "@angular/core";
|
|
||||||
|
|
||||||
import { ModalRef } from "jslib-angular/components/modal/modal.ref";
|
|
||||||
import { ModalConfig } from "jslib-angular/services/modal.service";
|
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
|
||||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
|
||||||
import { LogService } from "jslib-common/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
|
||||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
|
||||||
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
|
|
||||||
import { Utils } from "jslib-common/misc/utils";
|
|
||||||
import { Organization } from "jslib-common/models/domain/organization";
|
|
||||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest";
|
|
||||||
import { Verification } from "jslib-common/types/verification";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-enroll-master-password-reset",
|
|
||||||
templateUrl: "enroll-master-password-reset.component.html",
|
|
||||||
})
|
|
||||||
export class EnrollMasterPasswordReset {
|
|
||||||
organization: Organization;
|
|
||||||
|
|
||||||
verification: Verification;
|
|
||||||
formPromise: Promise<any>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private userVerificationService: UserVerificationService,
|
|
||||||
private apiService: ApiService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private syncService: SyncService,
|
|
||||||
private logService: LogService,
|
|
||||||
private modalRef: ModalRef,
|
|
||||||
config: ModalConfig
|
|
||||||
) {
|
|
||||||
this.organization = config.data.organization;
|
|
||||||
}
|
|
||||||
|
|
||||||
async submit() {
|
|
||||||
let toastStringRef = "withdrawPasswordResetSuccess";
|
|
||||||
|
|
||||||
this.formPromise = this.userVerificationService
|
|
||||||
.buildRequest(this.verification, OrganizationUserResetPasswordEnrollmentRequest)
|
|
||||||
.then(async (request) => {
|
|
||||||
// Set variables
|
|
||||||
let keyString: string = null;
|
|
||||||
|
|
||||||
// Enrolling
|
|
||||||
if (!this.organization.resetPasswordEnrolled) {
|
|
||||||
// Retrieve Public Key
|
|
||||||
const orgKeys = await this.apiService.getOrganizationKeys(this.organization.id);
|
|
||||||
if (orgKeys == null) {
|
|
||||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const publicKey = Utils.fromB64ToArray(orgKeys.publicKey);
|
|
||||||
|
|
||||||
// RSA Encrypt user's encKey.key with organization public key
|
|
||||||
const encKey = await this.cryptoService.getEncKey();
|
|
||||||
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
|
|
||||||
keyString = encryptedKey.encryptedString;
|
|
||||||
toastStringRef = "enrollPasswordResetSuccess";
|
|
||||||
|
|
||||||
// Create request and execute enrollment
|
|
||||||
request.resetPasswordKey = keyString;
|
|
||||||
await this.apiService.putOrganizationUserResetPasswordEnrollment(
|
|
||||||
this.organization.id,
|
|
||||||
this.organization.userId,
|
|
||||||
request
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Withdrawal
|
|
||||||
request.resetPasswordKey = keyString;
|
|
||||||
await this.apiService.putOrganizationUserResetPasswordEnrollment(
|
|
||||||
this.organization.id,
|
|
||||||
this.organization.userId,
|
|
||||||
request
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.syncService.fullSync(true);
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await this.formPromise;
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t(toastStringRef));
|
|
||||||
this.modalRef.close();
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get isEnrolled(): boolean {
|
|
||||||
return this.organization.resetPasswordEnrolled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
|
||||||
import { NgModule } from "@angular/core";
|
|
||||||
|
|
||||||
import { LooseComponentsModule } from "../../loose-components.module";
|
|
||||||
import { SharedModule } from "../../shared.module";
|
|
||||||
|
|
||||||
import { EnrollMasterPasswordReset } from "./enroll-master-password-reset.component";
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [SharedModule, ScrollingModule, LooseComponentsModule],
|
|
||||||
declarations: [EnrollMasterPasswordReset],
|
|
||||||
exports: [EnrollMasterPasswordReset],
|
|
||||||
})
|
|
||||||
export class OrganizationUserModule {}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="billingSyncApiKeyTitle">
|
|
||||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
|
||||||
<form
|
|
||||||
class="modal-content"
|
|
||||||
#form
|
|
||||||
(ngSubmit)="submit()"
|
|
||||||
[appApiAction]="formPromise"
|
|
||||||
ngNativeValidate
|
|
||||||
>
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 class="modal-title" id="billingSyncApiKeyTitle">
|
|
||||||
{{ (hasBillingToken ? "viewBillingSyncToken" : "generateBillingSyncToken") | i18n }}
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="close"
|
|
||||||
data-dismiss="modal"
|
|
||||||
appA11yTitle="{{ 'close' | i18n }}"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<app-user-verification
|
|
||||||
[(ngModel)]="masterPassword"
|
|
||||||
ngDefaultControl
|
|
||||||
name="secret"
|
|
||||||
*ngIf="!clientSecret"
|
|
||||||
>
|
|
||||||
</app-user-verification>
|
|
||||||
<ng-container *ngIf="clientSecret && showRotateScreen">
|
|
||||||
<p>{{ "rotateBillingSyncTokenTitle" | i18n }}</p>
|
|
||||||
<app-callout type="warning">
|
|
||||||
{{ "rotateBillingSyncTokenWarning" | i18n }}
|
|
||||||
</app-callout>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<div *ngIf="clientSecret && !showRotateScreen">
|
|
||||||
<p>{{ "copyPasteBillingSync" | i18n }}</p>
|
|
||||||
<label for="clientSecret">Billing Sync Key</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input
|
|
||||||
id="clientSecret"
|
|
||||||
class="form-control text-monospace"
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="clientSecret"
|
|
||||||
name="clientSecret"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<div class="input-group-append">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
(click)="copy()"
|
|
||||||
[appA11yTitle]="'copy' | i18n"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="small text-muted mt-2" *ngIf="showLastSyncText">
|
|
||||||
<b class="font-weight-semibold">{{ "lastSync" | i18n }}:</b>
|
|
||||||
{{ lastSyncDate | date: "medium" }}
|
|
||||||
</div>
|
|
||||||
<div class="small text-danger mt-2" *ngIf="showAwaitingSyncText">
|
|
||||||
<i class="bwi bwi-error"></i>
|
|
||||||
{{
|
|
||||||
(daysBetween === 1 ? "awaitingSyncSingular" : "awaitingSyncPlural")
|
|
||||||
| i18n: daysBetween
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary btn-submit"
|
|
||||||
[disabled]="form.loading"
|
|
||||||
*ngIf="!clientSecret || showRotateScreen"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
*ngIf="form.loading"
|
|
||||||
></i>
|
|
||||||
<span>
|
|
||||||
{{ submitButtonText }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
data-dismiss="modal"
|
|
||||||
*ngIf="!showRotateScreen"
|
|
||||||
>
|
|
||||||
{{ "close" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
*ngIf="showRotateScreen"
|
|
||||||
(click)="cancelRotate()"
|
|
||||||
>
|
|
||||||
{{ "cancel" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
*ngIf="clientSecret && !showRotateScreen"
|
|
||||||
(click)="rotateToken()"
|
|
||||||
>
|
|
||||||
{{ "rotateToken" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import { Component } from "@angular/core";
|
|
||||||
|
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
|
||||||
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
|
|
||||||
import { OrganizationApiKeyType } from "jslib-common/enums/organizationApiKeyType";
|
|
||||||
import { OrganizationApiKeyRequest } from "jslib-common/models/request/organizationApiKeyRequest";
|
|
||||||
import { ApiKeyResponse } from "jslib-common/models/response/apiKeyResponse";
|
|
||||||
import { Verification } from "jslib-common/types/verification";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-billing-sync-api-key",
|
|
||||||
templateUrl: "billing-sync-api-key.component.html",
|
|
||||||
})
|
|
||||||
export class BillingSyncApiKeyComponent {
|
|
||||||
organizationId: string;
|
|
||||||
hasBillingToken: boolean;
|
|
||||||
|
|
||||||
showRotateScreen: boolean;
|
|
||||||
masterPassword: Verification;
|
|
||||||
formPromise: Promise<ApiKeyResponse>;
|
|
||||||
clientSecret?: string;
|
|
||||||
keyRevisionDate?: Date;
|
|
||||||
lastSyncDate?: Date = null;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private userVerificationService: UserVerificationService,
|
|
||||||
private apiService: ApiService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private i18nService: I18nService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
copy() {
|
|
||||||
this.platformUtilsService.copyToClipboard(this.clientSecret);
|
|
||||||
}
|
|
||||||
|
|
||||||
async submit() {
|
|
||||||
if (this.showRotateScreen) {
|
|
||||||
this.formPromise = this.userVerificationService
|
|
||||||
.buildRequest(this.masterPassword, OrganizationApiKeyRequest)
|
|
||||||
.then((request) => {
|
|
||||||
request.type = OrganizationApiKeyType.BillingSync;
|
|
||||||
return this.apiService.postOrganizationRotateApiKey(this.organizationId, request);
|
|
||||||
});
|
|
||||||
const response = await this.formPromise;
|
|
||||||
await this.load(response);
|
|
||||||
this.showRotateScreen = false;
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("billingSyncApiKeyRotated")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.formPromise = this.userVerificationService
|
|
||||||
.buildRequest(this.masterPassword, OrganizationApiKeyRequest)
|
|
||||||
.then((request) => {
|
|
||||||
request.type = OrganizationApiKeyType.BillingSync;
|
|
||||||
return this.apiService.postOrganizationApiKey(this.organizationId, request);
|
|
||||||
});
|
|
||||||
const response = await this.formPromise;
|
|
||||||
await this.load(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async load(response: ApiKeyResponse) {
|
|
||||||
this.clientSecret = response.apiKey;
|
|
||||||
this.keyRevisionDate = response.revisionDate;
|
|
||||||
this.hasBillingToken = true;
|
|
||||||
const syncStatus = await this.apiService.getSponsorshipSyncStatus(this.organizationId);
|
|
||||||
this.lastSyncDate = syncStatus.lastSyncDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelRotate() {
|
|
||||||
this.showRotateScreen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
rotateToken() {
|
|
||||||
this.showRotateScreen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private dayDiff(date1: Date, date2: Date): number {
|
|
||||||
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
|
||||||
return Math.round(diffTime / (1000 * 60 * 60 * 24));
|
|
||||||
}
|
|
||||||
|
|
||||||
get submitButtonText(): string {
|
|
||||||
if (this.showRotateScreen) {
|
|
||||||
return this.i18nService.t("rotateToken");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.i18nService.t(this.hasBillingToken ? "continue" : "generateToken");
|
|
||||||
}
|
|
||||||
|
|
||||||
get showLastSyncText(): boolean {
|
|
||||||
// If the keyRevisionDate is later than the lastSyncDate we need to show
|
|
||||||
// a warning that they need to put the billing sync key in their self hosted install
|
|
||||||
return this.lastSyncDate && this.lastSyncDate > this.keyRevisionDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
get showAwaitingSyncText(): boolean {
|
|
||||||
return this.lastSyncDate && this.lastSyncDate <= this.keyRevisionDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
get daysBetween(): number {
|
|
||||||
return this.dayDiff(this.keyRevisionDate, new Date());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="billingSyncTitle">
|
|
||||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
|
||||||
<form
|
|
||||||
class="modal-content"
|
|
||||||
#form
|
|
||||||
(ngSubmit)="submit()"
|
|
||||||
[appApiAction]="formPromise"
|
|
||||||
ngNativeValidate
|
|
||||||
>
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 class="modal-title" id="billingSyncTitle">{{ "manageBillingSync" | i18n }}</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="close"
|
|
||||||
data-dismiss="modal"
|
|
||||||
appA11yTitle="{{ 'close' | i18n }}"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>{{ "billingSyncKeyDesc" | i18n }}</p>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="billingSyncKey"
|
|
||||||
>{{ "billingSyncKey" | i18n }} <small>(</small><small>{{ "required" | i18n }}</small
|
|
||||||
><small>)</small></label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="billingSyncKey"
|
|
||||||
type="input"
|
|
||||||
name="billingSyncKey"
|
|
||||||
class="form-control"
|
|
||||||
[(ngModel)]="billingSyncKey"
|
|
||||||
required
|
|
||||||
appAutofocus
|
|
||||||
appInputVerbatim
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
|
||||||
<span>{{ "save" | i18n }}</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
|
||||||
{{ "cancel" | i18n }}
|
|
||||||
</button>
|
|
||||||
<div class="ml-auto">
|
|
||||||
<button
|
|
||||||
#deleteBtn
|
|
||||||
type="button"
|
|
||||||
(click)="deleteConnection()"
|
|
||||||
class="btn btn-outline-danger"
|
|
||||||
appA11yTitle="{{ 'delete' | i18n }}"
|
|
||||||
[disabled]="form.loading"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="form.loading" aria-hidden="true"></i>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
|
||||||
[hidden]="!form.loading"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Component } from "@angular/core";
|
|
||||||
|
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
|
||||||
import { LogService } from "jslib-common/abstractions/log.service";
|
|
||||||
import { OrganizationConnectionType } from "jslib-common/enums/organizationConnectionType";
|
|
||||||
import { Utils } from "jslib-common/misc/utils";
|
|
||||||
import { BillingSyncConfigApi } from "jslib-common/models/api/billingSyncConfigApi";
|
|
||||||
import { BillingSyncConfigRequest } from "jslib-common/models/request/billingSyncConfigRequest";
|
|
||||||
import { OrganizationConnectionRequest } from "jslib-common/models/request/organizationConnectionRequest";
|
|
||||||
import { OrganizationConnectionResponse } from "jslib-common/models/response/organizationConnectionResponse";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-billing-sync-key",
|
|
||||||
templateUrl: "billing-sync-key.component.html",
|
|
||||||
})
|
|
||||||
export class BillingSyncKeyComponent {
|
|
||||||
entityId: string;
|
|
||||||
existingConnectionId: string;
|
|
||||||
billingSyncKey: string;
|
|
||||||
setParentConnection: (connection: OrganizationConnectionResponse<BillingSyncConfigApi>) => void;
|
|
||||||
|
|
||||||
formPromise: Promise<OrganizationConnectionResponse<BillingSyncConfigApi>> | Promise<void>;
|
|
||||||
|
|
||||||
constructor(private apiService: ApiService, private logService: LogService) {}
|
|
||||||
|
|
||||||
async submit() {
|
|
||||||
try {
|
|
||||||
const request = new OrganizationConnectionRequest(
|
|
||||||
this.entityId,
|
|
||||||
OrganizationConnectionType.CloudBillingSync,
|
|
||||||
true,
|
|
||||||
new BillingSyncConfigRequest(this.billingSyncKey)
|
|
||||||
);
|
|
||||||
if (this.existingConnectionId == null) {
|
|
||||||
this.formPromise = this.apiService.createOrganizationConnection(
|
|
||||||
request,
|
|
||||||
BillingSyncConfigApi
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.formPromise = this.apiService.updateOrganizationConnection(
|
|
||||||
request,
|
|
||||||
BillingSyncConfigApi,
|
|
||||||
this.existingConnectionId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const response = (await this
|
|
||||||
.formPromise) as OrganizationConnectionResponse<BillingSyncConfigApi>;
|
|
||||||
this.existingConnectionId = response?.id;
|
|
||||||
this.billingSyncKey = response?.config?.billingSyncKey;
|
|
||||||
this.setParentConnection(response);
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteConnection() {
|
|
||||||
this.formPromise = this.apiService.deleteOrganizationConnection(this.existingConnectionId);
|
|
||||||
await this.formPromise;
|
|
||||||
this.setParentConnection(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<div class="container page-content">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="page-header">
|
|
||||||
<h1>{{ "newOrganization" | i18n }}</h1>
|
|
||||||
</div>
|
|
||||||
<p>{{ "newOrganizationDesc" | i18n }}</p>
|
|
||||||
<app-organization-plans></app-organization-plans>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import { formatDate } from "@angular/common";
|
|
||||||
import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core";
|
|
||||||
|
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
|
||||||
import { LogService } from "jslib-common/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
|
||||||
import { Organization } from "jslib-common/models/domain/organization";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "[sponsoring-org-row]",
|
|
||||||
templateUrl: "sponsoring-org-row.component.html",
|
|
||||||
})
|
|
||||||
export class SponsoringOrgRowComponent implements OnInit {
|
|
||||||
@Input() sponsoringOrg: Organization = null;
|
|
||||||
@Input() isSelfHosted = false;
|
|
||||||
|
|
||||||
@Output() sponsorshipRemoved = new EventEmitter();
|
|
||||||
|
|
||||||
statusMessage = "loading";
|
|
||||||
statusClass: "text-success" | "text-danger" = "text-success";
|
|
||||||
|
|
||||||
revokeSponsorshipPromise: Promise<any>;
|
|
||||||
resendEmailPromise: Promise<any>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private apiService: ApiService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private logService: LogService,
|
|
||||||
private platformUtilsService: PlatformUtilsService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.setStatus(
|
|
||||||
this.isSelfHosted,
|
|
||||||
this.sponsoringOrg.familySponsorshipToDelete,
|
|
||||||
this.sponsoringOrg.familySponsorshipValidUntil,
|
|
||||||
this.sponsoringOrg.familySponsorshipLastSyncDate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async revokeSponsorship() {
|
|
||||||
try {
|
|
||||||
this.revokeSponsorshipPromise = this.doRevokeSponsorship();
|
|
||||||
await this.revokeSponsorshipPromise;
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.revokeSponsorshipPromise = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async resendEmail() {
|
|
||||||
this.resendEmailPromise = this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id);
|
|
||||||
await this.resendEmailPromise;
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("emailSent"));
|
|
||||||
this.resendEmailPromise = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isSentAwaitingSync() {
|
|
||||||
return this.isSelfHosted && !this.sponsoringOrg.familySponsorshipLastSyncDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async doRevokeSponsorship() {
|
|
||||||
const isConfirmed = await this.platformUtilsService.showDialog(
|
|
||||||
this.i18nService.t("revokeSponsorshipConfirmation"),
|
|
||||||
`${this.i18nService.t("remove")} ${this.sponsoringOrg.familySponsorshipFriendlyName}?`,
|
|
||||||
this.i18nService.t("remove"),
|
|
||||||
this.i18nService.t("cancel"),
|
|
||||||
"warning"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isConfirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.apiService.deleteRevokeSponsorship(this.sponsoringOrg.id);
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("reclaimedFreePlan"));
|
|
||||||
this.sponsorshipRemoved.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
private setStatus(
|
|
||||||
selfHosted: boolean,
|
|
||||||
toDelete?: boolean,
|
|
||||||
validUntil?: Date,
|
|
||||||
lastSyncDate?: Date
|
|
||||||
) {
|
|
||||||
/*
|
|
||||||
* Possible Statuses:
|
|
||||||
* Requested (self-hosted only)
|
|
||||||
* Sent
|
|
||||||
* Active
|
|
||||||
* RequestRevoke
|
|
||||||
* RevokeWhenExpired
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (toDelete && validUntil) {
|
|
||||||
// They want to delete but there is a valid until date which means there is an active sponsorship
|
|
||||||
this.statusMessage = this.i18nService.t(
|
|
||||||
"revokeWhenExpired",
|
|
||||||
formatDate(validUntil, "MM/dd/yyyy", this.i18nService.locale)
|
|
||||||
);
|
|
||||||
this.statusClass = "text-danger";
|
|
||||||
} else if (toDelete) {
|
|
||||||
// They want to delete and we don't have a valid until date so we can
|
|
||||||
// this should only happen on a self-hosted install
|
|
||||||
this.statusMessage = this.i18nService.t("requestRemoved");
|
|
||||||
this.statusClass = "text-danger";
|
|
||||||
} else if (validUntil) {
|
|
||||||
// They don't want to delete and they have a valid until date
|
|
||||||
// that means they are actively sponsoring someone
|
|
||||||
this.statusMessage = this.i18nService.t("active");
|
|
||||||
this.statusClass = "text-success";
|
|
||||||
} else if (selfHosted && lastSyncDate) {
|
|
||||||
// We are on a self-hosted install and it has been synced but we have not gotten
|
|
||||||
// a valid until date so we can't know if they are actively sponsoring someone
|
|
||||||
this.statusMessage = this.i18nService.t("sent");
|
|
||||||
this.statusClass = "text-success";
|
|
||||||
} else if (!selfHosted) {
|
|
||||||
// We are in cloud and all other status checks have been false therefore we have
|
|
||||||
// sent the request but it hasn't been accepted yet
|
|
||||||
this.statusMessage = this.i18nService.t("sent");
|
|
||||||
this.statusClass = "text-success";
|
|
||||||
} else {
|
|
||||||
// We are on a self-hosted install and we have not synced yet
|
|
||||||
this.statusMessage = this.i18nService.t("requested");
|
|
||||||
this.statusClass = "text-success";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
16
bitwarden-web.sln
Normal file
16
bitwarden-web.sln
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "dotnet-src\Web\Web.csproj", "{D0B6D8EB-21F0-400A-91E5-2C4722B9D170}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{D0B6D8EB-21F0-400A-91E5-2C4722B9D170}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D0B6D8EB-21F0-400A-91E5-2C4722B9D170}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D0B6D8EB-21F0-400A-91E5-2C4722B9D170}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D0B6D8EB-21F0-400A-91E5-2C4722B9D170}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -22,16 +22,14 @@ const routes: Routes = [
|
|||||||
component: ManageComponent,
|
component: ManageComponent,
|
||||||
canActivate: [PermissionsGuard],
|
canActivate: [PermissionsGuard],
|
||||||
data: {
|
data: {
|
||||||
permissions: NavigationPermissionsService.getPermissions("manage"),
|
permissions: [
|
||||||
|
NavigationPermissionsService.getPermissions("manage").concat(Permissions.ManageSso),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "sso",
|
path: "sso",
|
||||||
component: SsoComponent,
|
component: SsoComponent,
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
permissions: [Permissions.ManageSso],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user