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()) {
-
- }
- @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"],
});