diff --git a/apps/web/src/app/admin-console/organizations/members/components/invite-members-dialog/by-link-tab.component.html b/apps/web/src/app/admin-console/organizations/members/components/invite-members-dialog/by-link-tab.component.html new file mode 100644 index 000000000000..57ba1c2299f2 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/invite-members-dialog/by-link-tab.component.html @@ -0,0 +1,44 @@ +

+ {{ "inviteByLinkDesc" | i18n }} + + {{ "learnMore" | i18n }} + +

+ +
+ + {{ "allowedDomains" | i18n }} + ({{ "required" | i18n }}) + + {{ "allowedDomainsHint" | i18n }} + + + @if (form.dirty) { + + } +
+ +@if (showCallout()) { + + + {{ "enterAllowedDomainsToGenerateInviteLink" | i18n }} + + +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/invite-members-dialog/by-link-tab.component.ts b/apps/web/src/app/admin-console/organizations/members/components/invite-members-dialog/by-link-tab.component.ts index c871437d9eb4..abe2d37c43a0 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/invite-members-dialog/by-link-tab.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/invite-members-dialog/by-link-tab.component.ts @@ -1,9 +1,117 @@ -import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { + ChangeDetectionStrategy, + Component, + inject, + input, + OnInit, + signal, + WritableSignal, +} from "@angular/core"; +import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; +import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { + AsyncActionsModule, + ButtonModule, + CalloutModule, + FormFieldModule, + LinkComponent, + ToastService, +} from "@bitwarden/components"; +import { + OrganizationInviteLink, + OrganizationInviteLinkService, +} from "@bitwarden/organization-invite-link"; +import { I18nPipe } from "@bitwarden/ui-common"; @Component({ standalone: true, selector: "app-by-link-tab", - template: ``, + templateUrl: "by-link-tab.component.html", changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + AsyncActionsModule, + ButtonModule, + CalloutModule, + CommonModule, + FormFieldModule, + I18nPipe, + ReactiveFormsModule, + LinkComponent, + ], }) -export class ByLinkTabComponent {} +export class ByLinkTabComponent implements OnInit { + readonly organizationId = input.required({ + transform: (value: string) => value as OrganizationId, + }); + + private readonly accountService = inject(AccountService); + private readonly inviteLinkService = inject(OrganizationInviteLinkService); + private readonly toastService = inject(ToastService); + private readonly i18nService = inject(I18nService); + private readonly fb = inject(FormBuilder); + + protected readonly inviteLink$: Observable = combineLatest([ + this.accountService.activeAccount$.pipe(getUserId), + toObservable(this.organizationId), + ]).pipe(switchMap(([userId, orgId]) => this.inviteLinkService.inviteLink$(userId, orgId))); + + protected readonly showCallout: WritableSignal = signal(false); + + protected readonly form = this.fb.group({ + domains: ["", Validators.required], + }); + + constructor() { + this.inviteLink$.pipe(takeUntilDestroyed()).subscribe((inviteLink) => { + if (inviteLink && !this.form.dirty) { + this.form.controls.domains.setValue(inviteLink.allowedDomains.join(", ")); + this.form.markAsPristine(); + } + }); + } + + async ngOnInit() { + this.showCallout.set(await firstValueFrom(this.inviteLink$.pipe(map((link) => link == null)))); + } + + readonly save = async () => { + this.form.markAllAsTouched(); + if (this.form.invalid) { + return; + } + + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const rawDomains = this.form.value.domains; + if (rawDomains == null) { + throw new Error("Must provide at least one valid domain."); + } + + const domains = rawDomains + .split(",") + .map((domain) => domain.trim()) + .filter((domain) => domain.length > 0); + + const inviteLink = await firstValueFrom(this.inviteLink$); + if (inviteLink) { + await this.inviteLinkService.updateInviteLink(userId, this.organizationId(), domains); + } else { + await this.inviteLinkService.createInviteLink(userId, this.organizationId(), domains); + } + + this.form.markAsPristine(); + + this.showCallout.set(false); + + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("domainsEdited"), + }); + }; +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/invite-members-dialog/invite-members-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/invite-members-dialog/invite-members-dialog.component.html index e9ddce99ae88..5485b62038db 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/invite-members-dialog/invite-members-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/invite-members-dialog/invite-members-dialog.component.html @@ -14,7 +14,7 @@ > - + } @else { @@ -37,160 +37,147 @@ {{ "cancel" | i18n }} - - - -

{{ "inviteUserDesc" | i18n }}

- @if ( - { - remainingSeats: remainingSeats$ | async, - batchLimit: emailBatchLimit$ | async, - isDynamicSeatPlan: isDynamicSeatPlan$ | async, - }; - as inviteInfo - ) { - - {{ "email" | i18n }} - - @if (inviteInfo.isDynamicSeatPlan) { - {{ "inviteMultipleEmailsNoSeatLimit" | i18n: inviteInfo.batchLimit }} - } - @if (!inviteInfo.isDynamicSeatPlan && inviteInfo.remainingSeats > 1) { - {{ - "inviteMultipleEmailsWithSeatLimit" - | i18n: inviteInfo.batchLimit : inviteInfo.remainingSeats - }} - } - @if (!inviteInfo.isDynamicSeatPlan && inviteInfo.remainingSeats === 1) { - {{ "inviteSingleEmailDesc" | i18n }} + +

{{ "inviteUserDesc" | i18n }}

+ @if ({ seats: remainingSeats$ | async }; as remaining) { + + {{ "email" | i18n }} + + @if (remaining.seats > 1) { + {{ "inviteMultipleEmailDesc" | i18n: remaining.seats }} + } + @if (remaining.seats === 1) { + {{ "inviteSingleEmailDesc" | i18n }} + } + @if (remaining.seats === 0) { + {{ "inviteZeroEmailDesc" | i18n }} + } + } - @if (!inviteInfo.isDynamicSeatPlan && inviteInfo.remainingSeats <= 0) { - {{ "inviteZeroEmailDesc" | i18n }} - } -
- } - - - {{ "memberRole" | i18n }} - - - - - - - - - + + + {{ "memberRole" | i18n }} + + + + + + + + + - @if (customUserTypeSelected()) { -
-
- - - {{ "accessEventLogs" | i18n }} - - - - {{ "accessImportExport" | i18n }} - + @if (customUserTypeSelected()) { +
+
+ + + {{ "accessEventLogs" | i18n }} + + + + {{ "accessImportExport" | i18n }} + + + + {{ "accessReports" | i18n }} + +
+
+
+ + + {{ "manageGroups" | i18n }} + + + + {{ "manageSso" | i18n }} + + + + {{ "managePolicies" | i18n }} + + + + {{ "manageUsers" | i18n }} + + + + {{ "manageAccountRecovery" | i18n }} + +
+
+
+ } + @if (organization.useSecretsManager) { - - {{ "accessReports" | i18n }} +
+ + + {{ "grantSecretsManager" | i18n }} + + + + +
-
-
-
- - - {{ "manageGroups" | i18n }} - - - - {{ "manageSso" | i18n }} - - - - {{ "managePolicies" | i18n }} - - - - {{ "manageUsers" | i18n }} - - - - {{ "manageAccountRecovery" | i18n }} - -
-
-
- } - @if (organization.useSecretsManager) { - -
- - - {{ "grantSecretsManager" | i18n }} - - - - -
-
- } - - - @if (organization.useGroups) { - - } - - -
+ } + + + @if (organization.useGroups) { + + } + + + + + diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 31b8492247d3..e198e0c39695 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5459,6 +5459,21 @@ "byLink": { "message": "By link" }, + "allowedDomains": { + "message": "Allowed domains" + }, + "allowedDomainsHint": { + "message": "If entering multiple domains, separate with commas" + }, + "enterAllowedDomainsToGenerateInviteLink": { + "message": "Enter Allowed domains to generate an invite link" + }, + "domainsEdited": { + "message": "Domains edited" + }, + "inviteByLinkDesc": { + "message": "Invite members to your organization with a link. If no role has been specified yet, they’ll be added as a default user." + }, "byContinuingYouAgreeToThe": { "message": "By continuing, you agree to the" }, diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 120e503cc79d..89ed4f313dae 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -1696,21 +1696,20 @@ export class ApiService implements ApiServiceAbstraction { const responseIsCsv = responseType != null && responseType.indexOf("text/csv") !== -1; const responseIsBlob = responseType != null && responseType.indexOf("application/octet-stream") !== -1; - if (hasResponse && response.status === HttpStatusCode.Ok && responseIsJson) { + const responseIsSuccess = + response.status === HttpStatusCode.Ok || response.status === HttpStatusCode.Created; + if (hasResponse && responseIsSuccess && responseIsJson) { const responseJson = await response.json(); return responseJson; - } else if (hasResponse && response.status === HttpStatusCode.Ok && responseIsCsv) { + } else if (hasResponse && responseIsSuccess && responseIsCsv) { return await response.text(); - } else if (hasResponse && response.status === HttpStatusCode.Ok && responseIsBlob) { + } else if (hasResponse && responseIsSuccess && responseIsBlob) { const disposition = response.headers.get("Content-Disposition") ?? ""; const match = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); const fileName = match ? match[1].replace(/['"]/g, "") : "download"; const blob = await response.blob(); return { blob, fileName }; - } else if ( - response.status !== HttpStatusCode.Ok && - response.status !== HttpStatusCode.NoContent - ) { + } else if (!responseIsSuccess && response.status !== HttpStatusCode.NoContent) { const error = await this.handleApiRequestError(response, userIdMakingRequest != null); return Promise.reject(error); } diff --git a/libs/organization-invite-link/src/abstractions/organization-invite-link-api.service.ts b/libs/organization-invite-link/src/abstractions/organization-invite-link-api.service.ts index d4949307a620..2a5b39ddfd8c 100644 --- a/libs/organization-invite-link/src/abstractions/organization-invite-link-api.service.ts +++ b/libs/organization-invite-link/src/abstractions/organization-invite-link-api.service.ts @@ -9,13 +9,13 @@ export abstract class OrganizationInviteLinkApiService { request: OrganizationInviteLinkCreateRequest, ): Promise; - /** Update allowed domains on the invite link for the given organization */ + /** Update the allowed domains for the given organization's invite link */ abstract update( organizationId: string, request: OrganizationInviteLinkUpdateRequest, ): Promise; - /** Retrieve the invite link for the given organization */ + /** Retrieve the current invite link for the given organization */ abstract get(organizationId: string): Promise; /** Delete (revoke) the invite link for the given organization */ diff --git a/libs/organization-invite-link/src/abstractions/organization-invite-link.service.ts b/libs/organization-invite-link/src/abstractions/organization-invite-link.service.ts index 4eef49465126..9e8bf37ff27a 100644 --- a/libs/organization-invite-link/src/abstractions/organization-invite-link.service.ts +++ b/libs/organization-invite-link/src/abstractions/organization-invite-link.service.ts @@ -2,40 +2,46 @@ import { Observable } from "rxjs"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { OrganizationInviteLinkResponseModel } from "../models/responses/organization-invite-link.response"; +import { OrganizationInviteLink } from "../models/responses/organization-invite-link.response"; export abstract class OrganizationInviteLinkService { /** Observable stream of the cached invite link for the given user */ - abstract inviteLink$(userId: UserId): Observable; + abstract inviteLink$( + userId: UserId, + orgId: OrganizationId, + ): Observable; /** * Create a new invite link for the organization. - * Emits the shareable URL once, then completes. */ abstract createInviteLink( userId: UserId, orgId: OrganizationId, domains: string[], - ): Promise; + ): Promise; /** - * Refresh the invite link using cached allowed domains. - * Emits the shareable URL once, then completes. + * Update the allowed domains on an existing invite link. */ - abstract refreshInviteLink(userId: UserId, orgId: OrganizationId): Promise; + abstract updateInviteLink( + userId: UserId, + orgId: OrganizationId, + domains: string[], + ): Promise; /** - * Reconstruct the shareable URL from the server-stored invite link. - * Emits the URL (or undefined when none exists), then completes. + * Updates the Organization invite link without modifying allowed domains (generates a new link) */ - abstract reconstructUrl(userId: UserId, orgId: OrganizationId): Promise; + abstract refreshInviteLink(userId: UserId, orgId: OrganizationId): Promise; - /** Persist an invite link response to local state */ - abstract upsert(userId: UserId, data: OrganizationInviteLinkResponseModel): Promise; + /** + * Reconstruct and returns the shareable URL from OrganizationInviteLink in local state as a string + */ + abstract reconstructUrl(userId: UserId, orgId: OrganizationId): Promise; + + /** Persist an invite link to local state */ + abstract upsert(userId: UserId, data: OrganizationInviteLink): Promise; /** Delete (revoke) the invite link via the API and clear local cached state */ abstract delete(userId: UserId, orgId: OrganizationId): Promise; - - /** Clear local cached invite link state for the user without calling the API */ - abstract clear(userId: UserId): Promise; } diff --git a/libs/organization-invite-link/src/models/requests/organization-invite-link-create.request.ts b/libs/organization-invite-link/src/models/requests/organization-invite-link-create.request.ts index 3ac20f3af0bb..032c434df45c 100644 --- a/libs/organization-invite-link/src/models/requests/organization-invite-link-create.request.ts +++ b/libs/organization-invite-link/src/models/requests/organization-invite-link-create.request.ts @@ -5,20 +5,20 @@ export class OrganizationInviteLinkCreateRequest { encryptedInviteKey: string; encryptedOrgKey: string | null; - constructor(c: { - allowedDomains: string[]; - encryptedInviteKey: EncString; - encryptedOrgKey?: EncString | null; - }) { - if (!c.allowedDomains || c.allowedDomains.length === 0) { + constructor( + allowedDomains: string[], + encryptedInviteKey: EncString, + encryptedOrgKey?: EncString | null, + ) { + if (!allowedDomains || allowedDomains.length === 0) { throw new Error("At least one allowed domain is required."); } - if (!c.encryptedInviteKey?.encryptedString) { + if (!encryptedInviteKey?.encryptedString) { throw new Error("EncryptedInviteKey is required."); } - this.allowedDomains = c.allowedDomains; - this.encryptedInviteKey = c.encryptedInviteKey.encryptedString; - this.encryptedOrgKey = c.encryptedOrgKey?.encryptedString ?? null; + this.allowedDomains = allowedDomains; + this.encryptedInviteKey = encryptedInviteKey.encryptedString; + this.encryptedOrgKey = encryptedOrgKey?.encryptedString ?? null; } } diff --git a/libs/organization-invite-link/src/models/requests/organization-invite-link-update.request.ts b/libs/organization-invite-link/src/models/requests/organization-invite-link-update.request.ts index 4d6259689d03..b7aac149b585 100644 --- a/libs/organization-invite-link/src/models/requests/organization-invite-link-update.request.ts +++ b/libs/organization-invite-link/src/models/requests/organization-invite-link-update.request.ts @@ -1,10 +1,11 @@ export class OrganizationInviteLinkUpdateRequest { allowedDomains: string[]; - constructor(c: { allowedDomains: string[] }) { - if (!c.allowedDomains || c.allowedDomains.length === 0) { + constructor(allowedDomains: string[]) { + if (!allowedDomains || allowedDomains.length === 0) { throw new Error("At least one allowed domain is required."); } - this.allowedDomains = c.allowedDomains; + + this.allowedDomains = allowedDomains; } } diff --git a/libs/organization-invite-link/src/models/responses/organization-invite-link.response.ts b/libs/organization-invite-link/src/models/responses/organization-invite-link.response.ts index 4d03ee697797..6dc520304716 100644 --- a/libs/organization-invite-link/src/models/responses/organization-invite-link.response.ts +++ b/libs/organization-invite-link/src/models/responses/organization-invite-link.response.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { BaseResponse } from "@bitwarden/common/models/response/base.response"; export class OrganizationInviteLinkResponseModel extends BaseResponse { @@ -20,3 +22,27 @@ export class OrganizationInviteLinkResponseModel extends BaseResponse { this.creationDate = this.getResponseProperty("CreationDate"); } } + +export class OrganizationInviteLink { + id: string; + code: string; + organizationId: string; + allowedDomains: string[]; + encryptedInviteKey: string; + encryptedOrgKey: string | undefined; + creationDate: string; + + constructor(response: OrganizationInviteLinkResponseModel) { + this.id = response.id; + this.code = response.code; + this.organizationId = response.organizationId; + this.allowedDomains = response.allowedDomains; + this.encryptedInviteKey = response.encryptedInviteKey; + this.encryptedOrgKey = response.encryptedOrgKey; + this.creationDate = response.creationDate; + } + + static fromJSON(obj: Jsonify): OrganizationInviteLink { + return Object.assign(new OrganizationInviteLink(obj as any), obj); + } +} diff --git a/libs/organization-invite-link/src/services/default-organization-invite-link.service.ts b/libs/organization-invite-link/src/services/default-organization-invite-link.service.ts index 766f11101142..fc36a5633823 100644 --- a/libs/organization-invite-link/src/services/default-organization-invite-link.service.ts +++ b/libs/organization-invite-link/src/services/default-organization-invite-link.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom, map, Observable } from "rxjs"; +import { catchError, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; @@ -11,7 +11,11 @@ import { StateProvider } from "@bitwarden/state"; import { OrganizationInviteLinkApiService } from "../abstractions/organization-invite-link-api.service"; import { OrganizationInviteLinkService } from "../abstractions/organization-invite-link.service"; import { OrganizationInviteLinkCreateRequest } from "../models/requests/organization-invite-link-create.request"; -import { OrganizationInviteLinkResponseModel } from "../models/responses/organization-invite-link.response"; +import { OrganizationInviteLinkUpdateRequest } from "../models/requests/organization-invite-link-update.request"; +import { + OrganizationInviteLink, + OrganizationInviteLinkResponseModel, +} from "../models/responses/organization-invite-link.response"; import { ORGANIZATION_INVITE_LINK_KEY } from "../state/organization-invite-link-state"; export class DefaultOrganizationInviteLinkService implements OrganizationInviteLinkService { @@ -23,93 +27,106 @@ export class DefaultOrganizationInviteLinkService implements OrganizationInviteL private readonly stateProvider: StateProvider, ) {} - inviteLink$(userId: UserId): Observable { - return this.stateProvider - .getUser(userId, ORGANIZATION_INVITE_LINK_KEY) - .state$.pipe(map((state) => state ?? undefined)); - } - - async createInviteLink( + inviteLink$( userId: UserId, orgId: OrganizationId, - domains: string[], - ): Promise { - const rawInviteKey = await this.generateCryptoBundle(); - - const orgKey = await firstValueFrom( - this.keyService.orgKeys$(userId).pipe( - map((orgKeys) => { - const orgKey = orgKeys?.[orgId as OrganizationId] ?? undefined; - if (orgKey == null) { - throw new Error(`Organization key not found for org ${orgId}`); - } - - return orgKey; - }), - ), + ): Observable { + return this.stateProvider.getUser(userId, ORGANIZATION_INVITE_LINK_KEY).state$.pipe( + switchMap((state) => { + if (state == null) { + return this.getInviteLink(userId, orgId); + } + return of(state); + }), + catchError(() => of(undefined)), ); + } + async createInviteLink(userId: UserId, orgId: OrganizationId, domains: string[]): Promise { + const rawInviteKey = await this.generateCryptoBundle(); + const orgKey = await firstValueFrom(this.getOrgKey(userId, orgId)); const encryptedInviteKey = await this.encryptService.wrapSymmetricKey(rawInviteKey, orgKey); - - const request = new OrganizationInviteLinkCreateRequest({ - allowedDomains: domains, - encryptedInviteKey, - }); - + const request = new OrganizationInviteLinkCreateRequest(domains, encryptedInviteKey); const response = await this.apiService.create(orgId, request); - await this.upsert(userId, response); + const inviteLink = new OrganizationInviteLink(response); - return this.buildInviteUrl(response.code, rawInviteKey.keyB64); + await this.upsert(userId, inviteLink); } - async refreshInviteLink(userId: UserId, orgId: OrganizationId): Promise { - const cached = await firstValueFrom(this.inviteLink$(userId)); - const domains = cached?.allowedDomains ?? []; - return this.createInviteLink(userId, orgId, domains); - } + async updateInviteLink(userId: UserId, orgId: OrganizationId, domains: string[]): Promise { + const request = new OrganizationInviteLinkUpdateRequest(domains); + const response = await this.apiService.update(orgId, request); + const inviteLink = new OrganizationInviteLink(response); - async reconstructUrl(userId: UserId, orgId: OrganizationId): Promise { - const response = await this.apiService.get(orgId); - if (response == null) { - return; - } + await this.upsert(userId, inviteLink); + } - await this.upsert(userId, response); + async refreshInviteLink(userId: UserId, orgId: OrganizationId): Promise { + const inviteLink = await firstValueFrom(this.inviteLink$(userId, orgId)); + const domains = inviteLink?.allowedDomains ?? []; + await this.updateInviteLink(userId, orgId, domains); + } - const orgKey = await firstValueFrom( - this.keyService.orgKeys$(userId).pipe( - map((orgKeys) => { - const orgKey = orgKeys?.[orgId] ?? undefined; - if (orgKey == null) { - throw new Error(`Organization key not found for org ${orgId}`); + async reconstructUrl(userId: UserId, orgId: OrganizationId): Promise { + const inviteLink = await firstValueFrom( + this.inviteLink$(userId, orgId).pipe( + map((inviteLink) => { + if (inviteLink == null) { + throw new Error("Organization does not have an invite link to reconstruct"); } - - return orgKey; + return inviteLink; }), ), ); - - const encKey = new EncString(response.encryptedInviteKey); + const orgKey = await firstValueFrom(this.getOrgKey(userId, orgId)); + const encKey = new EncString(inviteLink.encryptedInviteKey); const rawInviteKey = await this.encryptService.unwrapSymmetricKey(encKey, orgKey); - return this.buildInviteUrl(response.code, rawInviteKey.keyB64); - } - - private buildInviteUrl(code: string, keyB64: string): string { - return `/#/join/${code}?key=${keyB64}`; + return this.buildInviteUrl(inviteLink.code, rawInviteKey.keyB64); } - async upsert(userId: UserId, data: OrganizationInviteLinkResponseModel): Promise { + async upsert(userId: UserId, data: OrganizationInviteLink): Promise { await this.stateProvider.getUser(userId, ORGANIZATION_INVITE_LINK_KEY).update(() => data); } async delete(userId: UserId, orgId: OrganizationId): Promise { await this.apiService.delete(orgId); - await this.clear(userId); } - async clear(userId: UserId): Promise { - await this.stateProvider.getUser(userId, ORGANIZATION_INVITE_LINK_KEY).update(() => undefined); + private buildInviteUrl(code: string, keyB64: string): string { + return `/#/join/${code}?key=${keyB64}`; + } + + private async getInviteLink( + userId: UserId, + orgId: OrganizationId, + ): Promise { + let response: OrganizationInviteLinkResponseModel; + try { + response = await this.apiService.get(orgId); + } catch (e: any) { + if (e.status === 404) { + return undefined; + } + throw e; + } + + const inviteLink = new OrganizationInviteLink(response); + await this.upsert(userId, inviteLink); + return inviteLink; + } + + private getOrgKey(userId: UserId, orgId: OrganizationId) { + return this.keyService.orgKeys$(userId).pipe( + map((orgKeys) => { + const orgKey = orgKeys?.[orgId] ?? undefined; + if (orgKey == null) { + throw new Error(`Organization key not found for org ${orgId}`); + } + + return orgKey; + }), + ); } /** diff --git a/libs/organization-invite-link/src/state/organization-invite-link-state.ts b/libs/organization-invite-link/src/state/organization-invite-link-state.ts index 9376c48479c6..3f150eb4ca7a 100644 --- a/libs/organization-invite-link/src/state/organization-invite-link-state.ts +++ b/libs/organization-invite-link/src/state/organization-invite-link-state.ts @@ -1,11 +1,13 @@ +import { Jsonify } from "type-fest"; + import { ORGANIZATION_INVITE_LINK_DISK, UserKeyDefinition } from "@bitwarden/state"; -import { OrganizationInviteLinkResponseModel } from "../models/responses/organization-invite-link.response"; +import { OrganizationInviteLink } from "../models/responses/organization-invite-link.response"; export const ORGANIZATION_INVITE_LINK_KEY = new UserKeyDefinition< - OrganizationInviteLinkResponseModel | undefined + OrganizationInviteLink | undefined >(ORGANIZATION_INVITE_LINK_DISK, "inviteLink", { - deserializer: (obj) => - obj == null ? undefined : Object.assign(new OrganizationInviteLinkResponseModel(obj), obj), + deserializer: (obj: Jsonify) => + obj == null ? undefined : OrganizationInviteLink.fromJSON(obj), clearOn: ["logout"], });