1
0
mirror of https://github.com/bitwarden/web synced 2025-12-10 13:23:15 +00:00

Compare commits

..

10 Commits

Author SHA1 Message Date
Vince Grassia
6b295ce392 Merge branch 'master' into update-self-hosted 2022-05-10 12:47:55 -04:00
Vince Grassia
254f215efd Update Dockerfiles 2022-05-10 11:22:21 -04:00
Vince Grassia
74bd2a0884 Update build workflow and create QA Dockerfile 2022-05-05 12:18:42 -04:00
Vince Grassia
c490b67f74 Merge branch 'master' into update-self-hosted 2022-05-05 09:13:46 -04:00
Vince Grassia
3fb6b36874 Update Dockerfile 2022-04-07 16:34:04 -04:00
Vince Grassia
b9c31597a2 Fix Web project version 2022-04-07 12:10:26 -04:00
Vince Grassia
a6f41f9020 Merge branch 'master' into update-self-hosted 2022-04-07 12:06:41 -04:00
Vince Grassia
8add15eae9 Add web server 2022-04-07 12:05:30 -04:00
Vince Grassia
9d2cfe4a3d Update Dockerfile 2022-03-21 11:08:56 -04:00
Vince Grassia
dbd70f687d Update docker 2022-02-26 18:14:31 -05:00
75 changed files with 1366 additions and 1709 deletions

View File

@@ -1,3 +1,3 @@
*
!build/*
!entrypoint.sh
**/bin
**/obj
**/node_modules

View File

@@ -218,7 +218,7 @@ jobs:
run: |
echo -e "\nBuilding Docker image"
docker --version
docker build -t bitwarden/web .
docker build -t bitwarden/web -f docker/Dockerfile .
- name: Tag rc branch
if: github.ref == 'refs/heads/rc'
@@ -340,7 +340,7 @@ jobs:
echo -e "\nBuilding Docker image"
docker --version
docker build -t bitwardenqa.azurecr.io/web .
docker build -t bitwardenqa.azurecr.io/web -f docker/Dockerfile-QA .
- name: Get image tag
id: image-tag

View File

@@ -38,6 +38,12 @@ jobs:
version: ${{ github.event.inputs.version_number }}
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
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"

12
.gitignore vendored
View File

@@ -13,3 +13,15 @@ dist/
build/
!dev-server.shared.pem
config/local.json
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/

View File

@@ -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"]

16
bitwarden-web.sln Normal file
View 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

View File

