1
0
mirror of https://github.com/bitwarden/web synced 2025-12-12 06:13:28 +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
70 changed files with 1342 additions and 1588 deletions

View File

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

View File

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

View File

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

View File

@@ -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
.gitignore vendored
View File

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

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, component: ManageComponent,
canActivate: [PermissionsGuard], canActivate: [PermissionsGuard],
data: { data: {
permissions: NavigationPermissionsService.getPermissions("manage").concat( permissions: [
Permissions.ManageSso NavigationPermissionsService.getPermissions("manage").concat(Permissions.ManageSso),
), ],
}, },
children: [ 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: 1cc4bed671...00deb38de5

1306
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@bitwarden/web-vault", "name": "@bitwarden/web-vault",
"version": "2022.05.0", "version": "2.28.1",
"license": "GPL-3.0", "license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web", "repository": "https://github.com/bitwarden/web",
"scripts": { "scripts": {

View File

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

View File

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

View File

@@ -71,7 +71,7 @@ export class RegisterComponent extends BaseRegisterComponent {
} else if (qParams.org != null) { } else if (qParams.org != null) {
this.showCreateOrgMessage = true; this.showCreateOrgMessage = true;
this.referenceData.flow = qParams.org; this.referenceData.flow = qParams.org;
const route = this.router.createUrlTree(["create-organization"], { const route = this.router.createUrlTree(["settings/create-organization"], {
queryParams: { plan: qParams.org }, queryParams: { plan: qParams.org },
}); });
this.routerService.setPreviousUrl(route.toString()); 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 userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = await this.eventService.getEventInfo(r); const eventInfo = await this.eventService.getEventInfo(r);
const user = this.getUserName(r, userId); const user = this.getUserName(r, userId);
const userName = user != null ? user.name : this.i18nService.t("unknown");
return new EventView({ return new EventView({
message: eventInfo.message, message: eventInfo.message,
humanReadableMessage: eventInfo.humanReadableMessage, humanReadableMessage: eventInfo.humanReadableMessage,
appIcon: eventInfo.appIcon, appIcon: eventInfo.appIcon,
appName: eventInfo.appName, appName: eventInfo.appName,
userId: userId, userId: userId,
userName: r.installationId != null ? `Installation: ${r.installationId}` : userName, userName: user != null ? user.name : this.i18nService.t("unknown"),
userEmail: user != null ? user.email : "", userEmail: user != null ? user.email : "",
date: r.date, date: r.date,
ip: r.ipAddress, ip: r.ipAddress,
type: r.type, type: r.type,
installationId: r.installationId,
}); });
}) })
); );

View File

@@ -58,7 +58,7 @@
</li> </li>
<bit-menu-divider></bit-menu-divider> <bit-menu-divider></bit-menu-divider>
<li class="tw-list-none" role="none"> <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> <i class="bwi bwi-plus mr-2"></i>
{{ "newOrganization" | i18n }}</a {{ "newOrganization" | i18n }}</a
> >

View File

@@ -38,7 +38,7 @@
<li> <li>
<button <button
[bitMenuTriggerFor]="accountMenu" [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-user-circle bwi-lg" aria-hidden="true"></i>
<i class="bwi bwi-caret-down bwi-sm" 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 { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service"; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service"; import { OrganizationService } from "jslib-common/abstractions/organization.service";
@@ -32,9 +31,7 @@ export class NavbarComponent implements OnInit {
private providerService: ProviderService, private providerService: ProviderService,
private syncService: SyncService, private syncService: SyncService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private i18nService: I18nService, private i18nService: I18nService
private broadcasterService: BroadcasterService,
private ngZone: NgZone
) { ) {
this.selfHosted = this.platformUtilsService.isSelfHost(); this.selfHosted = this.platformUtilsService.isSelfHost();
} }
@@ -52,24 +49,8 @@ export class NavbarComponent implements OnInit {
} }
this.providers = await this.providerService.getAll(); 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(); const allOrgs = await this.organizationService.getAll();
return allOrgs this.organizations = allOrgs
.filter((org) => OrgNavigationPermissionsService.canAccessAdmin(org)) .filter((org) => OrgNavigationPermissionsService.canAccessAdmin(org))
.sort(Utils.getSortFunction(this.i18nService, "name")); .sort(Utils.getSortFunction(this.i18nService, "name"));
} }

