From ba7d7a3469ce388e7ed2de98fa314e58c6c278f9 Mon Sep 17 00:00:00 2001 From: Cy Okeke Date: Thu, 13 Feb 2025 16:48:56 +0100 Subject: [PATCH] Changes for the dummy event --- .../manage/events.component.html | 63 ++++++++- .../organizations/manage/events.component.ts | 129 +++++++++++++++++- .../organization-reporting-routing.module.ts | 4 +- .../layouts/header/web-header.component.html | 3 + .../layouts/header/web-header.component.ts | 3 + apps/web/src/locales/en/messages.json | 12 ++ .../models/domain/organization.ts | 10 +- 7 files changed, 215 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.html b/apps/web/src/app/admin-console/organizations/manage/events.component.html index 654020ec263..bd8fe44e2db 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.html @@ -1,5 +1,7 @@ - - + +
+ {{ dummyEventsDisclaimer }} +
@@ -31,6 +33,7 @@ bitFormButton buttonType="primary" [bitAction]="refreshEvents" + [disabled]="organization.isFreeOrFamilyOrg" > {{ "update" | i18n }} @@ -42,7 +45,7 @@ bitButton bitFormButton [bitAction]="exportEvents" - [disabled]="dirtyDates" + [disabled]="dirtyDates || organization.isFreeOrFamilyOrg" > {{ "export" | i18n }} @@ -58,7 +61,7 @@ > {{ "loading" | i18n }} - +

{{ "noEventsInList" | i18n }}

@@ -92,3 +95,55 @@ {{ "loadMore" | i18n }} + + + + + + {{ "timestamp" | i18n }} + {{ "client" | i18n }} + {{ "member" | i18n }} + {{ "event" | i18n }} + + + + + + {{ event.timeStamp }} + + {{ event.deviceType }} + + + {{ event.member }} + + {{ event.event }} + + + + ****** + *** + ********** + ********** + + + + +
+
+ + +

{{ "limitedEventLogs" | i18n: getOrganizationPlan() }}

+

+ {{ "upgradeForFullEvents" | i18n }} +