@@ -22,9 +22,9 @@ const routes: Routes = [
component: ManageComponent,
canActivate: [PermissionsGuard],
data: {
permissions: NavigationPermissionsService.getPermissions("manage").concat(
Permissions.ManageSso
),
permissions: [
NavigationPermissionsService.getPermissions("manage").concat(Permissions.ManageSso),
],
},
children: [
{

84
docker/Dockerfile Normal file
View File

@@ -0,0 +1,84 @@
###############################################
# Build stage #
###############################################
FROM node:16-slim AS node-build
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /source
COPY . .
RUN npm ci
RUN npm run dist:bit:selfhost
###############################################
# Build stage #
###############################################
FROM mcr.microsoft.com/dotnet/sdk:5.0-alpine AS dotnet-build
# Add packages
RUN apk add --update-cache \
npm \
&& rm -rf /var/cache/apk/*
# Copy csproj files as distinct layers
WORKDIR /source
COPY dotnet-src/Web/*.csproj ./src/Web/
#COPY Directory.Build.props .
# Restore project dependencies and tools
WORKDIR /source/src/Web
RUN dotnet restore
# Copy required project files
WORKDIR /source
COPY dotnet-src/Web/. ./src/Web/
# Build app
WORKDIR /source/src/Web
RUN dotnet publish -c release -o /app --no-restore
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:5.0-alpine
LABEL com.bitwarden.product="bitwarden"
LABEL com.bitwarden.project="web"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS http://+:5000
EXPOSE 5000
# Add packages
RUN apk add --update-cache \
curl \
&& rm -rf /var/cache/apk/*
# Create required directories
RUN mkdir -p /etc/bitwarden/web
COPY docker/confd/app-id.toml /etc/confd/conf.d/
COPY docker/confd/app-id.conf.tmpl /etc/confd/templates/
ADD https://github.com/kelseyhightower/confd/releases/download/v0.16.0/confd-0.16.0-linux-amd64 /usr/local/bin/confd
RUN chmod +x /usr/local/bin/confd
# Copy Web server from dotnet-build stage
COPY --from=dotnet-build /app /server
# Copy app from build stage
WORKDIR /app
COPY --from=node-build /source/build ./
# Copy entrypoint script and make it executable
COPY docker/entrypoint.sh /
RUN chmod +x /entrypoint.sh
# Create non-root user to run app
RUN adduser -s /bin/false -D bitwarden && chown -R bitwarden:bitwarden /app /server /etc/bitwarden
USER bitwarden:bitwarden
HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1
ENTRYPOINT ["/entrypoint.sh"]

87
docker/Dockerfile-QA Normal file
View File

@@ -0,0 +1,87 @@
###############################################
# Build stage #
###############################################
FROM node:16-slim AS node-build
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /source
COPY . .
RUN npm ci
# TODO: Make sure version is correct when building QA image.
# RUN jq --arg version "$VERSION - ${GITHUB_SHA:0:7}" '.version = $version' package.json > package.json.tmp
# RUN mv package.json.tmp package.json
RUN npm run build:bit:qa
###############################################
# Build stage #
###############################################
FROM mcr.microsoft.com/dotnet/sdk:5.0-alpine AS dotnet-build
# Add packages
RUN apk add --update-cache \
npm \
&& rm -rf /var/cache/apk/*
# Copy csproj files as distinct layers
WORKDIR /source
COPY dotnet-src/Web/*.csproj ./src/Web/
#COPY Directory.Build.props .
# Restore project dependencies and tools
WORKDIR /source/src/Web
RUN dotnet restore
# Copy required project files
WORKDIR /source
COPY dotnet-src/Web/. ./src/Web/
# Build app
WORKDIR /source/src/Web
RUN dotnet publish -c release -o /app --no-restore
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:5.0-alpine
LABEL com.bitwarden.product="bitwarden"
LABEL com.bitwarden.project="web"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS http://+:5000
EXPOSE 5000
# Add packages
RUN apk add --update-cache \
curl \
&& rm -rf /var/cache/apk/*
# Create required directories
RUN mkdir -p /etc/bitwarden/web
COPY docker/confd/app-id.toml /etc/confd/conf.d/
COPY docker/confd/app-id.conf.tmpl /etc/confd/templates/
ADD https://github.com/kelseyhightower/confd/releases/download/v0.16.0/confd-0.16.0-linux-amd64 /usr/local/bin/confd
RUN chmod +x /usr/local/bin/confd
# Copy Web server from dotnet-build stage
COPY --from=dotnet-build /app /server
# Copy app from build stage
WORKDIR /app
COPY --from=node-build /source/build ./
# Copy entrypoint script and make it executable
COPY docker/entrypoint.sh /
RUN chmod +x /entrypoint.sh
# Create non-root user to run app
RUN adduser -s /bin/false -D bitwarden && chown -R bitwarden:bitwarden /app /server /etc/bitwarden
USER bitwarden:bitwarden
HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,15 @@
{
"trustedFacets": [
{
"version": {
"major": 1,
"minor": 0
},
"ids": [
"{{ getenv "globalSettings__baseServiceUri__vault" "https://localhost" }}",
"ios:bundle-id:com.8bit.bitwarden",
"android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI"
]
}
]
}

6
docker/confd/app-id.toml Normal file
View File

@@ -0,0 +1,6 @@
[template]
src = "app-id.conf.tmpl"
dest = "/etc/bitwarden/web/app-id.json"
keys = [
"globalSettings__baseServiceUri__vault"
]

7
docker/entrypoint.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
/usr/local/bin/confd -onetime -backend env
cp /etc/bitwarden/web/app-id.json /app/app-id.json
exec dotnet /server/Web.dll /contentRoot=/app /webRoot=.

46
dotnet-src/Web/Program.cs Normal file
View File

@@ -0,0 +1,46 @@
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Bit.Web
{
public class Program
{
public static void Main(string[] args)
{
var config = new ConfigurationBuilder()
.AddCommandLine(args)
.Build();
var builder = new WebHostBuilder()
.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>()
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConsole().AddDebug();
})
.ConfigureKestrel((context, options) => { });
var contentRoot = config.GetValue<string>("contentRoot");
if (!string.IsNullOrWhiteSpace(contentRoot))
{
builder.UseContentRoot(contentRoot);
}
else
{
builder.UseContentRoot(Directory.GetCurrentDirectory());
}
var webRoot = config.GetValue<string>("webRoot");
if (string.IsNullOrWhiteSpace(webRoot))
{
builder.UseWebRoot(webRoot);
}
var host = builder.Build();
host.Run();
}
}
}

View File

@@ -0,0 +1,12 @@
{
"profiles": {
"Server": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53910/"
}
}
}

79
dotnet-src/Web/Startup.cs Normal file
View File

@@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Web
{
public class Startup
{
private readonly List<string> _longCachedPaths = new List<string>
{
"/app/", "/locales/", "/fonts/", "/connectors/", "/scripts/"
};
private readonly List<string> _mediumCachedPaths = new List<string>
{
"/images/"
};
public Startup()
{
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
}
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
}
public void Configure(
IApplicationBuilder app,
IConfiguration configuration)
{
// TODO: This should be removed when asp.net natively support avif
var provider = new FileExtensionContentTypeProvider { Mappings = { [".avif"] = "image/avif" } };
var options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("index.html");
app.UseDefaultFiles(options);
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider,
OnPrepareResponse = ctx =>
{
if (!ctx.Context.Request.Path.HasValue ||
ctx.Context.Response.Headers.ContainsKey("Cache-Control"))
{
return;
}
var path = ctx.Context.Request.Path.Value;
if (_longCachedPaths.Any(ext => path.StartsWith(ext)))
{
// 14 days
ctx.Context.Response.Headers.Append("Cache-Control", "max-age=1209600");
}
if (_mediumCachedPaths.Any(ext => path.StartsWith(ext)))
{
// 7 days
ctx.Context.Response.Headers.Append("Cache-Control", "max-age=604800");
}
}
});
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/alive",
async context => await context.Response.WriteAsJsonAsync(System.DateTime.UtcNow));
endpoints.MapGet("/version",
async context => await context.Response.WriteAsJsonAsync(Assembly.GetEntryAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion));
});
}
}
}

11
dotnet-src/Web/Web.csproj Normal file
View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
<TargetFramework>net5.0</TargetFramework>
<Version>2.27.0</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
</Project>

15
dotnet-src/Web/build.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
echo -e "\n## Building Web"
echo -e "\nBuilding app"
echo ".NET Core version $(dotnet --version)"
echo "Restore"
dotnet restore "$DIR/Web.csproj"
echo "Clean"
dotnet clean "$DIR/Web.csproj" -c "Release" -o "$DIR/obj/build-output/publish"
echo "Publish"
dotnet publish "$DIR/Web.csproj" -c "Release" -o "$DIR/obj/build-output/publish"

View File

@@ -0,0 +1,6 @@
{
"version": 1,
"dependencies": {
".NETCoreApp,Version=v5.0": {}
}
}

View File

@@ -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

2
jslib

Submodule jslib updated: 3cb94623e2...00deb38de5

1302
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -55,7 +55,7 @@ export class LockComponent extends BaseLockComponent {
await super.ngOnInit();
this.onSuccessfulSubmit = async () => {
const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl && previousUrl !== "/" && previousUrl.indexOf("lock") === -1) {
if (previousUrl !== "/" && previousUrl.indexOf("lock") === -1) {
this.successRoute = previousUrl;
}
this.router.navigateByUrl(this.successRoute);

View File

@@ -74,7 +74,7 @@ export class LoginComponent extends BaseLoginComponent {
if (qParams.premium != null) {
this.routerService.setPreviousUrl("/settings/premium");
} else if (qParams.org != null) {
const route = this.router.createUrlTree(["create-organization"], {
const route = this.router.createUrlTree(["settings/create-organization"], {
queryParams: { plan: qParams.org },
});
this.routerService.setPreviousUrl(route.toString());

View File

@@ -71,7 +71,7 @@ export class RegisterComponent extends BaseRegisterComponent {
} else if (qParams.org != null) {
this.showCreateOrgMessage = true;
this.referenceData.flow = qParams.org;
const route = this.router.createUrlTree(["create-organization"], {
const route = this.router.createUrlTree(["settings/create-organization"], {
queryParams: { plan: qParams.org },
});
this.routerService.setPreviousUrl(route.toString());

View File

@@ -122,20 +122,17 @@ export abstract class BaseEventsComponent {
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = await this.eventService.getEventInfo(r);
const user = this.getUserName(r, userId);
const userName = user != null ? user.name : this.i18nService.t("unknown");
return new EventView({
message: eventInfo.message,
humanReadableMessage: eventInfo.humanReadableMessage,
appIcon: eventInfo.appIcon,
appName: eventInfo.appName,
userId: userId,
userName: r.installationId != null ? `Installation: ${r.installationId}` : userName,
userName: user != null ? user.name : this.i18nService.t("unknown"),
userEmail: user != null ? user.email : "",
date: r.date,
ip: r.ipAddress,
type: r.type,
installationId: r.installationId,
});
})
);

View File

@@ -58,7 +58,7 @@
</li>
<bit-menu-divider></bit-menu-divider>
<li class="tw-list-none" role="none">
<a bit-menu-item routerLink="/create-organization">
<a bit-menu-item routerLink="/settings/create-organization">
<i class="bwi bwi-plus mr-2"></i>
{{ "newOrganization" | i18n }}</a
>

View File

@@ -1,6 +1,6 @@
<div class="container footer text-muted">
<div class="row">
<div class="col">&copy; {{ year }} Bitwarden Inc.</div>
<div class="col">&copy; {{ year }}, Bitwarden Inc.</div>
<div class="col text-center"></div>
<div class="col text-right">
{{ "versionNumber" | i18n: version }}

View File

@@ -1,5 +1,5 @@
<router-outlet></router-outlet>
<div class="container my-5 text-muted text-center">
&copy; {{ year }} Bitwarden Inc. <br />
&copy; {{ year }}, Bitwarden Inc. <br />
{{ "versionNumber" | i18n: version }}
</div>

View File

@@ -38,7 +38,7 @@
<li>
<button
[bitMenuTriggerFor]="accountMenu"
class="tw-border-0 tw-bg-transparent tw-text-alt2 tw-opacity-70 hover:tw-opacity-90"
class="tw-border-0 tw-bg-transparent tw-text-contrast tw-opacity-70 hover:tw-opacity-90"
>
<i class="bwi bwi-user-circle bwi-lg" aria-hidden="true"></i>
<i class="bwi bwi-caret-down bwi-sm" aria-hidden="true"></i>

View File

@@ -1,6 +1,5 @@
import { Component, NgZone, OnInit } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
@@ -32,9 +31,7 @@ export class NavbarComponent implements OnInit {
private providerService: ProviderService,
private syncService: SyncService,
private organizationService: OrganizationService,
private i18nService: I18nService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone
private i18nService: I18nService
) {
this.selfHosted = this.platformUtilsService.isSelfHost();
}
@@ -52,24 +49,8 @@ export class NavbarComponent implements OnInit {
}
this.providers = await this.providerService.getAll();
this.organizations = await this.buildOrganizations();
this.broadcasterService.subscribe(this.constructor.name, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "organizationCreated":
if (this.organizations.length < 1) {
this.organizations = await this.buildOrganizations();
}
break;
}
});
});
}
async buildOrganizations() {
const allOrgs = await this.organizationService.getAll();
return allOrgs
this.organizations = allOrgs
.filter((org) => OrgNavigationPermissionsService.canAccessAdmin(org))
.sort(Utils.getSortFunction(this.i18nService, "name"));
}

View File

@@ -35,6 +35,7 @@ import { BulkStatusComponent as OrgBulkStatusComponent } from "../organizations/
import { CollectionAddEditComponent as OrgCollectionAddEditComponent } from "../organizations/manage/collection-add-edit.component";
import { CollectionsComponent as OrgManageCollectionsComponent } from "../organizations/manage/collections.component";
import { EntityEventsComponent as OrgEntityEventsComponent } from "../organizations/manage/entity-events.component";
import { EntityUsersComponent as OrgEntityUsersComponent } from "../organizations/manage/entity-users.component";
import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component";
import { GroupAddEditComponent as OrgGroupAddEditComponent } from "../organizations/manage/group-add-edit.component";
import { GroupsComponent as OrgGroupsComponent } from "../organizations/manage/groups.component";
@@ -57,7 +58,6 @@ import { SingleOrgPolicyComponent } from "../organizations/policies/single-org.c
import { TwoFactorAuthenticationPolicyComponent } from "../organizations/policies/two-factor-authentication.component";
import { AccountComponent as OrgAccountComponent } from "../organizations/settings/account.component";
import { AdjustSubscription } from "../organizations/settings/adjust-subscription.component";
import { BillingSyncApiKeyComponent } from "../organizations/settings/billing-sync-api-key.component";
import { ChangePlanComponent } from "../organizations/settings/change-plan.component";
import { DeleteOrganizationComponent } from "../organizations/settings/delete-organization.component";
import { DownloadLicenseComponent } from "../organizations/settings/download-license.component";
@@ -66,7 +66,6 @@ import { OrganizationBillingComponent } from "../organizations/settings/organiza
import { OrganizationSubscriptionComponent } from "../organizations/settings/organization-subscription.component";
import { SettingsComponent as OrgSettingComponent } from "../organizations/settings/settings.component";
import { TwoFactorSetupComponent as OrgTwoFactorSetupComponent } from "../organizations/settings/two-factor-setup.component";
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
import { ExportComponent as OrgExportComponent } from "../organizations/tools/export.component";
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../organizations/tools/exposed-passwords-report.component";
@@ -99,7 +98,6 @@ import { AddCreditComponent } from "../settings/add-credit.component";
import { AdjustPaymentComponent } from "../settings/adjust-payment.component";
import { AdjustStorageComponent } from "../settings/adjust-storage.component";
import { ApiKeyComponent } from "../settings/api-key.component";
import { BillingSyncKeyComponent } from "../settings/billing-sync-key.component";
import { ChangeEmailComponent } from "../settings/change-email.component";
import { ChangeKdfComponent } from "../settings/change-kdf.component";
import { ChangePasswordComponent } from "../settings/change-password.component";
@@ -173,7 +171,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
declarations: [
PremiumBadgeComponent,
AcceptEmergencyComponent,
AcceptFamilySponsorshipComponent,
AcceptOrganizationComponent,
AccessComponent,
AccountComponent,
@@ -186,8 +183,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
AdjustSubscription,
ApiKeyComponent,
AttachmentsComponent,
BillingSyncApiKeyComponent,
BillingSyncKeyComponent,
BreachReportComponent,
BulkActionsComponent,
BulkDeleteComponent,
@@ -244,6 +239,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
OrgEntityEventsComponent,
OrgEntityUsersComponent,
OrgEventsComponent,
OrgExportComponent,
OrgExposedPasswordsReportComponent,
@@ -404,6 +400,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
OrgEntityEventsComponent,
OrgEntityUsersComponent,
OrgEventsComponent,
OrgExportComponent,
OrgExposedPasswordsReportComponent,

View File

@@ -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 {}

View File

@@ -16,7 +16,7 @@
aria-hidden="true"
></i>
</button>
<h3 class="filter-title">&nbsp;{{ collectionsGrouping.name | i18n }}</h3>
<h3 class="filter-title">{{ collectionsGrouping.name | i18n }}</h3>
</div>
<ul id="collection-filters" *ngIf="!isCollapsed(collectionsGrouping)" class="filter-options">
<ng-template #recursiveCollections let-collections>
@@ -51,7 +51,7 @@
class="bwi bwi-collection bwi-fw"
aria-hidden="true"
></i
>&nbsp;{{ c.node.name }}
>{{ c.node.name }}
</button>
</span>
<ul

View File

@@ -1,4 +1,4 @@
<ng-container *ngIf="!hide">
<ng-container *ngIf="!hide && !activeFilter.selectedOrganizationId">
<div class="filter-heading">
<button
class="toggle-button"
@@ -16,7 +16,9 @@
}"
></i>
</button>
<h3 class="filter-title">&nbsp;{{ "folders" | i18n }}</h3>
<h3 class="filter-title">
{{ "folders" | i18n }}
</h3>
<button
class="text-muted ml-auto add-button"
(click)="addFolder()"
@@ -54,7 +56,7 @@
</button>
<button class="filter-button" (click)="applyFilter(f.node)">
<i *ngIf="f.children.length === 0" class="bwi bwi-fw bwi-folder" aria-hidden="true"></i
>&nbsp;{{ f.node.name }}
>{{ f.node.name }}
</button>
<button
class="edit-button"

View File

@@ -12,7 +12,7 @@
</li>
<li class="filter-option">
<span class="filter-buttons">
<a href="#" routerLink="/create-organization" class="filter-button">
<a href="#" routerLink="/settings/create-organization" class="filter-button">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newOrganization" | i18n }}
</a>
@@ -47,9 +47,9 @@
</button>
<a
href="#"
routerLink="/create-organization"
routerLink="/settings/create-organization"
class="text-muted ml-auto create-organization-link"
appA11yTitle="{{ 'newOrganization' | i18n }}"
appA11yTitle="{{ 'addOrganization' | i18n }}"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</a>
@@ -78,14 +78,16 @@
</ul>
</ng-container>
<ng-container *ngSwitchCase="'singleOrganizationAndPersonalOwnershipPolicies'">
<div class="filter-heading">
<button class="filter-button active">
<ul class="filter-options">
<li class="filter-option active">
<button class="filter-button">
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
{{ organizations[0].name }}
</button>
</div>
</li>
</ul>
</ng-container>
<ng-container *ngSwitchDefault>
<ng-container *ngSwitchCase="'organizationMember'">
<div class="filter-heading">
<button
class="toggle-button"
@@ -112,10 +114,9 @@
</button>
<a
href="#"
routerLink="/create-organization"
routerLink="/settings/create-organization"
class="text-muted ml-auto create-organization-link"
appA11yTitle="{{ 'newOrganization' | i18n }}"
*ngIf="!(displayMode === 'singleOrganizationPolicy')"
appA11yTitle="{{ 'addOrganization' | i18n }}"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</a>
@@ -139,7 +140,7 @@
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
{{ organization.name }}
</button>
<ng-container>
<ng-container *ngIf="organization.id === activeFilter.selectedOrganizationId">
<button [bitMenuTriggerFor]="orgMenu" class="org-options ml-auto">
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
</button>

View File

@@ -35,6 +35,7 @@ export class OrganizationOptionsComponent {
) {}
async ngOnInit() {
await this.syncService.fullSync(true);
await this.load();
}
@@ -82,7 +83,6 @@ export class OrganizationOptionsComponent {
this.platformUtilsService.showToast("success", null, "Unlinked SSO");
await this.load();
} catch (e) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
this.logService.error(e);
}
}
@@ -107,7 +107,6 @@ export class OrganizationOptionsComponent {
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
await this.load();
} catch (e) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
this.logService.error(e);
}
}
@@ -175,7 +174,6 @@ export class OrganizationOptionsComponent {
this.platformUtilsService.showToast("success", null, this.i18nService.t(toastStringRef));
await this.load();
} catch (e) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
this.logService.error(e);
}
}

View File

@@ -15,7 +15,9 @@
}"
></i>
</button>
<h3>&nbsp;{{ "types" | i18n }}</h3>
<h3>
{{ "types" | i18n }}
</h3>
</div>
<ul id="type-filters" *ngIf="!isCollapsed" class="filter-options">
<li
@@ -24,14 +26,14 @@
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Login)">
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>&nbsp;{{ "typeLogin" | i18n }}
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>{{ "typeLogin" | i18n }}
</button>
</span>
</li>
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Card }">
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Card)">
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>&nbsp;{{ "typeCard" | i18n }}
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>{{ "typeCard" | i18n }}
</button>
</span>
</li>
@@ -41,7 +43,7 @@
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Identity)">
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i>&nbsp;{{ "typeIdentity" | i18n }}
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i>{{ "typeIdentity" | i18n }}
</button>
</span>
</li>
@@ -51,9 +53,7 @@
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.SecureNote)">
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>&nbsp;{{
"typeSecureNote" | i18n
}}
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>{{ "typeSecureNote" | i18n }}
</button>
</span>
</li>

View File

@@ -26,6 +26,7 @@
autocomplete="off"
appAutofocus
/>
<div class="filter">
<app-organization-filter
*ngIf="showOrgFilter"
[hide]="hideOrganizations"
@@ -37,6 +38,7 @@
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
(onFilterChange)="applyFilter($event)"
></app-organization-filter>
</div>
<div class="filter">
<app-status-filter
[hideFavorites]="!showFavorites"

View File

@@ -28,6 +28,12 @@ export class VaultFilterComponent extends BaseVaultFilterComponent {
this.onSearchTextChanged.emit(this.searchText);
}
// This method exists because the vault component gets its data mixed up during the initial sync on first login. It looks for data before the sync is complete.
// It should be removed as soon as doing so makes sense.
async reloadOrganizations() {
this.organizations = await this.vaultFilterService.buildOrganizations();
}
async initCollections() {
return await this.vaultFilterService.buildCollections(this.organization?.id);
}

View File

@@ -32,26 +32,19 @@
</small>
</h1>
<div class="ml-auto d-flex">
<app-vault-bulk-actions
[ciphersComponent]="ciphersComponent"
[deleted]="activeFilter.status === 'trash'"
>
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [deleted]="deleted">
</app-vault-bulk-actions>
<button
type="button"
class="btn btn-outline-primary btn-sm"
(click)="addCipher()"
*ngIf="activeFilter.status !== 'trash'"
*ngIf="!deleted"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
</button>
</div>
</div>
<app-callout
type="warning"
*ngIf="activeFilter.status === 'trash'"
icon="bwi-exclamation-triangle"
>
<app-callout type="warning" *ngIf="deleted" icon="bwi-exclamation-triangle">
{{ trashCleanupWarning }}
</app-callout>
<app-vault-ciphers
@@ -102,10 +95,7 @@
</div>
<div class="card-body">
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
<a
class="btn btn-block btn-outline-secondary"
routerLink="/settings/subscription/premium"
>
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/premium">
{{ "goPremium" | i18n }}
</a>
</div>

View File

@@ -209,7 +209,7 @@ export class IndividualVaultComponent implements OnInit, OnDestroy {
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
}
if (
this.activeFilter.selectedFolder &&
this.activeFilter.selectedFolderId != null &&
this.activeFilter.selectedFolderId != "none" &&
cipherPassesFilter
) {
@@ -352,7 +352,7 @@ export class IndividualVaultComponent implements OnInit, OnDestroy {
async editCipherId(id: string) {
const cipher = await this.cipherService.get(id);
if (cipher != null && cipher.reprompt != 0) {
if (cipher.reprompt != 0) {
if (!(await this.passwordRepromptService.showPasswordPrompt())) {
this.go({ cipherId: null });
return;

View File

@@ -86,7 +86,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
this.ciphersComponent.organization = this.organization;
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.ciphersComponent.searchText = this.vaultFilterComponent.searchText = qParams.search;
// this.ciphersComponent.searchText = this.vaultFilterComponent.search = qParams.search;
if (!this.organization.canViewAllCollections) {
await this.syncService.fullSync(false);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
@@ -123,11 +123,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
this.route.queryParams.subscribe(async (params) => {
if (params.cipherId) {
if (
// Handle users with implicit collection access since they use the admin endpoint
this.organization.canEditAnyCollection ||
(await this.cipherService.get(params.cipherId)) != null
) {
if ((await this.cipherService.get(params.cipherId)) != null) {
this.editCipherId(params.cipherId);
} else {
this.platformUtilsService.showToast(
@@ -172,7 +168,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
}
if (
this.activeFilter.selectedFolder != null &&
this.activeFilter.selectedFolderId != null &&
this.activeFilter.selectedFolderId != "none" &&
cipherPassesFilter
) {

View File

@@ -1,5 +1,5 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
@@ -17,7 +17,7 @@ export class PermissionsGuard implements CanActivate {
private syncService: SyncService
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
async canActivate(route: ActivatedRouteSnapshot) {
// TODO: We need to fix this issue once and for all.
if ((await this.syncService.getLastSync()) == null) {
await this.syncService.fullSync(false);
@@ -39,16 +39,6 @@ export class PermissionsGuard implements CanActivate {
const permissions = route.data == null ? [] : (route.data.permissions as Permissions[]);
if (permissions != null && !org.hasAnyPermission(permissions)) {
// Handle linkable ciphers for organizations the user only has view access to
// https://bitwarden.atlassian.net/browse/EC-203
if (state.root.queryParamMap.has("cipherId")) {
return this.router.createUrlTree(["/vault"], {
queryParams: {
cipherId: state.root.queryParamMap.get("cipherId"),
},
});
}
this.platformUtilsService.showToast("error", null, this.i18nService.t("accessDenied"));
return this.router.createUrlTree(["/"]);
}

View File

@@ -1,4 +1,3 @@
<app-navbar></app-navbar>
<div class="org-nav" *ngIf="organization">
<div class="container d-flex">
<div class="d-flex flex-column">
@@ -36,4 +35,3 @@
</div>
</div>
<router-outlet></router-outlet>
<app-footer></app-footer>

View File

@@ -20,9 +20,8 @@ import {
import { ListResponse } from "jslib-common/models/response/listResponse";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { EntityUsersComponent } from "../../modules/organizations/manage/entity-users.component";
import { CollectionAddEditComponent } from "./collection-add-edit.component";
import { EntityUsersComponent } from "./entity-users.component";
@Component({
selector: "app-org-manage-collections",

View File

@@ -29,13 +29,12 @@
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<cdk-virtual-scroll-viewport
itemSize="46"
minBufferPx="600"
maxBufferPx="1200"
[style]="scrollViewportStyle"
<div
class="modal-body"
*ngIf="
!loading && users && (users | search: searchText:'name':'email':'id') as searchedUsers
"
>
<div class="modal-body" *ngIf="!loading && users && searchedUsers">
<div class="d-flex">
<div class="mr-3">
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
@@ -74,7 +73,8 @@
<hr />
{{ "noUsersInList" | i18n }}
</ng-container>
<table class="table table-hover table-list mb-0" [hidden]="!searchedUsers.length">
<ng-container *ngIf="searchedUsers.length">
<table class="table table-hover table-list mb-0">
<thead>
<tr>
<th>&nbsp;</th>
@@ -91,7 +91,7 @@
</tr>
</thead>
<tbody>
<tr *cdkVirtualFor="let u of searchedUsers" class="">
<tr *ngFor="let u of searchedUsers">
<td class="table-list-checkbox" (click)="check(u)">
<input
type="checkbox"
@@ -164,8 +164,8 @@
</tr>
</tbody>
</table>
</ng-container>
</div>
</cdk-virtual-scroll-viewport>
<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>

View File

@@ -1,6 +1,5 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { SearchPipe } from "jslib-angular/pipes/search.pipe";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
@@ -14,7 +13,6 @@ import { OrganizationUserUserDetailsResponse } from "jslib-common/models/respons
@Component({
selector: "app-entity-users",
templateUrl: "entity-users.component.html",
providers: [SearchPipe],
})
export class EntityUsersComponent implements OnInit {
@Input() entity: "group" | "collection";
@@ -35,7 +33,6 @@ export class EntityUsersComponent implements OnInit {
private allUsers: OrganizationUserUserDetailsResponse[] = [];
constructor(
private search: SearchPipe,
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
@@ -55,14 +52,6 @@ export class EntityUsersComponent implements OnInit {
}
}
get searchedUsers() {
return this.search.transform(this.users, this.searchText, "name", "email", "id");
}
get scrollViewportStyle() {
return `min-height: 120px; height: ${120 + this.searchedUsers.length * 46}px`;
}
async loadUsers() {
const users = await this.apiService.getOrganizationUsers(this.organizationId);
this.allUsers = users.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, "email"));

View File

@@ -12,8 +12,7 @@ import { SearchService } from "jslib-common/abstractions/search.service";
import { Utils } from "jslib-common/misc/utils";
import { GroupResponse } from "jslib-common/models/response/groupResponse";
import { EntityUsersComponent } from "../../modules/organizations/manage/entity-users.component";
import { EntityUsersComponent } from "./entity-users.component";
import { GroupAddEditComponent } from "./group-add-edit.component";
@Component({

View File

@@ -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">&times;</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>

View File

@@ -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());
}
}

View File

@@ -188,10 +188,10 @@
></app-adjust-storage>
</div>
</ng-container>
<!--Switch to i18n-->
<h2 class="spaced-header">{{ "selfHostingTitle" | i18n }}</h2>
<h2 class="spaced-header">{{ "additionalOptions" | i18n }}</h2>
<p class="mb-4">
{{ "selfHostingEnterpriseOrganizationSectionCopy" | i18n }}
{{ "additionalOptionsDesc" | i18n }}
</p>
<div class="d-flex">
<button
@@ -203,27 +203,6 @@
>
{{ "downloadLicense" | i18n }}
</button>
<button
type="button"
class="btn btn-outline-secondary ml-1"
(click)="manageBillingSync()"
*ngIf="canManageBillingSync"
>
{{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }}
</button>
</div>
<div class="mt-3" *ngIf="showDownloadLicense">
<app-download-license
[organizationId]="organizationId"
(onDownloaded)="closeDownloadLicense()"
(onCanceled)="closeDownloadLicense()"
></app-download-license>
</div>
<h2 class="spaced-header">{{ "additionalOptions" | i18n }}</h2>
<p class="mb-4">
{{ "additionalOptionsDesc" | i18n }}
</p>
<div class="d-flex">
<button
#cancelBtn
type="button"
@@ -237,6 +216,13 @@
<span>{{ "cancelSubscription" | i18n }}</span>
</button>
</div>
<div class="mt-3" *ngIf="showDownloadLicense">
<app-download-license
[organizationId]="organizationId"
(onDownloaded)="closeDownloadLicense()"
(onCanceled)="closeDownloadLicense()"
></app-download-license>
</div>
</ng-container>
<ng-container *ngIf="selfHosted">
<dl>
@@ -283,31 +269,5 @@
></app-update-license>
</div>
</div>
<div *ngIf="showBillingSyncKey">
<h2 class="mt-5">
{{ "billingSync" | i18n }}
</h2>
<p>
{{ "billingSyncDesc" | i18n }}
</p>
<button
type="button"
class="btn btn-outline-secondary"
(click)="manageBillingSyncSelfHosted()"
>
{{ "manageBillingSync" | i18n }}
</button>
<small class="form-text text-muted" *ngIf="billingSyncSetUp">
{{ "lastSync" | i18n }}:
<span *ngIf="userOrg.familySponsorshipLastSyncDate != null">
{{ userOrg.familySponsorshipLastSyncDate | date: "medium" }}
</span>
<span *ngIf="userOrg.familySponsorshipLastSyncDate == null">
{{ "never" | i18n | lowercase }}
</span>
</small>
</div>
</ng-container>
</ng-container>
<ng-template #setupBillingSyncTemplate></ng-template>
<ng-template #rotateBillingSyncKeyTemplate></ng-template>

View File

@@ -1,34 +1,21 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ModalRef } from "jslib-angular/components/modal/modal.ref";
import { ModalService } from "jslib-angular/services/modal.service";
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 { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { OrganizationApiKeyType } from "jslib-common/enums/organizationApiKeyType";
import { OrganizationConnectionType } from "jslib-common/enums/organizationConnectionType";
import { PlanType } from "jslib-common/enums/planType";
import { BillingSyncConfigApi } from "jslib-common/models/api/billingSyncConfigApi";
import { Organization } from "jslib-common/models/domain/organization";
import { OrganizationConnectionResponse } from "jslib-common/models/response/organizationConnectionResponse";
import { OrganizationSubscriptionResponse } from "jslib-common/models/response/organizationSubscriptionResponse";
import { BillingSyncKeyComponent } from "src/app/settings/billing-sync-key.component";
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
@Component({
selector: "app-org-subscription",
templateUrl: "organization-subscription.component.html",
})
export class OrganizationSubscriptionComponent implements OnInit {
@ViewChild("setupBillingSyncTemplate", { read: ViewContainerRef, static: true })
setupBillingSyncModalRef: ViewContainerRef;
loading = false;
firstLoaded = false;
organizationId: string;
@@ -38,24 +25,17 @@ export class OrganizationSubscriptionComponent implements OnInit {
adjustStorageAdd = true;
showAdjustStorage = false;
showUpdateLicense = false;
showBillingSyncKey = false;
showDownloadLicense = false;
showChangePlan = false;
sub: OrganizationSubscriptionResponse;
selfHosted = false;
hasBillingSyncToken: boolean;
userOrg: Organization;
existingBillingSyncConnection: OrganizationConnectionResponse<BillingSyncConfigApi>;
removeSponsorshipPromise: Promise<any>;
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
@ViewChild("rotateBillingSyncKeyTemplate", { read: ViewContainerRef, static: true })
billingSyncKeyViewContainerRef: ViewContainerRef;
billingSyncKeyRef: [ModalRef, BillingSyncKeyComponent];
constructor(
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
@@ -63,8 +43,7 @@ export class OrganizationSubscriptionComponent implements OnInit {
private messagingService: MessagingService,
private route: ActivatedRoute,
private organizationService: OrganizationService,
private logService: LogService,
private modalService: ModalService
private logService: LogService
) {
this.selfHosted = platformUtilsService.isSelfHost();
}
@@ -84,27 +63,10 @@ export class OrganizationSubscriptionComponent implements OnInit {
this.loading = true;
this.userOrg = await this.organizationService.get(this.organizationId);
if (this.userOrg.canManageBilling) {
this.sub = await this.apiService.getOrganizationSubscription(this.organizationId);
}
const apiKeyResponse = await this.apiService.getOrganizationApiKeyInformation(
this.organizationId
);
this.hasBillingSyncToken = apiKeyResponse.data.some(
(i) => i.keyType === OrganizationApiKeyType.BillingSync
);
if (this.selfHosted) {
this.showBillingSyncKey = await this.apiService.getCloudCommunicationsEnabled();
}
if (this.showBillingSyncKey) {
this.existingBillingSyncConnection = await this.apiService.getOrganizationConnection(
this.organizationId,
OrganizationConnectionType.CloudBillingSync,
BillingSyncConfigApi
);
}
this.loading = false;
}
@@ -176,20 +138,6 @@ export class OrganizationSubscriptionComponent implements OnInit {
this.showDownloadLicense = !this.showDownloadLicense;
}
async manageBillingSync() {
const [ref] = await this.modalService.openViewRef(
BillingSyncApiKeyComponent,
this.setupBillingSyncModalRef,
(comp) => {
comp.organizationId = this.organizationId;
comp.hasBillingToken = this.hasBillingSyncToken;
}
);
ref.onClosed.subscribe(async () => {
await this.load();
});
}
closeDownloadLicense() {
this.showDownloadLicense = false;
}
@@ -252,24 +200,6 @@ export class OrganizationSubscriptionComponent implements OnInit {
}
}
async manageBillingSyncSelfHosted() {
this.billingSyncKeyRef = await this.modalService.openViewRef(
BillingSyncKeyComponent,
this.billingSyncKeyViewContainerRef,
(comp) => {
comp.entityId = this.organizationId;
comp.existingConnectionId = this.existingBillingSyncConnection?.id;
comp.billingSyncKey = this.existingBillingSyncConnection?.config?.billingSyncKey;
comp.setParentConnection = (
connection: OrganizationConnectionResponse<BillingSyncConfigApi>
) => {
this.existingBillingSyncConnection = connection;
this.billingSyncKeyRef[0].close();
};
}
);
}
get isExpired() {
return (
this.sub != null && this.sub.expiration != null && new Date(this.sub.expiration) < new Date()
@@ -336,16 +266,6 @@ export class OrganizationSubscriptionComponent implements OnInit {
);
}
get canManageBillingSync() {
return (
!this.selfHosted &&
(this.sub.planType === PlanType.EnterpriseAnnually ||
this.sub.planType === PlanType.EnterpriseMonthly ||
this.sub.planType === PlanType.EnterpriseAnnually2019 ||
this.sub.planType === PlanType.EnterpriseMonthly2019)
);
}
get subscriptionDesc() {
if (this.sub.planType === PlanType.Free) {
return this.i18nService.t("subscriptionFreePlan", this.sub.seats.toString());
@@ -373,8 +293,4 @@ export class OrganizationSubscriptionComponent implements OnInit {
get showChangePlanButton() {
return this.subscription == null && this.sub.planType === PlanType.Free && !this.showChangePlan;
}
get billingSyncSetUp() {
return this.existingBillingSyncConnection?.id != null;
}
}

View File

@@ -14,7 +14,7 @@ import { CipherView } from "jslib-common/models/view/cipherView";
import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent } from "../../reports/exposed-passwords-report.component";
@Component({
selector: "app-org-exposed-passwords-report",
selector: "app-exposed-passwords-report",
templateUrl: "../../reports/exposed-passwords-report.component.html",
})
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
@@ -41,10 +41,12 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
}
ngOnInit() {
const dynamicSuper = Object.getPrototypeOf(this.constructor.prototype);
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.organizationService.get(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
await this.checkAccess();
// TODO: We should do something about this, calling super in an async function is bad
dynamicSuper.ngOnInit();
});
}

View File

@@ -58,8 +58,7 @@ export class CiphersComponent extends BaseCiphersComponent {
);
}
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
this.deleted = deleted || false;
async load(filter: (cipher: CipherView) => boolean = null) {
if (this.organization.canEditAnyCollection) {
this.accessEvents = this.organization.useEvents;
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);

View File

@@ -155,11 +155,6 @@ const routes: Routes = [
.IndividualVaultModule,
},
{ path: "sends", component: SendComponent, data: { title: "Send" } },
{
path: "create-organization",
component: CreateOrganizationComponent,
data: { titleId: "newOrganization" },
},
{
path: "settings",
component: SettingsComponent,
@@ -186,6 +181,11 @@ const routes: Routes = [
loadChildren: async () =>
(await import("./settings/subscription-routing.module")).SubscriptionRoutingModule,
},
{
path: "create-organization",
component: CreateOrganizationComponent,
data: { titleId: "newOrganization" },
},
{
path: "emergency-access",
children: [
@@ -229,8 +229,6 @@ const routes: Routes = [
(await import("./reports/reports-routing.module")).ReportsRoutingModule,
},
{ path: "setup/families-for-enterprise", component: FamiliesForEnterpriseSetupComponent },
],
},
{
path: "organizations",
loadChildren: () =>
@@ -238,6 +236,8 @@ const routes: Routes = [
(m) => m.OrganizationsRoutingModule
),
},
],
},
];
@NgModule({

View File

@@ -1,7 +1,6 @@
import { NgModule } from "@angular/core";
import { LooseComponentsModule } from "./modules/loose-components.module";
import { OrganizationManageModule } from "./modules/organizations/manage/organization-manage.module";
import { PipesModule } from "./modules/pipes/pipes.module";
import { SharedModule } from "./modules/shared.module";
import { VaultFilterModule } from "./modules/vault-filter/vault-filter.module";
@@ -14,7 +13,6 @@ import { OrganizationBadgeModule } from "./modules/vault/modules/organization-ba
VaultFilterModule,
OrganizationBadgeModule,
PipesModule,
OrganizationManageModule,
],
exports: [LooseComponentsModule, VaultFilterModule, OrganizationBadgeModule, PipesModule],
bootstrap: [],

View File

@@ -307,9 +307,6 @@ export class EventService {
case EventType.Organization_DisabledKeyConnector:
msg = humanReadableMsg = this.i18nService.t("disabledKeyConnector");
break;
case EventType.Organization_SponsorshipsSynced:
msg = humanReadableMsg = this.i18nService.t("sponsorshipsSynced");
break;
// Policies
case EventType.Policy_Updated: {
msg = this.i18nService.t("modifiedPolicyId", this.formatPolicyId(ev));

View File

@@ -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">&times;</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>

View File

@@ -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);
}
}

View File

@@ -68,7 +68,6 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
if (await this.keyConnectorService.getUsesKeyConnector()) {
this.router.navigate(["/settings/security/two-factor"]);
}
await super.ngOnInit();
}
async rotateEncKeyClicked() {

View File

@@ -1,11 +1,5 @@
<div class="container page-content">
<div class="row">
<div class="col-12">
<div class="page-header">
<div class="page-header">
<h1>{{ "newOrganization" | i18n }}</h1>
</div>
<p>{{ "newOrganizationDesc" | i18n }}</p>
<app-organization-plans></app-organization-plans>
</div>
</div>
</div>
<p>{{ "newOrganizationDesc" | i18n }}</p>
<app-organization-plans></app-organization-plans>

View File

@@ -5,7 +5,6 @@ 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 { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
@@ -69,8 +68,7 @@ export class OrganizationPlansComponent implements OnInit {
private syncService: SyncService,
private policyService: PolicyService,
private organizationService: OrganizationService,
private logService: LogService,
private messagingService: MessagingService
private logService: LogService
) {
this.selfHosted = platformUtilsService.isSelfHost();
}
@@ -300,7 +298,6 @@ export class OrganizationPlansComponent implements OnInit {
this.formPromise = doSubmit();
const organizationId = await this.formPromise;
this.onSuccess.emit({ organizationId: organizationId });
this.messagingService.send("organizationCreated", organizationId);
} catch (e) {
this.logService.error(e);
}

View File

@@ -54,11 +54,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.premium = await this.tokenService.getPremium();
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
const hasPremiumFromOrg = await this.stateService.getCanAccessPremium();
let billing = null;
if (!this.selfHosted) {
billing = await this.apiService.getUserBillingHistory();
}
this.hideSubscription =
!this.premium && hasPremiumFromOrg && (this.selfHosted || billing?.hasNoHistory);
const billing = await this.apiService.getUserBillingHistory();
this.hideSubscription = !this.premium && hasPremiumFromOrg && billing.hasNoHistory;
}
}

View File

@@ -20,55 +20,33 @@
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
[formGroup]="sponsorshipForm"
ngNativeValidate
*ngIf="anyOrgsAvailable"
>
<div class="form-group col-7">
<div *ngIf="moreThanOneOrgAvailable" class="form-group col-7">
<label for="availableSponsorshipOrg">{{ "familiesSponsoringOrgSelect" | i18n }}</label>
<select
id="availableSponsorshipOrg"
name="Available Sponsorship Organization"
formControlName="selectedSponsorshipOrgId"
[(ngModel)]="selectedSponsorshipOrgId"
class="form-control"
required
>
<option disabled="true" value="">-- {{ "select" | i18n }} --</option>
<option value="">-- {{ "select" | i18n }} --</option>
<option *ngFor="let o of availableSponsorshipOrgs" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div>
<div class="form-group col-7">
<label for="sponsorshipEmail">{{ "sponsoredFamiliesEmail" | i18n }}:</label>
<label for="accountEmail">{{ "sponsoredFamiliesEmail" | i18n }}:</label>
<input
id="sponsorshipEmail"
id="accountEmail"
class="form-control"
inputmode="email"
formControlName="sponsorshipEmail"
[(ngModel)]="sponsorshipEmail"
name="sponsorshipEmail"
required
[attr.aria-invalid]="sponsorshipEmailControl.invalid"
/>
<small
aria-errormessage="sponsorshipEmail"
*ngIf="sponsorshipEmailControl.errors?.notAllowedValue"
class="error-inline"
role="alert"
>
<i class="bwi bwi-error" aria-hidden="true"></i>
{{ "cannotSponsorSelf" | i18n }}
</small>
<small
aria-errormessage="sponsorshipEmail"
*ngIf="sponsorshipEmailControl.errors?.email"
class="error-inline"
role="alert"
>
<i class="bwi bwi-error" aria-hidden="true"></i>
{{ "invalidEmail" | i18n }}
</small>
</div>
<div class="form-group col-7">
<button class="btn btn-primary btn-submit mt-2" type="submit" [disabled]="form.loading">
<button class="btn btn-primary btn-submit mt-4" type="submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "redeem" | i18n }}</span>
</button>
@@ -81,18 +59,12 @@
<tr>
<th>{{ "recipient" | i18n }}</th>
<th>{{ "sponsoringOrg" | i18n }}</th>
<th>{{ "status" | i18n }}</th>
<th></th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let o of activeSponsorshipOrgs">
<tr
sponsoring-org-row
[sponsoringOrg]="o"
[isSelfHosted]="isSelfHosted"
(sponsorshipRemoved)="load(true)"
></tr>
<tr sponsoring-org-row [sponsoringOrg]="o" (sponsorshipRemoved)="load(true)"></tr>
</ng-container>
</tbody>
</table>

View File

@@ -1,12 +1,9 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { notAllowedValueAsync } from "jslib-angular/validators/notAllowedValueAsync.validator";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { PlanSponsorshipType } from "jslib-common/enums/planSponsorshipType";
import { Organization } from "jslib-common/models/domain/organization";
@@ -20,54 +17,30 @@ export class SponsoredFamiliesComponent implements OnInit {
availableSponsorshipOrgs: Organization[] = [];
activeSponsorshipOrgs: Organization[] = [];
selectedSponsorshipOrgId = "";
sponsorshipEmail = "";
// Conditional display properties
formPromise: Promise<any>;
sponsorshipForm: FormGroup;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private syncService: SyncService,
private organizationService: OrganizationService,
private formBuilder: FormBuilder,
private stateService: StateService
) {
this.sponsorshipForm = this.formBuilder.group({
selectedSponsorshipOrgId: [
"",
{
validators: [Validators.required],
},
],
sponsorshipEmail: [
"",
{
validators: [Validators.email],
asyncValidators: [
notAllowedValueAsync(async () => await this.stateService.getEmail(), true),
],
updateOn: "blur",
},
],
});
}
private organizationService: OrganizationService
) {}
async ngOnInit() {
await this.load();
}
async submit() {
this.formPromise = this.apiService.postCreateSponsorship(
this.sponsorshipForm.value.selectedSponsorshipOrgId,
{
sponsoredEmail: this.sponsorshipForm.value.sponsorshipEmail,
this.formPromise = this.apiService.postCreateSponsorship(this.selectedSponsorshipOrgId, {
sponsoredEmail: this.sponsorshipEmail,
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
friendlyName: this.sponsorshipForm.value.sponsorshipEmail,
}
);
friendlyName: this.sponsorshipEmail,
});
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("sponsorshipCreated"));
@@ -94,19 +67,14 @@ export class SponsoredFamiliesComponent implements OnInit {
);
if (this.availableSponsorshipOrgs.length === 1) {
this.sponsorshipForm.patchValue({
selectedSponsorshipOrgId: this.availableSponsorshipOrgs[0].id,
});
this.selectedSponsorshipOrgId = this.availableSponsorshipOrgs[0].id;
}
this.loading = false;
}
get sponsorshipEmailControl() {
return this.sponsorshipForm.controls["sponsorshipEmail"];
}
private async resetForm() {
this.sponsorshipForm.reset();
this.sponsorshipEmail = "";
this.selectedSponsorshipOrgId = "";
}
get anyActiveSponsorships(): boolean {
@@ -117,7 +85,7 @@ export class SponsoredFamiliesComponent implements OnInit {
return this.availableSponsorshipOrgs.length > 0;
}
get isSelfHosted(): boolean {
return this.platformUtilsService.isSelfHost();
get moreThanOneOrgAvailable(): boolean {
return this.availableSponsorshipOrgs.length > 1;
}
}

View File

@@ -2,13 +2,9 @@
{{ sponsoringOrg.familySponsorshipFriendlyName }}
</td>
<td>{{ sponsoringOrg.name }}</td>
<td>
<span [ngClass]="statusClass">{{ statusMessage }}</span>
</td>
<td class="table-action-right">
<div class="dropdown" appListDropdown>
<button
*ngIf="!sponsoringOrg.familySponsorshipToDelete"
class="btn btn-outline-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton"
@@ -22,7 +18,6 @@
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<button
#resendEmailBtn
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
[appApiAction]="resendEmailPromise"
class="dropdown-item btn-submit"
[disabled]="resendEmailBtn.loading"

View File

@@ -1,5 +1,4 @@
import { formatDate } from "@angular/common";
import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
@@ -11,15 +10,11 @@ import { Organization } from "jslib-common/models/domain/organization";
selector: "[sponsoring-org-row]",
templateUrl: "sponsoring-org-row.component.html",
})
export class SponsoringOrgRowComponent implements OnInit {
export class SponsoringOrgRowComponent {
@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>;
@@ -30,15 +25,6 @@ export class SponsoringOrgRowComponent implements OnInit {
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();
@@ -57,10 +43,6 @@ export class SponsoringOrgRowComponent implements OnInit {
this.resendEmailPromise = null;
}
get isSentAwaitingSync() {
return this.isSelfHosted && !this.sponsoringOrg.familySponsorshipLastSyncDate;
}
private async doRevokeSponsorship() {
const isConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("revokeSponsorshipConfirmation"),
@@ -78,53 +60,4 @@ export class SponsoringOrgRowComponent implements OnInit {
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";
}
}
}

View File

@@ -73,15 +73,11 @@ export class TaxInfoComponent {
this.logService.error(e);
}
} else {
try {
const taxInfo = await this.apiService.getTaxInfo();
if (taxInfo) {
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || "US";
}
} catch (e) {
this.logService.error(e);
}
}
this.pristine = Object.assign({}, this.taxInfo);
// If not the default (US) then trigger onCountryChanged
@@ -90,17 +86,10 @@ export class TaxInfoComponent {
}
});
try {
const taxRates = await this.apiService.getTaxRates();
if (taxRates) {
this.taxRates = taxRates.data;
}
} catch (e) {
this.logService.error(e);
} finally {
this.loading = false;
}
}
get taxRate() {
if (this.taxRates != null) {

View File

@@ -295,6 +295,16 @@
(blur)="saveUsernameOptions()"
/>
</div>
<div class="form-group col-4">
<label for="simplelogin-hostname">{{ "hostname" | i18n }}</label>
<input
id="simplelogin-hostname"
class="form-control"
type="text"
[(ngModel)]="usernameOptions.forwardedSimpleLoginHostname"
(blur)="saveUsernameOptions()"
/>
</div>
</div>
<div class="row" *ngIf="usernameOptions.forwardedService === 'anonaddy'">
<div class="form-group col-4">

View File

@@ -50,8 +50,7 @@
organizationName="{{ c.organizationId | orgNameFromId: organizations }}"
profileName="{{ profileName }}"
(onOrganizationClicked)="onOrganizationClicked(c.organizationId)"
>
</app-org-badge>
></app-org-badge>
</td>
<td class="table-list-options">
<button

View File

@@ -77,7 +77,7 @@
</button>
<a
href="#"
routerLink="/create-organization"
routerLink="/settings/create-organization"
class="btn btn-primary"
*ngIf="!organizations || !organizations.length"
>

View File

@@ -4594,7 +4594,7 @@
"message": "Enter your personal email to redeem Bitwarden Families"
},
"sponsoredFamiliesLeaveCopy": {
"message": "If you remove an offer or are removed from the sponsoring organization, your Families sponsorship will expire at the next renewal date."
"message": "If you leave or are removed from the sponsoring organization, your Families plan will expire at the end of the billing period."
},
"acceptBitwardenFamiliesHelp": {
"message": "Accept offer for an existing organization or create a new Families organization."
@@ -4669,7 +4669,7 @@
"message": "Email Sent"
},
"revokeSponsorshipConfirmation": {
"message": "After removing this account, the Families plan sponsorship will expire at the end of the billing period. You will not be able to redeem a new sponsorship offer until the existing one expires. Are you sure you want to continue?"
"message": "After removing this account, the Families organization owner will be responsible for this subscription and related invoices. Are you sure you want to continue?"
},
"removeSponsorshipSuccess": {
"message": "Sponsorship Removed"
@@ -4803,75 +4803,6 @@
"freeWithSponsorship": {
"message": "FREE with sponsorship"
},
"viewBillingSyncToken": {
"message": "View Billing Sync Token"
},
"generateBillingSyncToken": {
"message": "Generate Billing Sync Token"
},
"copyPasteBillingSync": {
"message": "Copy and paste this token into the Billing Sync settings of your self-hosted organization."
},
"billingSyncCanAccess": {
"message": "Your Billing Sync token can access and edit this organization's subscription settings."
},
"manageBillingSync": {
"message": "Manage Billing Sync"
},
"setUpBillingSync": {
"message": "Set Up Billing Sync"
},
"generateToken": {
"message": "Generate Token"
},
"rotateToken": {
"message": "Rotate Token"
},
"rotateBillingSyncTokenWarning": {
"message": "If you proceed, you will need to re-setup billing sync on your self-hosted server."
},
"rotateBillingSyncTokenTitle": {
"message": "Rotating the Billing Sync Token will invalidate the previous token."
},
"selfHostingTitle": {
"message": "Self-Hosting"
},
"selfHostingEnterpriseOrganizationSectionCopy": {
"message": "To set-up your organization on your own server, you will need to upload your license file. To support Free Families plans and advanced billing capabilities for your self-hosted organization, you will need to set up billing sync."
},
"billingSyncApiKeyRotated": {
"message": "Token rotated."
},
"billingSync": {
"message": "Billing Sync"
},
"billingSyncDesc": {
"message": "Billing Sync provides Free Families plans for members and advanced billing capabilities by linking your self-hosted Bitwarden to the Bitwarden cloud server."
},
"billingSyncKeyDesc": {
"message": "A Billing Sync Token from your cloud organization's subscription settings is required to complete this form."
},
"billingSyncKey": {
"message": "Billing Sync Token"
},
"active": {
"message": "Active"
},
"inactive": {
"message": "Inactive"
},
"sentAwaitingSync": {
"message": "Sent (Awaiting Sync)"
},
"sent": {
"message": "Sent"
},
"requestRemoved": {
"message": "Removed (Awaiting Sync)"
},
"requested": {
"message": "Requested"
},
"formErrorSummaryPlural": {
"message": "$COUNT$ fields above need your attention.",
"placeholders": {
@@ -5004,46 +4935,6 @@
"service": {
"message": "Service"
},
"unknownCipher": {
"message": "Unknown Item, you may need to request permission to access this item."
},
"cannotSponsorSelf": {
"message": "You cannot redeem for the active account. Enter a different email."
},
"revokeWhenExpired": {
"message": "Expires $DATE$",
"placeholders": {
"date": {
"content": "$1",
"example": "12/31/2020"
}
}
},
"awaitingSyncSingular": {
"message": "Token rotated $DAYS$ day ago. Update the billing sync token in your self-hosted organization settings.",
"placeholders": {
"days": {
"content": "$1",
"example": "1"
}
}
},
"awaitingSyncPlural": {
"message": "Token rotated $DAYS$ days ago. Update the billing sync token in your self-hosted organization settings.",
"placeholders": {
"days": {
"content": "$1",
"example": "1"
}
}
},
"lastSync": {
"message": "Last Sync",
"Description": "Used as a prefix to indicate the last time a sync occured. Example \"Last sync 1968-11-16 00:00:00\""
},
"sponsorshipsSynced": {
"message": "Self-hosted sponsorships synced."
},
"billingManagedByProvider": {
"message": "Managed by $PROVIDER$",
"placeholders": {
@@ -5069,5 +4960,8 @@
},
"apiAccessToken": {
"message": "API Access Token"
},
"unknownCipher": {
"message": "Unknown Item, you may need to login with another account to access this item."
}
}

View File

@@ -68,6 +68,7 @@
}
.modal-footer {
border-radius: 0.3rem 0.3rem 0 0;
justify-content: flex-start;
@include themify($themes) {
background-color: themed("footerBackgroundColor");

View File

@@ -28,6 +28,8 @@
h3,
button.filter-button {
text-transform: uppercase;
text-transform: uppercase;
margin: 0;
@include themify($themes) {
color: themed("textMuted");
@@ -116,7 +118,6 @@
}
text-decoration: none;
}
max-width: 90%;
}
.edit-button {

View File

@@ -204,60 +204,8 @@ const devServer =
return [
{
key: "Content-Security-Policy",
value: `
default-src 'self';
script-src
'self'
'sha256-ryoU+5+IUZTuUyTElqkrQGBJXr1brEv6r2CA62WUw8w='
https://js.stripe.com
https://js.braintreegateway.com
https://www.paypalobjects.com;
style-src
'self'
https://assets.braintreegateway.com
https://*.paypal.com
'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='
'sha256-JVRXyYPueLWdwGwY9m/7u4QlZ1xeQdqUj2t8OVIzZE4=';
'sha256-0xHKHIT3+e2Gknxsm/cpErSprhL+o254L/y5bljg74U='
img-src
'self'
data:
https://icons.bitwarden.net
https://*.paypal.com
https://www.paypalobjects.com
https://q.stripe.com
https://haveibeenpwned.com
https://www.gravatar.com;
child-src
'self'
https://js.stripe.com
https://assets.braintreegateway.com
https://*.paypal.com
https://*.duosecurity.com;
frame-src
'self'
https://js.stripe.com
https://assets.braintreegateway.com
https://*.paypal.com
https://*.duosecurity.com;
connect-src
'self'
wss://notifications.bitwarden.com
https://notifications.bitwarden.com
https://cdn.bitwarden.net
https://api.pwnedpasswords.com
https://2fa.directory/api/v3/totp.json
https://api.stripe.com
https://www.paypal.com
https://api.braintreegateway.com
https://client-analytics.braintreegateway.com
https://*.braintree-api.com
https://*.blob.core.windows.net
https://app.simplelogin.io/api/alias/random/new
https://app.anonaddy.com/api/v1/aliases;
object-src
'self'
blob:;`,
value:
"default-src 'self'; script-src 'self' 'sha256-ryoU+5+IUZTuUyTElqkrQGBJXr1brEv6r2CA62WUw8w=' https://js.stripe.com https://js.braintreegateway.com https://www.paypalobjects.com; style-src 'self' https://assets.braintreegateway.com https://*.paypal.com 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' 'sha256-JVRXyYPueLWdwGwY9m/7u4QlZ1xeQdqUj2t8OVIzZE4='; img-src 'self' data: https://icons.bitwarden.net https://*.paypal.com https://www.paypalobjects.com https://q.stripe.com https://haveibeenpwned.com https://www.gravatar.com; child-src 'self' https://js.stripe.com https://assets.braintreegateway.com https://*.paypal.com https://*.duosecurity.com; frame-src 'self' https://js.stripe.com https://assets.braintreegateway.com https://*.paypal.com https://*.duosecurity.com; connect-src 'self' wss://notifications.bitwarden.com https://notifications.bitwarden.com https://cdn.bitwarden.net https://api.pwnedpasswords.com https://2fa.directory/api/v3/totp.json https://api.stripe.com https://www.paypal.com https://api.braintreegateway.com https://client-analytics.braintreegateway.com https://*.braintree-api.com https://*.blob.core.windows.net https://app.simplelogin.io/api/alias/random/new https://app.anonaddy.com/api/v1/aliases; object-src 'self' blob:;",
},
];
}