View File

@@ -58,7 +58,6 @@ import { SingleOrgPolicyComponent } from "../organizations/policies/single-org.c
import { TwoFactorAuthenticationPolicyComponent } from "../organizations/policies/two-factor-authentication.component"; import { TwoFactorAuthenticationPolicyComponent } from "../organizations/policies/two-factor-authentication.component";
import { AccountComponent as OrgAccountComponent } from "../organizations/settings/account.component"; import { AccountComponent as OrgAccountComponent } from "../organizations/settings/account.component";
import { AdjustSubscription } from "../organizations/settings/adjust-subscription.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 { ChangePlanComponent } from "../organizations/settings/change-plan.component";
import { DeleteOrganizationComponent } from "../organizations/settings/delete-organization.component"; import { DeleteOrganizationComponent } from "../organizations/settings/delete-organization.component";
import { DownloadLicenseComponent } from "../organizations/settings/download-license.component"; import { DownloadLicenseComponent } from "../organizations/settings/download-license.component";
@@ -67,7 +66,6 @@ import { OrganizationBillingComponent } from "../organizations/settings/organiza
import { OrganizationSubscriptionComponent } from "../organizations/settings/organization-subscription.component"; import { OrganizationSubscriptionComponent } from "../organizations/settings/organization-subscription.component";
import { SettingsComponent as OrgSettingComponent } from "../organizations/settings/settings.component"; import { SettingsComponent as OrgSettingComponent } from "../organizations/settings/settings.component";
import { TwoFactorSetupComponent as OrgTwoFactorSetupComponent } from "../organizations/settings/two-factor-setup.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 { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
import { ExportComponent as OrgExportComponent } from "../organizations/tools/export.component"; import { ExportComponent as OrgExportComponent } from "../organizations/tools/export.component";
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../organizations/tools/exposed-passwords-report.component"; import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../organizations/tools/exposed-passwords-report.component";
@@ -100,7 +98,6 @@ import { AddCreditComponent } from "../settings/add-credit.component";
import { AdjustPaymentComponent } from "../settings/adjust-payment.component"; import { AdjustPaymentComponent } from "../settings/adjust-payment.component";
import { AdjustStorageComponent } from "../settings/adjust-storage.component"; import { AdjustStorageComponent } from "../settings/adjust-storage.component";
import { ApiKeyComponent } from "../settings/api-key.component"; import { ApiKeyComponent } from "../settings/api-key.component";
import { BillingSyncKeyComponent } from "../settings/billing-sync-key.component";
import { ChangeEmailComponent } from "../settings/change-email.component"; import { ChangeEmailComponent } from "../settings/change-email.component";
import { ChangeKdfComponent } from "../settings/change-kdf.component"; import { ChangeKdfComponent } from "../settings/change-kdf.component";
import { ChangePasswordComponent } from "../settings/change-password.component"; import { ChangePasswordComponent } from "../settings/change-password.component";
@@ -115,6 +112,7 @@ import { EmergencyAccessTakeoverComponent } from "../settings/emergency-access-t
import { EmergencyAccessViewComponent } from "../settings/emergency-access-view.component"; import { EmergencyAccessViewComponent } from "../settings/emergency-access-view.component";
import { EmergencyAccessComponent } from "../settings/emergency-access.component"; import { EmergencyAccessComponent } from "../settings/emergency-access.component";
import { EmergencyAddEditComponent } from "../settings/emergency-add-edit.component"; import { EmergencyAddEditComponent } from "../settings/emergency-add-edit.component";
import { LinkSsoComponent } from "../settings/link-sso.component";
import { OrganizationPlansComponent } from "../settings/organization-plans.component"; import { OrganizationPlansComponent } from "../settings/organization-plans.component";
import { PaymentMethodComponent } from "../settings/payment-method.component"; import { PaymentMethodComponent } from "../settings/payment-method.component";
import { PaymentComponent } from "../settings/payment.component"; import { PaymentComponent } from "../settings/payment.component";
@@ -173,7 +171,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
declarations: [ declarations: [
PremiumBadgeComponent, PremiumBadgeComponent,
AcceptEmergencyComponent, AcceptEmergencyComponent,
AcceptFamilySponsorshipComponent,
AcceptOrganizationComponent, AcceptOrganizationComponent,
AccessComponent, AccessComponent,
AccountComponent, AccountComponent,
@@ -186,8 +183,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
AdjustSubscription, AdjustSubscription,
ApiKeyComponent, ApiKeyComponent,
AttachmentsComponent, AttachmentsComponent,
BillingSyncApiKeyComponent,
BillingSyncKeyComponent,
BreachReportComponent, BreachReportComponent,
BulkActionsComponent, BulkActionsComponent,
BulkDeleteComponent, BulkDeleteComponent,
@@ -223,6 +218,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
HintComponent, HintComponent,
ImportComponent, ImportComponent,
InactiveTwoFactorReportComponent, InactiveTwoFactorReportComponent,
LinkSsoComponent,
LockComponent, LockComponent,
LoginComponent, LoginComponent,
MasterPasswordPolicyComponent, MasterPasswordPolicyComponent,
@@ -383,6 +379,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
HintComponent, HintComponent,
ImportComponent, ImportComponent,
InactiveTwoFactorReportComponent, InactiveTwoFactorReportComponent,
LinkSsoComponent,
LockComponent, LockComponent,
LoginComponent, LoginComponent,
MasterPasswordPolicyComponent, MasterPasswordPolicyComponent,

View File

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

View File

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

View File

@@ -12,9 +12,9 @@
</li> </li>
<li class="filter-option"> <li class="filter-option">
<span class="filter-buttons"> <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> <i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
&nbsp;{{ "newOrganization" | i18n }} {{ "newOrganization" | i18n }}
</a> </a>
</span> </span>
</li> </li>
@@ -45,6 +45,14 @@
> >
&nbsp;{{ organizationGrouping.name | i18n }} &nbsp;{{ organizationGrouping.name | i18n }}
</button> </button>
<a
href="#"
routerLink="/settings/create-organization"
class="text-muted ml-auto create-organization-link"
appA11yTitle="{{ 'addOrganization' | i18n }}"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</a>
</div> </div>
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options"> <ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options">
<li <li
@@ -67,25 +75,19 @@
</ng-container> </ng-container>
</span> </span>
</li> </li>
<li class="filter-option">
<span class="filter-buttons">
<a href="#" routerLink="/create-organization" class="filter-button">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
&nbsp;{{ "newOrganization" | i18n }}
</a>
</span>
</li>
</ul> </ul>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'singleOrganizationAndPersonalOwnershipPolicies'"> <ng-container *ngSwitchCase="'singleOrganizationAndPersonalOwnershipPolicies'">
<div class="filter-heading"> <ul class="filter-options">
<button class="filter-button active"> <li class="filter-option active">
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i> <button class="filter-button">
{{ organizations[0].name }} <i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
</button> {{ organizations[0].name }}
</div> </button>
</li>
</ul>
</ng-container> </ng-container>
<ng-container *ngSwitchDefault> <ng-container *ngSwitchCase="'organizationMember'">
<div class="filter-heading"> <div class="filter-heading">
<button <button
class="toggle-button" class="toggle-button"
@@ -110,6 +112,14 @@
> >
&nbsp;{{ organizationGrouping.name | i18n }} &nbsp;{{ organizationGrouping.name | i18n }}
</button> </button>
<a
href="#"
routerLink="/settings/create-organization"
class="text-muted ml-auto create-organization-link"
appA11yTitle="{{ 'addOrganization' | i18n }}"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</a>
</div> </div>
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options"> <ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options">
<li class="filter-option" [ngClass]="{ active: activeFilter.myVaultOnly }"> <li class="filter-option" [ngClass]="{ active: activeFilter.myVaultOnly }">
@@ -130,7 +140,7 @@
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i> <i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
{{ organization.name }} {{ organization.name }}
</button> </button>
<ng-container> <ng-container *ngIf="organization.id === activeFilter.selectedOrganizationId">
<button [bitMenuTriggerFor]="orgMenu" class="org-options ml-auto"> <button [bitMenuTriggerFor]="orgMenu" class="org-options ml-auto">
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i> <i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
</button> </button>
@@ -140,14 +150,6 @@
</ng-container> </ng-container>
</span> </span>
</li> </li>
<li class="filter-option" *ngIf="!(displayMode === 'singleOrganizationPolicy')">
<span class="filter-buttons">
<a href="#" routerLink="/create-organization" class="filter-button">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
&nbsp;{{ "newOrganization" | i18n }}
</a>
</span>
</li>
</ul> </ul>
</ng-container> </ng-container>
</ng-container> </ng-container>

View File

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

View File

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

View File

@@ -26,17 +26,19 @@
autocomplete="off" autocomplete="off"
appAutofocus appAutofocus
/> />
<app-organization-filter <div class="filter">
*ngIf="showOrgFilter" <app-organization-filter
[hide]="hideOrganizations" *ngIf="showOrgFilter"
[activeFilter]="activeFilter" [hide]="hideOrganizations"
[collapsedFilterNodes]="collapsedFilterNodes" [activeFilter]="activeFilter"
[organizations]="organizations" [collapsedFilterNodes]="collapsedFilterNodes"
[activePersonalOwnershipPolicy]="activePersonalOwnershipPolicy" [organizations]="organizations"
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy" [activePersonalOwnershipPolicy]="activePersonalOwnershipPolicy"
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)" [activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
(onFilterChange)="applyFilter($event)" (onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
></app-organization-filter> (onFilterChange)="applyFilter($event)"
></app-organization-filter>
</div>
<div class="filter"> <div class="filter">
<app-status-filter <app-status-filter
[hideFavorites]="!showFavorites" [hideFavorites]="!showFavorites"

View File

@@ -32,10 +32,6 @@ export class VaultFilterComponent extends BaseVaultFilterComponent {
// It should be removed as soon as doing so makes sense. // It should be removed as soon as doing so makes sense.
async reloadOrganizations() { async reloadOrganizations() {
this.organizations = await this.vaultFilterService.buildOrganizations(); this.organizations = await this.vaultFilterService.buildOrganizations();
this.activePersonalOwnershipPolicy =
await this.vaultFilterService.checkForPersonalOwnershipPolicy();
this.activeSingleOrganizationPolicy =
await this.vaultFilterService.checkForSingleOrganizationPolicy();
} }
async initCollections() { async initCollections() {

View File

@@ -12,7 +12,6 @@ import { SharedModule } from "../shared.module";
import { CollectionFilterComponent } from "./components/collection-filter.component"; import { CollectionFilterComponent } from "./components/collection-filter.component";
import { FolderFilterComponent } from "./components/folder-filter.component"; import { FolderFilterComponent } from "./components/folder-filter.component";
import { LinkSsoComponent } from "./components/link-sso.component";
import { OrganizationFilterComponent } from "./components/organization-filter.component"; import { OrganizationFilterComponent } from "./components/organization-filter.component";
import { OrganizationOptionsComponent } from "./components/organization-options.component"; import { OrganizationOptionsComponent } from "./components/organization-options.component";
import { StatusFilterComponent } from "./components/status-filter.component"; import { StatusFilterComponent } from "./components/status-filter.component";
@@ -29,7 +28,6 @@ import { VaultFilterComponent } from "./vault-filter.component";
OrganizationOptionsComponent, OrganizationOptionsComponent,
StatusFilterComponent, StatusFilterComponent,
TypeFilterComponent, TypeFilterComponent,
LinkSsoComponent,
], ],
exports: [VaultFilterComponent], exports: [VaultFilterComponent],
providers: [ providers: [

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { Injectable } from "@angular/core"; 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 { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service"; import { OrganizationService } from "jslib-common/abstractions/organization.service";
@@ -17,7 +17,7 @@ export class PermissionsGuard implements CanActivate {
private syncService: SyncService private syncService: SyncService
) {} ) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { async canActivate(route: ActivatedRouteSnapshot) {
// TODO: We need to fix this issue once and for all. // TODO: We need to fix this issue once and for all.
if ((await this.syncService.getLastSync()) == null) { if ((await this.syncService.getLastSync()) == null) {
await this.syncService.fullSync(false); await this.syncService.fullSync(false);
@@ -39,16 +39,6 @@ export class PermissionsGuard implements CanActivate {
const permissions = route.data == null ? [] : (route.data.permissions as Permissions[]); const permissions = route.data == null ? [] : (route.data.permissions as Permissions[]);
if (permissions != null && !org.hasAnyPermission(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")); this.platformUtilsService.showToast("error", null, this.i18nService.t("accessDenied"));
return this.router.createUrlTree(["/"]); return this.router.createUrlTree(["/"]);
} }

View File

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

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> ></app-adjust-storage>
</div> </div>
</ng-container> </ng-container>
<!--Switch to i18n-->
<h2 class="spaced-header">{{ "selfHostingTitle" | i18n }}</h2> <h2 class="spaced-header">{{ "additionalOptions" | i18n }}</h2>
<p class="mb-4"> <p class="mb-4">
{{ "selfHostingEnterpriseOrganizationSectionCopy" | i18n }} {{ "additionalOptionsDesc" | i18n }}
</p> </p>
<div class="d-flex"> <div class="d-flex">
<button <button
@@ -203,27 +203,6 @@
> >
{{ "downloadLicense" | i18n }} {{ "downloadLicense" | i18n }}
</button> </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 <button
#cancelBtn #cancelBtn
type="button" type="button"
@@ -237,6 +216,13 @@
<span>{{ "cancelSubscription" | i18n }}</span> <span>{{ "cancelSubscription" | i18n }}</span>
</button> </button>
</div> </div>
<div class="mt-3" *ngIf="showDownloadLicense">
<app-download-license
[organizationId]="organizationId"
(onDownloaded)="closeDownloadLicense()"
(onCanceled)="closeDownloadLicense()"
></app-download-license>
</div>
</ng-container> </ng-container>
<ng-container *ngIf="selfHosted"> <ng-container *ngIf="selfHosted">
<dl> <dl>
@@ -283,31 +269,5 @@
></app-update-license> ></app-update-license>
</div> </div>
</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-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 { 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 { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service"; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service"; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service"; import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.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 { PlanType } from "jslib-common/enums/planType";
import { BillingSyncConfigApi } from "jslib-common/models/api/billingSyncConfigApi";
import { Organization } from "jslib-common/models/domain/organization"; 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 { 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({ @Component({
selector: "app-org-subscription", selector: "app-org-subscription",
templateUrl: "organization-subscription.component.html", templateUrl: "organization-subscription.component.html",
}) })
export class OrganizationSubscriptionComponent implements OnInit { export class OrganizationSubscriptionComponent implements OnInit {
@ViewChild("setupBillingSyncTemplate", { read: ViewContainerRef, static: true })
setupBillingSyncModalRef: ViewContainerRef;
loading = false; loading = false;
firstLoaded = false; firstLoaded = false;
organizationId: string; organizationId: string;
@@ -38,24 +25,17 @@ export class OrganizationSubscriptionComponent implements OnInit {
adjustStorageAdd = true; adjustStorageAdd = true;
showAdjustStorage = false; showAdjustStorage = false;
showUpdateLicense = false; showUpdateLicense = false;
showBillingSyncKey = false;
showDownloadLicense = false; showDownloadLicense = false;
showChangePlan = false; showChangePlan = false;
sub: OrganizationSubscriptionResponse; sub: OrganizationSubscriptionResponse;
selfHosted = false; selfHosted = false;
hasBillingSyncToken: boolean;
userOrg: Organization; userOrg: Organization;
existingBillingSyncConnection: OrganizationConnectionResponse<BillingSyncConfigApi>;
removeSponsorshipPromise: Promise<any>; removeSponsorshipPromise: Promise<any>;
cancelPromise: Promise<any>; cancelPromise: Promise<any>;
reinstatePromise: Promise<any>; reinstatePromise: Promise<any>;
@ViewChild("rotateBillingSyncKeyTemplate", { read: ViewContainerRef, static: true })
billingSyncKeyViewContainerRef: ViewContainerRef;
billingSyncKeyRef: [ModalRef, BillingSyncKeyComponent];
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
@@ -63,8 +43,7 @@ export class OrganizationSubscriptionComponent implements OnInit {
private messagingService: MessagingService, private messagingService: MessagingService,
private route: ActivatedRoute, private route: ActivatedRoute,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private logService: LogService, private logService: LogService
private modalService: ModalService
) { ) {
this.selfHosted = platformUtilsService.isSelfHost(); this.selfHosted = platformUtilsService.isSelfHost();
} }
@@ -84,27 +63,10 @@ export class OrganizationSubscriptionComponent implements OnInit {
this.loading = true; this.loading = true;
this.userOrg = await this.organizationService.get(this.organizationId); this.userOrg = await this.organizationService.get(this.organizationId);
if (this.userOrg.canManageBilling) { if (this.userOrg.canManageBilling) {
this.sub = await this.apiService.getOrganizationSubscription(this.organizationId); 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; this.loading = false;
} }
@@ -176,20 +138,6 @@ export class OrganizationSubscriptionComponent implements OnInit {
this.showDownloadLicense = !this.showDownloadLicense; 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() { closeDownloadLicense() {
this.showDownloadLicense = false; 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() { get isExpired() {
return ( return (
this.sub != null && this.sub.expiration != null && new Date(this.sub.expiration) < new Date() 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() { get subscriptionDesc() {
if (this.sub.planType === PlanType.Free) { if (this.sub.planType === PlanType.Free) {
return this.i18nService.t("subscriptionFreePlan", this.sub.seats.toString()); return this.i18nService.t("subscriptionFreePlan", this.sub.seats.toString());
@@ -373,8 +293,4 @@ export class OrganizationSubscriptionComponent implements OnInit {
get showChangePlanButton() { get showChangePlanButton() {
return this.subscription == null && this.sub.planType === PlanType.Free && !this.showChangePlan; 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"; import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent } from "../../reports/exposed-passwords-report.component";
@Component({ @Component({
selector: "app-org-exposed-passwords-report", selector: "app-exposed-passwords-report",
templateUrl: "../../reports/exposed-passwords-report.component.html", templateUrl: "../../reports/exposed-passwords-report.component.html",
}) })
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent { export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
@@ -41,10 +41,12 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
} }
ngOnInit() { ngOnInit() {
const dynamicSuper = Object.getPrototypeOf(this.constructor.prototype);
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.organizationService.get(params.organizationId); this.organization = await this.organizationService.get(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll(); 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) { async load(filter: (cipher: CipherView) => boolean = null) {
this.deleted = deleted || false;
if (this.organization.canEditAnyCollection) { if (this.organization.canEditAnyCollection) {
this.accessEvents = this.organization.useEvents; this.accessEvents = this.organization.useEvents;
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id); this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);

View File

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

View File

@@ -307,9 +307,6 @@ export class EventService {
case EventType.Organization_DisabledKeyConnector: case EventType.Organization_DisabledKeyConnector:
msg = humanReadableMsg = this.i18nService.t("disabledKeyConnector"); msg = humanReadableMsg = this.i18nService.t("disabledKeyConnector");
break; break;
case EventType.Organization_SponsorshipsSynced:
msg = humanReadableMsg = this.i18nService.t("sponsorshipsSynced");
break;
// Policies // Policies
case EventType.Policy_Updated: { case EventType.Policy_Updated: {
msg = this.i18nService.t("modifiedPolicyId", this.formatPolicyId(ev)); 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()) { if (await this.keyConnectorService.getUsesKeyConnector()) {
this.router.navigate(["/settings/security/two-factor"]); this.router.navigate(["/settings/security/two-factor"]);
} }
await super.ngOnInit();
} }
async rotateEncKeyClicked() { async rotateEncKeyClicked() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import { formatDate } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service"; import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.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]", selector: "[sponsoring-org-row]",
templateUrl: "sponsoring-org-row.component.html", templateUrl: "sponsoring-org-row.component.html",
}) })
export class SponsoringOrgRowComponent implements OnInit { export class SponsoringOrgRowComponent {
@Input() sponsoringOrg: Organization = null; @Input() sponsoringOrg: Organization = null;
@Input() isSelfHosted = false;
@Output() sponsorshipRemoved = new EventEmitter(); @Output() sponsorshipRemoved = new EventEmitter();
statusMessage = "loading";
statusClass: "text-success" | "text-danger" = "text-success";
revokeSponsorshipPromise: Promise<any>; revokeSponsorshipPromise: Promise<any>;
resendEmailPromise: Promise<any>; resendEmailPromise: Promise<any>;
@@ -30,15 +25,6 @@ export class SponsoringOrgRowComponent implements OnInit {
private platformUtilsService: PlatformUtilsService private platformUtilsService: PlatformUtilsService
) {} ) {}
ngOnInit(): void {
this.setStatus(
this.isSelfHosted,
this.sponsoringOrg.familySponsorshipToDelete,
this.sponsoringOrg.familySponsorshipValidUntil,
this.sponsoringOrg.familySponsorshipLastSyncDate
);
}
async revokeSponsorship() { async revokeSponsorship() {
try { try {
this.revokeSponsorshipPromise = this.doRevokeSponsorship(); this.revokeSponsorshipPromise = this.doRevokeSponsorship();
@@ -57,10 +43,6 @@ export class SponsoringOrgRowComponent implements OnInit {
this.resendEmailPromise = null; this.resendEmailPromise = null;
} }
get isSentAwaitingSync() {
return this.isSelfHosted && !this.sponsoringOrg.familySponsorshipLastSyncDate;
}
private async doRevokeSponsorship() { private async doRevokeSponsorship() {
const isConfirmed = await this.platformUtilsService.showDialog( const isConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("revokeSponsorshipConfirmation"), this.i18nService.t("revokeSponsorshipConfirmation"),
@@ -78,53 +60,4 @@ export class SponsoringOrgRowComponent implements OnInit {
this.platformUtilsService.showToast("success", null, this.i18nService.t("reclaimedFreePlan")); this.platformUtilsService.showToast("success", null, this.i18nService.t("reclaimedFreePlan"));
this.sponsorshipRemoved.emit(); 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

@@ -295,6 +295,16 @@
(blur)="saveUsernameOptions()" (blur)="saveUsernameOptions()"
/> />
</div> </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>
<div class="row" *ngIf="usernameOptions.forwardedService === 'anonaddy'"> <div class="row" *ngIf="usernameOptions.forwardedService === 'anonaddy'">
<div class="form-group col-4"> <div class="form-group col-4">

View File

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

View File

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

View File

@@ -916,7 +916,7 @@
}, },
"specialCharacters": { "specialCharacters": {
"message": "Special Characters (!@#$%^&*)" "message": "Special Characters (!@#$%^&*)"
}, },
"numWords": { "numWords": {
"message": "Number of Words" "message": "Number of Words"
}, },
@@ -3034,10 +3034,10 @@
"message": "Adjustments to your subscription will result in prorated changes to your billing totals. You cannot invite more than $COUNT$ users without increasing your subscription seats.", "message": "Adjustments to your subscription will result in prorated changes to your billing totals. You cannot invite more than $COUNT$ users without increasing your subscription seats.",
"placeholders": { "placeholders": {
"count": { "count": {
"content": "$1", "content": "$1",
"example": "50" "example": "50"
} }
} }
}, },
"seatsToAdd": { "seatsToAdd": {
"message": "Seats To Add" "message": "Seats To Add"
@@ -4395,10 +4395,10 @@
"message": "Your Master Password does not meet the policy requirements of this organization. In order to join the organization, you must update your Master Password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." "message": "Your Master Password does not meet the policy requirements of this organization. In order to join the organization, you must update your Master Password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
}, },
"maximumVaultTimeout": { "maximumVaultTimeout": {
"message": "Vault Timeout" "message": "Vault Timeout"
}, },
"maximumVaultTimeoutDesc": { "maximumVaultTimeoutDesc": {
"message": "Configure a maximum vault timeout for all users." "message": "Configure a maximum vault timeout for all users."
}, },
"maximumVaultTimeoutLabel": { "maximumVaultTimeoutLabel": {
"message": "Maximum Vault Timeout" "message": "Maximum Vault Timeout"
@@ -4594,7 +4594,7 @@
"message": "Enter your personal email to redeem Bitwarden Families" "message": "Enter your personal email to redeem Bitwarden Families"
}, },
"sponsoredFamiliesLeaveCopy": { "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": { "acceptBitwardenFamiliesHelp": {
"message": "Accept offer for an existing organization or create a new Families organization." "message": "Accept offer for an existing organization or create a new Families organization."
@@ -4669,13 +4669,13 @@
"message": "Email Sent" "message": "Email Sent"
}, },
"revokeSponsorshipConfirmation": { "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": { "removeSponsorshipSuccess": {
"message": "Sponsorship Removed" "message": "Sponsorship Removed"
}, },
"ssoKeyConnectorError": { "ssoKeyConnectorUnavailable": {
"message": "Key Connector error: make sure Key Connector is available and working correctly." "message": "Unable to reach the Key Connector, try again later."
}, },
"keyConnectorUrl": { "keyConnectorUrl": {
"message": "Key Connector URL" "message": "Key Connector URL"
@@ -4803,75 +4803,6 @@
"freeWithSponsorship": { "freeWithSponsorship": {
"message": "FREE with sponsorship" "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": { "formErrorSummaryPlural": {
"message": "$COUNT$ fields above need your attention.", "message": "$COUNT$ fields above need your attention.",
"placeholders": { "placeholders": {
@@ -5004,46 +4935,6 @@
"service": { "service": {
"message": "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": { "billingManagedByProvider": {
"message": "Managed by $PROVIDER$", "message": "Managed by $PROVIDER$",
"placeholders": { "placeholders": {
@@ -5069,5 +4960,8 @@
}, },
"apiAccessToken": { "apiAccessToken": {
"message": "API Access Token" "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 { .modal-footer {
border-radius: 0.3rem 0.3rem 0 0;
justify-content: flex-start; justify-content: flex-start;
@include themify($themes) { @include themify($themes) {
background-color: themed("footerBackgroundColor"); background-color: themed("footerBackgroundColor");

View File

@@ -14,12 +14,22 @@
font-size: $font-size-base; font-size: $font-size-base;
} }
a.create-organization-link {
&:hover {
@include themify($themes) {
color: themed("iconHover") !important;
}
}
}
button { button {
@extend .no-btn; @extend .no-btn;
} }
h3, h3,
button.filter-button { button.filter-button {
text-transform: uppercase;
text-transform: uppercase;
margin: 0; margin: 0;
@include themify($themes) { @include themify($themes) {
color: themed("textMuted"); color: themed("textMuted");
@@ -108,7 +118,6 @@
} }
text-decoration: none; text-decoration: none;
} }
max-width: 90%;
} }
.edit-button { .edit-button {