+ + +
+
+
diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index c6969f5b55e..55e0a63e408 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -2,11 +2,12 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, lastValueFrom, Subject, takeUntil } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { getOrganizationById, OrganizationService, @@ -15,14 +16,20 @@ import { ProviderService } from "@bitwarden/common/admin-console/abstractions/pr import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { EventSystemUser } from "@bitwarden/common/enums"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "../../../billing/organizations/change-plan-dialog.component"; import { EventService } from "../../../core"; import { EventExportService } from "../../../tools/event-export"; import { BaseEventsComponent } from "../../common/base.events.component"; @@ -41,9 +48,52 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe exportFileName = "org-events"; organizationId: string; organization: Organization; + sub: OrganizationSubscriptionResponse; + userOrg: Organization; + dummyEvents = [ + { + timeStamp: this.getRandomDateTime(), + deviceType: "Extension - Firefox", + member: "Alice", + event: "Logged in", + }, + { + timeStamp: this.getRandomDateTime(), + deviceType: "Mobile - iOS", + member: "Bob", + event: "Viewed item 000000", + }, + { + timeStamp: this.getRandomDateTime(), + deviceType: "Desktop - Linux", + member: "Carlos", + event: "Login attempt failed with incorrect password", + }, + { + timeStamp: this.getRandomDateTime(), + deviceType: "Web vault - Chrome", + member: "Ivan", + event: "Confirmed user 000000", + }, + { + timeStamp: this.getRandomDateTime(), + deviceType: "Mobile - Android", + member: "Franz", + event: "Sent item 000000 to trash", + }, + ]; + + dummyEventsSorted: { + timeStamp: string; + deviceType: string; + member: string; + event: string; + }[]; private orgUsersUserIdMap = new Map(); private destroy$ = new Subject(); + readonly dummyEventsDisclaimer = + "These events are examples only and do not reflect real events within your Bitwarden organization."; constructor( private apiService: ApiService, @@ -57,10 +107,12 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe private userNamePipe: UserNamePipe, private organizationService: OrganizationService, private organizationUserApiService: OrganizationUserApiService, + private organizationApiService: OrganizationApiServiceAbstraction, private providerService: ProviderService, fileDownloadService: FileDownloadService, toastService: ToastService, private accountService: AccountService, + private dialogService: DialogService, ) { super( eventService, @@ -84,10 +136,30 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe .organizations$(userId) .pipe(getOrganizationById(this.organizationId)), ); - if (this.organization == null || !this.organization.useEvents) { + + if ( + !this.organization || + (!this.organization.useEvents && !this.organization.isFreeOrFamilyOrg) + ) { + //update true to isFamily await this.router.navigate(["/organizations", this.organizationId]); return; } + + if (this.organization.isOwner && this.organization.isFreeOrFamilyOrg) { + this.eventsForm.get("start").disable(); + this.eventsForm.get("end").disable(); + + this.sortDummyEvents(); + + this.userOrg = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); + this.sub = await this.organizationApiService.getSubscription(this.organizationId); + } + await this.load(); }), takeUntil(this.destroy$), @@ -186,6 +258,57 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe return id?.substring(0, 8); } + async changePlan() { + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: this.organizationId, + subscription: this.sub, + productTierType: this.userOrg.productTierType, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === ChangePlanDialogResultType.Closed) { + return; + } + await this.load(); + } + + getRandomDateTime() { + const now = new Date(); + const past24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000); + const randomTime = + past24Hours.getTime() + Math.random() * (now.getTime() - past24Hours.getTime()); + const randomDate = new Date(randomTime); + + return randomDate.toLocaleString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: true, + }); + } + + sortDummyEvents() { + this.dummyEventsSorted = this.dummyEvents.sort( + (a, b) => new Date(a.timeStamp).getTime() - new Date(b.timeStamp).getTime(), + ); + } + + getOrganizationPlan() { + const tierValue = this.organization.productTierType; + const tierName = ProductTierType[tierValue]; + return tierName; + } + + formatEventText(event: string): string { + return event.replace(/000000/g, '000000'); + } + ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); diff --git a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts index d6c7bdd97cd..d44fc1b2999 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts @@ -82,7 +82,9 @@ const routes: Routes = [ { path: "events", component: EventsComponent, - canActivate: [organizationPermissionsGuard((org) => org.canAccessEventLogs)], + canActivate: [ + organizationPermissionsGuard((org) => org.canAccessEventLogs || org.isFreeOrFamilyOrg), + ], data: { titleId: "eventLogs", }, diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index 7cba19b29ad..5ec5cc09f15 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -17,6 +17,9 @@ > {{ title || (routeData.titleId | i18n) }} + + {{ "upgrade" | i18n }} +
diff --git a/apps/web/src/app/layouts/header/web-header.component.ts b/apps/web/src/app/layouts/header/web-header.component.ts index 2f0c9d4772b..7c61f230bc7 100644 --- a/apps/web/src/app/layouts/header/web-header.component.ts +++ b/apps/web/src/app/layouts/header/web-header.component.ts @@ -27,6 +27,9 @@ export class WebHeaderComponent { */ @Input() icon: string; + /** Whether the current organization is a free or family organization */ + @Input() isFreeOrFamilyOrg = false; + protected routeData$: Observable<{ titleId: string }>; protected account$: Observable; protected canLock$: Observable; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 0f48595f09b..c2b3960e531 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10370,5 +10370,17 @@ }, "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." + }, + "limitedEventLogs": { + "message": "$PRODUCT_TYPE$ plans do not have access to real event logs", + "placeholders": { + "product_type": { + "content": "$1", + "example": "Teams" + } + } + }, + "upgradeForFullEvents": { + "message": "Get full access to organization event logs by upgrading to a Teams or Enterprise plan." } } diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 6f7ff561f04..20957687a64 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -172,7 +172,10 @@ export class Organization { } get canAccessEventLogs() { - return (this.isAdmin || this.permissions.accessEventLogs) && this.useEvents; + return ( + ((this.isAdmin || this.permissions.accessEventLogs) && this.useEvents) || + (this.isOwner && this.isFreeOrFamilyOrg) + ); } /** @@ -349,6 +352,11 @@ export class Organization { return !this.useTotp; } + get isFreeOrFamilyOrg() { + // return true if organization is on free or family tier + return [ProductTierType.Free, ProductTierType.Families].includes(this.productTierType); + } + get canManageSponsorships() { return this.familySponsorshipAvailable || this.familySponsorshipFriendlyName !== null; }