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">
<button class="filter-button">
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i> <i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
{{ organizations[0].name }} {{ organizations[0].name }}
</button> </button>
</div> </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,6 +26,7 @@
autocomplete="off" autocomplete="off"
appAutofocus appAutofocus
/> />
<div class="filter">
<app-organization-filter <app-organization-filter
*ngIf="showOrgFilter" *ngIf="showOrgFilter"
[hide]="hideOrganizations" [hide]="hideOrganizations"
@@ -37,6 +38,7 @@
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)" (onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
(onFilterChange)="applyFilter($event)" (onFilterChange)="applyFilter($event)"
></app-organization-filter> ></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,8 +229,6 @@ 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", path: "organizations",
loadChildren: () => loadChildren: () =>
@@ -238,6 +236,8 @@ const routes: Routes = [
(m) => m.OrganizationsRoutingModule (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="row">
<div class="col-12">
<div class="page-header"> <div class="page-header">
<h1>{{ "newOrganization" | i18n }}</h1> <h1>{{ "newOrganization" | i18n }}</h1>
</div> </div>
<p>{{ "newOrganizationDesc" | i18n }}</p> <p>{{ "newOrganizationDesc" | i18n }}</p>
<app-organization-plans></app-organization-plans> <app-organization-plans></app-organization-plans>
</div>
</div>
</div>

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,
{
sponsoredEmail: this.sponsorshipForm.value.sponsorshipEmail,
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise, planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
friendlyName: this.sponsorshipForm.value.sponsorshipEmail, friendlyName: this.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

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