mirror of
https://github.com/bitwarden/clients.git
synced 2026-02-04 02:54:59 -06:00
Merge branch 'main' into dirt/PM-30543
This commit is contained in:
commit
a97bcf8cca
18
.github/workflows/build-web.yml
vendored
18
.github/workflows/build-web.yml
vendored
@ -63,6 +63,11 @@ jobs:
|
||||
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
uses: bitwarden/ios/.github/actions/log-inputs@main
|
||||
with:
|
||||
inputs: "${{ toJson(inputs) }}"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
@ -181,6 +186,19 @@ jobs:
|
||||
ref: ${{ steps.set-server-ref.outputs.server_ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download SDK Artifacts
|
||||
if: ${{ inputs.sdk_branch != '' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
artifacts: sdk-internal
|
||||
repo: bitwarden/sdk-internal
|
||||
path: sdk-internal
|
||||
if_no_artifact_found: fail
|
||||
|
||||
- name: Check Branch to Publish
|
||||
env:
|
||||
PUBLISH_BRANCHES: "main,rc,hotfix-rc-web"
|
||||
|
||||
@ -15,6 +15,12 @@ RUN if [ "${LICENSE_TYPE}" != "commercial" ] ; then \
|
||||
rm -rf node_modules/@bitwarden/commercial-sdk-internal ; \
|
||||
fi
|
||||
|
||||
# Override SDK if custom artifacts are present
|
||||
RUN if [ -d "sdk-internal" ]; then \
|
||||
echo "Overriding SDK with custom artifacts from sdk-internal" ; \
|
||||
npm link ./sdk-internal ; \
|
||||
fi
|
||||
|
||||
WORKDIR /source/apps/web
|
||||
ARG NPM_COMMAND=dist:bit:selfhost
|
||||
RUN npm run ${NPM_COMMAND}
|
||||
|
||||
@ -54,7 +54,7 @@
|
||||
{{ "owner" | i18n }}
|
||||
</th>
|
||||
}
|
||||
<th bitCell class="tw-text-right" bitSortable="scoreKey" default>
|
||||
<th bitCell class="tw-text-right" bitSortable="scoreKey" default="desc">
|
||||
{{ "weakness" | i18n }}
|
||||
</th>
|
||||
</ng-container>
|
||||
|
||||
@ -593,7 +593,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("itemsWereSentToArchive"),
|
||||
message: this.i18nService.t("itemWasSentToArchive"),
|
||||
});
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
|
||||
@ -157,7 +157,7 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
||||
|
||||
// If item is archived always show unarchive button, even if user is not premium
|
||||
protected get showUnArchiveButton() {
|
||||
if (!this.archiveEnabled()) {
|
||||
if (!this.archiveEnabled() || this.viewingOrgVault) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -91,4 +91,33 @@ describe("BitwardenCsvImporter", () => {
|
||||
expect(result.collections[0].name).toBe("collection1/collection2");
|
||||
expect(result.collections[1].name).toBe("collection1");
|
||||
});
|
||||
|
||||
it("should parse archived items correctly", async () => {
|
||||
const archivedDate = "2025-01-15T10:30:00.000Z";
|
||||
const data =
|
||||
`name,type,archivedDate,login_uri,login_username,login_password` +
|
||||
`\nArchived Login,login,${archivedDate},https://example.com,user,pass`;
|
||||
|
||||
importer.organizationId = null;
|
||||
const result = await importer.parse(data);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.ciphers.length).toBe(1);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.name).toBe("Archived Login");
|
||||
expect(cipher.archivedDate).toBeDefined();
|
||||
expect(cipher.archivedDate.toISOString()).toBe(archivedDate);
|
||||
});
|
||||
|
||||
it("should handle missing archivedDate gracefully", async () => {
|
||||
const data = `name,type,login_uri` + `\nTest Login,login,https://example.com`;
|
||||
|
||||
importer.organizationId = null;
|
||||
const result = await importer.parse(data);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.ciphers.length).toBe(1);
|
||||
expect(result.ciphers[0].archivedDate).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -51,6 +51,15 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer {
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
}
|
||||
|
||||
if (!this.isNullOrWhitespace(value.archivedDate)) {
|
||||
try {
|
||||
cipher.archivedDate = new Date(value.archivedDate);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
console.error("Unable to parse archivedDate value", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isNullOrWhitespace(value.fields)) {
|
||||
const fields = this.splitNewLine(value.fields);
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
|
||||
87
libs/importer/src/importers/buttercup-csv-importer.spec.ts
Normal file
87
libs/importer/src/importers/buttercup-csv-importer.spec.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { ButtercupCsvImporter } from "./buttercup-csv-importer";
|
||||
import {
|
||||
buttercupCsvTestData,
|
||||
buttercupCsvWithCustomFieldsTestData,
|
||||
buttercupCsvWithNoteTestData,
|
||||
buttercupCsvWithSubfoldersTestData,
|
||||
buttercupCsvWithUrlFieldTestData,
|
||||
} from "./spec-data/buttercup-csv/testdata.csv";
|
||||
|
||||
describe("Buttercup CSV Importer", () => {
|
||||
let importer: ButtercupCsvImporter;
|
||||
|
||||
beforeEach(() => {
|
||||
importer = new ButtercupCsvImporter();
|
||||
});
|
||||
|
||||
describe("given basic login data", () => {
|
||||
it("should parse login data when provided valid CSV", async () => {
|
||||
const result = await importer.parse(buttercupCsvTestData);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.ciphers.length).toBe(2);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.name).toEqual("Test Entry");
|
||||
expect(cipher.login.username).toEqual("testuser");
|
||||
expect(cipher.login.password).toEqual("testpass123");
|
||||
expect(cipher.login.uris.length).toEqual(1);
|
||||
expect(cipher.login.uris[0].uri).toEqual("https://example.com");
|
||||
});
|
||||
|
||||
it("should assign entries to folders based on group_name", async () => {
|
||||
const result = await importer.parse(buttercupCsvTestData);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.folders.length).toBe(1);
|
||||
expect(result.folders[0].name).toEqual("General");
|
||||
expect(result.folderRelationships.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given URL field variations", () => {
|
||||
it("should handle lowercase url field", async () => {
|
||||
const result = await importer.parse(buttercupCsvWithUrlFieldTestData);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.login.uris.length).toEqual(1);
|
||||
expect(cipher.login.uris[0].uri).toEqual("https://lowercase-url.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("given note field", () => {
|
||||
it("should map note field to notes", async () => {
|
||||
const result = await importer.parse(buttercupCsvWithNoteTestData);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.notes).toEqual("This is a note");
|
||||
});
|
||||
});
|
||||
|
||||
describe("given custom fields", () => {
|
||||
it("should import custom fields and exclude official props", async () => {
|
||||
const result = await importer.parse(buttercupCsvWithCustomFieldsTestData);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.fields.length).toBe(2);
|
||||
expect(cipher.fields[0].name).toEqual("custom_field");
|
||||
expect(cipher.fields[0].value).toEqual("custom value");
|
||||
expect(cipher.fields[1].name).toEqual("another_field");
|
||||
expect(cipher.fields[1].value).toEqual("another value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("given subfolders", () => {
|
||||
it("should create nested folder structure", async () => {
|
||||
const result = await importer.parse(buttercupCsvWithSubfoldersTestData);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const folderNames = result.folders.map((f) => f.name);
|
||||
expect(folderNames).toContain("Work/Projects");
|
||||
expect(folderNames).toContain("Work");
|
||||
expect(folderNames).toContain("Personal/Finance");
|
||||
expect(folderNames).toContain("Personal");
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -3,7 +3,18 @@ import { ImportResult } from "../models/import-result";
|
||||
import { BaseImporter } from "./base-importer";
|
||||
import { Importer } from "./importer";
|
||||
|
||||
const OfficialProps = ["!group_id", "!group_name", "title", "username", "password", "URL", "id"];
|
||||
const OfficialProps = [
|
||||
"!group_id",
|
||||
"!group_name",
|
||||
"!type",
|
||||
"title",
|
||||
"username",
|
||||
"password",
|
||||
"URL",
|
||||
"url",
|
||||
"note",
|
||||
"id",
|
||||
];
|
||||
|
||||
export class ButtercupCsvImporter extends BaseImporter implements Importer {
|
||||
parse(data: string): Promise<ImportResult> {
|
||||
@ -21,16 +32,24 @@ export class ButtercupCsvImporter extends BaseImporter implements Importer {
|
||||
cipher.name = this.getValueOrDefault(value.title, "--");
|
||||
cipher.login.username = this.getValueOrDefault(value.username);
|
||||
cipher.login.password = this.getValueOrDefault(value.password);
|
||||
cipher.login.uris = this.makeUriArray(value.URL);
|
||||
|
||||
let processingCustomFields = false;
|
||||
// Handle URL field (case-insensitive)
|
||||
const urlValue = value.URL || value.url || value.Url;
|
||||
cipher.login.uris = this.makeUriArray(urlValue);
|
||||
|
||||
// Handle note field (case-insensitive)
|
||||
const noteValue = value.note || value.Note || value.notes || value.Notes;
|
||||
if (noteValue) {
|
||||
cipher.notes = noteValue;
|
||||
}
|
||||
|
||||
// Process custom fields, excluding official props (case-insensitive)
|
||||
for (const prop in value) {
|
||||
// eslint-disable-next-line
|
||||
if (value.hasOwnProperty(prop)) {
|
||||
if (!processingCustomFields && OfficialProps.indexOf(prop) === -1) {
|
||||
processingCustomFields = true;
|
||||
}
|
||||
if (processingCustomFields) {
|
||||
const lowerProp = prop.toLowerCase();
|
||||
const isOfficialProp = OfficialProps.some((p) => p.toLowerCase() === lowerProp);
|
||||
if (!isOfficialProp && value[prop]) {
|
||||
this.processKvp(cipher, prop, value[prop]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
export const buttercupCsvTestData = `!group_id,!group_name,title,username,password,URL,id
|
||||
1,General,Test Entry,testuser,testpass123,https://example.com,entry1
|
||||
1,General,Another Entry,anotheruser,anotherpass,https://another.com,entry2`;
|
||||
|
||||
export const buttercupCsvWithUrlFieldTestData = `!group_id,!group_name,title,username,password,url,id
|
||||
1,General,Entry With Lowercase URL,user1,pass1,https://lowercase-url.com,entry1`;
|
||||
|
||||
export const buttercupCsvWithNoteTestData = `!group_id,!group_name,title,username,password,URL,note,id
|
||||
1,General,Entry With Note,user1,pass1,https://example.com,This is a note,entry1`;
|
||||
|
||||
export const buttercupCsvWithCustomFieldsTestData = `!group_id,!group_name,title,username,password,URL,custom_field,another_field,id
|
||||
1,General,Entry With Custom Fields,user1,pass1,https://example.com,custom value,another value,entry1`;
|
||||
|
||||
export const buttercupCsvWithSubfoldersTestData = `!group_id,!group_name,title,username,password,URL,id
|
||||
1,Work/Projects,Project Entry,projectuser,projectpass,https://project.com,entry1
|
||||
2,Personal/Finance,Finance Entry,financeuser,financepass,https://finance.com,entry2`;
|
||||
@ -59,6 +59,7 @@ export class BaseVaultExportService {
|
||||
cipher.notes = c.notes;
|
||||
cipher.fields = null;
|
||||
cipher.reprompt = c.reprompt;
|
||||
cipher.archivedDate = c.archivedDate ? c.archivedDate.toISOString() : null;
|
||||
// Login props
|
||||
cipher.login_uri = null;
|
||||
cipher.login_username = null;
|
||||
|
||||
@ -12,6 +12,7 @@ export type BitwardenCsvExportType = {
|
||||
login_password: string;
|
||||
login_totp: string;
|
||||
favorite: number | null;
|
||||
archivedDate: string | null;
|
||||
};
|
||||
|
||||
export type BitwardenCsvIndividualExportType = BitwardenCsvExportType & {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user