1
0
mirror of https://github.com/bitwarden/web synced 2025-12-06 00:03:28 +00:00

Compare commits

...

191 Commits

Author SHA1 Message Date
Vince Grassia
165944624a Fix docker tag version in workflow (#973) 2021-05-12 14:28:28 -07:00
Chad Scharf
11b96728f2 Version bump, 2.20.1 (#971) 2021-05-12 13:00:12 -04:00
Oscar Hinton
62e22ff149 Change WebAuthn connectors from using inline onclick to external (#969)
(cherry picked from commit a3506e833a)
2021-05-12 17:20:21 +02:00
Daniel James Smith
09968a18d0 Fix typo in webAuthnAuthenticate (#964) 2021-05-12 09:10:08 -04:00
Joseph Flinn
cc59d08a69 fixing docker push (#965)
* fixing docker push

* adding in the the missed vars
2021-05-11 12:51:02 -07:00
Joseph Flinn
ac45ac2eca Updating the docker signing key (#963)
* Updating the docker signing key

* restricting the Azure login to specific branches that use docker

* only retrieving secrets on specific branches
2021-05-11 12:05:58 -07:00
Kyle Spearrin
95c0e211c7 New Crowdin updates (#962)
* New translations messages.json (Romanian)

* New translations messages.json (Bengali)

* New translations messages.json (Swedish)

* New translations messages.json (Turkish)

* New translations messages.json (Ukrainian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Vietnamese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Indonesian)

* New translations messages.json (Croatian)

* New translations messages.json (Slovenian)

* New translations messages.json (Estonian)

* New translations messages.json (Latvian)

* New translations messages.json (English, United Kingdom)

* New translations messages.json (Esperanto)

* New translations messages.json (Malayalam)

* New translations messages.json (Sinhala)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Serbian (Latin))

* New translations messages.json (Serbian (Cyrillic))

* New translations messages.json (Slovak)

* New translations messages.json (French)

* New translations messages.json (Greek)

* New translations messages.json (Spanish)

* New translations messages.json (Afrikaans)

* New translations messages.json (Belarusian)

* New translations messages.json (Bulgarian)

* New translations messages.json (Catalan)

* New translations messages.json (Czech)

* New translations messages.json (Danish)

* New translations messages.json (German)

* New translations messages.json (Finnish)

* New translations messages.json (Russian)

* New translations messages.json (Hebrew)

* New translations messages.json (Hungarian)

* New translations messages.json (Italian)

* New translations messages.json (Japanese)

* New translations messages.json (Korean)

* New translations messages.json (Dutch)

* New translations messages.json (Polish)

* New translations messages.json (Portuguese)

* New translations messages.json (English, India)
2021-05-11 13:51:49 -04:00
Oscar Hinton
fed8990a6a Add auto delete warning to trash page (#953)
* Add warning to trash page

(cherry picked from commit dd56c9bc87)
2021-04-27 19:24:34 +02:00
Matt Gibson
b8431f0101 Update jslib 2021-04-26 15:24:32 -05:00
Matt Gibson
af47e25ead Update jslib 2021-04-23 14:09:14 -05:00
Matt Gibson
a315370e5d update jslib 2021-04-23 10:32:22 -05:00
Matt Gibson
5a64b22190 Specify organization id as the indexing entity (#945)
* Specify organization id as the indexing entity

* Update jslib

(cherry picked from commit f6eec08b70)
2021-04-23 09:53:33 -05:00
Vincent Salucci
4b05bc981c [Version] Bumped to 2.20.0 (#944)
* [Version] Bumped to 2.20.0

* Updated package-lock version
2021-04-22 11:57:30 -05:00
Vincent Salucci
00f0dc3499 [Reset Password] Feature Flag (#943) 2021-04-22 09:49:27 -05:00
vachan-maker
a1b77dc9ef Update change-password.component.html (#941) 2021-04-22 09:34:16 -04:00
Thomas Rittson
9b38095aba Use jslib unauthGuard, add lockGuard support (#939)
* Use jslib unauthGuard, add lockGuard support

* bump jslib
2021-04-22 18:13:43 +10:00
Thomas Rittson
714a574028 Do not show free trial wording if upgrading plan (#940) 2021-04-22 07:15:37 +10:00
Matt Gibson
3e8194a3f7 Update jslib (#942)
* Update jslib

* stub out new platformUtilsService method

* Throw not implemented

* Update jslib

* This interface method was reverted
2021-04-21 14:20:20 -05:00
Joseph Flinn
6e4782784c New client configuration pattern (#937)
* adding in initial config files

* working config files

* updating the client config pattern to default to dev instead of prod

* updating the npm script commands and docs

* Adding a helpful debugging log for the webpack build

* adding in more supporting documentation for running against production

* updating README.md and removing the unneeded ENV var
2021-04-21 11:29:33 -07:00
Joseph Flinn
ad40c38ca3 Build pipeline fix (#938)
* updating the build pipeline for the QA env

* changing the docker build context

* removed commented code

* moving commands to single line

* fixing typo

* removing unneeded build script
2021-04-20 13:27:09 -07:00
Thomas Rittson
68f2de171e Don't use tokenService to manage emailVerified (#932)
* update send add edit component dependencies
2021-04-15 16:28:21 +02:00
Oscar Hinton
a9ef011cf3 Remove dead code (#930)
* Remove last remnants of old analytics code
2021-04-14 23:43:40 +02:00
Matt Gibson
53bd9a3b14 Update jslib (#931) 2021-04-14 11:09:01 -05:00
Matt Gibson
1466933e2c update jslib (#928) 2021-04-12 11:36:00 -05:00
Kyle Spearrin
be515dc6a6 update types 2021-04-12 11:05:33 -04:00
Kyle Spearrin
83859230cd update some libs mentioned in npm audit 2021-04-09 12:52:41 -04:00
Kyle Spearrin
ec7a40df0b update jslib 2021-04-09 12:20:10 -04:00
Thomas Rittson
aba98ba944 Require user to verify email to use file Send (#915)
* Require user to verify email to use file Send

* bump jslib
2021-04-09 12:19:16 -04:00
Kyle Spearrin
f6e8c7152e npm audit fixes 2021-04-09 12:14:44 -04:00
Kyle Spearrin
3a1fd5ba83 update lib refs 2021-04-09 12:12:11 -04:00
Snyk bot
e43f816a8d fix: upgrade big-integer from 1.6.36 to 1.6.48 (#906)
Snyk has created this PR to upgrade big-integer from 1.6.36 to 1.6.48.

See this package in npm:
https://www.npmjs.com/package/big-integer

See this project in Snyk:
https://app.snyk.io/org/kspearrin/project/b65e7285-af54-4885-9245-af6852f0790a?utm_source=github&utm_medium=upgrade-pr
2021-04-09 12:08:04 -04:00
Snyk bot
9e61dbd512 fix: upgrade @microsoft/signalr-protocol-msgpack from 3.1.0 to 3.1.13 (#907)
Snyk has created this PR to upgrade @microsoft/signalr-protocol-msgpack from 3.1.0 to 3.1.13.

See this package in npm:
https://www.npmjs.com/package/@microsoft/signalr-protocol-msgpack

See this project in Snyk:
https://app.snyk.io/org/kspearrin/project/b65e7285-af54-4885-9245-af6852f0790a?utm_source=github&utm_medium=upgrade-pr

Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
2021-04-09 12:07:32 -04:00
Snyk bot
8734d028d3 fix: upgrade @microsoft/signalr from 3.1.0 to 3.1.13 (#905)
Snyk has created this PR to upgrade @microsoft/signalr from 3.1.0 to 3.1.13.

See this package in npm:
https://www.npmjs.com/package/@microsoft/signalr

See this project in Snyk:
https://app.snyk.io/org/kspearrin/project/b65e7285-af54-4885-9245-af6852f0790a?utm_source=github&utm_medium=upgrade-pr
2021-04-09 12:06:40 -04:00
Oscar Hinton
58850821ba Add proxies for notifications and portal. Simplify environment service (#919) 2021-04-09 09:57:25 +02:00
Oscar Hinton
f81ad479dd Resolve org user confirm not showing error when hide fingerprint is enabled (#918) 2021-04-09 00:46:16 +02:00
Vincent Salucci
133d30ba97 [Reset Password] Rotate encryption key (#916)
* [Reset Password] Rotate encryption key

* Added logic for updating reset password key only if necessary

* Updated user's resetPasswordKey for each confirmed organization on key rotation
2021-04-08 11:09:06 -05:00
Oscar Hinton
09fba343fc [Chore] Bump jslib (#917)
* Bump jslib
2021-04-07 20:42:57 +02:00
Vincent Salucci
ba3d4a2390 [Reset Password] Manage Reset Password permission (#902)
* [Reset Password] Manage Reset Password permission

* Update formatting

* Update jslib (f4f00b1 -> 97ece68)
2021-04-06 22:40:17 -05:00
Matt Gibson
b1c59f3dc1 Create index of cipher orgs and use advanced search with org ciphers (#903)
* Create index of cipher orgs and use advanced search with org ciphers

* Update jslib
2021-04-05 18:55:20 -05:00
Naoaki Iwakiri
89dc3b70e1 Sort weak passwords by severity (#446)
* Sort weak passwords by weakness

* Move static methods into local const
2021-04-05 18:23:48 -04:00
Oscar Hinton
769c247832 Configure webpack to proxy requests which avoids CORS issues (#914) 2021-04-05 22:38:21 +02:00
Oscar Hinton
12e4b614f5 Fix WebAuthn dialog not resetting on success (#910) 2021-04-05 22:37:02 +02:00
Thomas Rittson
b28eaa1aae Fix warning banner logic and open link in new tab (#909) 2021-04-06 06:26:04 +10:00
Matt Gibson
cd20b1c102 Update jslib (#913) 2021-04-05 15:19:46 -05:00
Erik Sennema
d6f80378eb Update README.md (#912) 2021-04-05 11:07:54 -04:00
Vincent Salucci
32e9124b9c [Reset Password] Enrollment actions (#900)
* [Reset Password] Enrollment actions

* Update jslib (0951424 -> f4f00b1)

* Added status icon
2021-04-05 09:48:46 -05:00
Matt Gibson
0aee3b7370 Update jslib (#901) 2021-03-30 19:01:16 -05:00
Thomas Rittson
6bb6a674ec Hide email address in Sends (#895)
* Let organizations disable anonymous sends only

* Add hide email option to send

* Display warning for anonymous Sends

* Enforce new Send policy, fix naming conventions

* Minor UI improvements

* Fix linting

* Fully disable editing anonymous Sends per policy

* Revert "Let organizations disable anonymous sends only"

This reverts commit 7877cb7751.

* Revert disableSendPolicy, add sendOptionsPolicy

* Rework UI for enforcing DisableHideEmail

* Fix typo

* Minor UI tweaks

* Minor UI tweaks

* Tweaks to UI copy

* Apply suggestions from code review

Minor changes to UI text

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>

* style fixes

* update jslib

* Move SendOptionsExemptions warning banner

* updated service params

* Remove whitespace

* updated jslib

* Revert "updated jslib"

This reverts commit 8fd141c5b7.

* updated jslib

* Attachment azure upload blobs (#898)

* Upload and download attachments using direct urls

* Include FileUploadService dependency

* Update max file size message to current max

* Update jslib

* Update jslib

* updated service params

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
Co-authored-by: addison <addisonbeck1@gmail.com>
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
2021-03-30 07:47:45 +10:00
Matt Gibson
29d7a5e37e Attachment azure upload blobs (#898)
* Upload and download attachments using direct urls

* Include FileUploadService dependency

* Update max file size message to current max

* Update jslib

* Update jslib
2021-03-29 09:44:08 -05:00
Matt Gibson
6067c1610c Azure upload blobs (#875)
* Include AzureStorageService in SendService

* Provide DI for abstrace AzureStorageService

* Use file upload service

* Update jslib
2021-03-26 16:54:13 -05:00
Daniel James Smith
1b74d22b46 Removed appveyor.yml and replaced badge in README.md (#896)
* Deleted appveyor.yml

* Replaced appveyor badge with GitHub workflow badge
2021-03-24 10:07:34 -04:00
cwille97
85a973afd4 Add locale info for hint equals password (#701)
Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
2021-03-23 17:11:24 -04:00
Oscar Hinton
1ea8762eeb WebAuthn (#633) 2021-03-16 17:44:31 +01:00
Matt Gibson
35ecbcc11a Open pdf in same tab for Safari (#888)
Safari blocks sharing objectURL data between tabs.
Just use the same tab.
2021-03-11 18:05:24 -06:00
Kyle Spearrin
3e988a741b New Crowdin updates (#887)
* New translations messages.json (Romanian)

* New translations messages.json (Russian)

* New translations messages.json (Latvian)

* New translations messages.json (Estonian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)

* New translations messages.json (Polish)

* New translations messages.json (Catalan)

* New translations messages.json (Dutch)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Hungarian)

* New translations messages.json (Finnish)

* New translations messages.json (German)

* New translations messages.json (Danish)

* New translations messages.json (English, United Kingdom)
2021-03-11 13:53:16 -05:00
Kyle Spearrin
db8f13d92f update jslib 2021-03-11 10:49:39 -05:00
Thomas Rittson
1a5885d6b4 Minor release version bump to 2.19.0 (#883)
* Minor release version bump to 2.19.0

* bump package-lock.json

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
2021-03-10 11:24:30 -05:00
Kyle Spearrin
31d2a09416 New Crowdin updates (#882)
* New translations messages.json (French)

* New translations messages.json (German)

* New translations messages.json (Slovak)

* New translations messages.json (Serbian (Cyrillic))
2021-03-09 13:18:52 -05:00
Kyle Spearrin
8ae96a6f88 New Crowdin updates (#881)
* New translations messages.json (Romanian)

* New translations messages.json (Bengali)

* New translations messages.json (Swedish)

* New translations messages.json (Turkish)

* New translations messages.json (Ukrainian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Vietnamese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Indonesian)

* New translations messages.json (Croatian)

* New translations messages.json (Slovenian)

* New translations messages.json (Estonian)

* New translations messages.json (Latvian)

* New translations messages.json (English, United Kingdom)

* New translations messages.json (Esperanto)

* New translations messages.json (Malayalam)

* New translations messages.json (Sinhala)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Serbian (Latin))

* New translations messages.json (Serbian (Cyrillic))

* New translations messages.json (Slovak)

* New translations messages.json (French)

* New translations messages.json (Greek)

* New translations messages.json (Spanish)

* New translations messages.json (Afrikaans)

* New translations messages.json (Belarusian)

* New translations messages.json (Bulgarian)

* New translations messages.json (Catalan)

* New translations messages.json (Czech)

* New translations messages.json (Danish)

* New translations messages.json (German)

* New translations messages.json (Finnish)

* New translations messages.json (Russian)

* New translations messages.json (Hebrew)

* New translations messages.json (Hungarian)

* New translations messages.json (Italian)

* New translations messages.json (Japanese)

* New translations messages.json (Korean)

* New translations messages.json (Dutch)

* New translations messages.json (Polish)

* New translations messages.json (Portuguese)

* New translations messages.json (English, India)
2021-03-09 11:48:27 -05:00
Addison Beck
d8aae1358b Send Date Fallback QA Fixes (#879)
* added expiration date time autopopulation and new messages

* updated jslib
2021-03-09 11:00:45 -05:00
Matt Gibson
ed53c3b8f6 Fix how it works link Send Info Card in Vault (#878) 2021-03-08 16:06:41 -06:00
Matt Gibson
79ffafcc17 Inform send in vault (#876)
* Add first pass info card to Vault view

* Make send-info title a link

* Make access links open new tab

* Separate Vault card message from access message

* Add period to card end

* Final wording. Change Bitwarden Send links to point to Send tab
2021-03-08 15:17:42 -06:00
Addison Beck
bdf6dcd8cd Safari date time fix (#877)
* date/time fixes for safari

* cleanup

* updated jslib

* whitespace
2021-03-08 16:10:16 -05:00
Thomas Rittson
ec3154ea46 Fix cross-origin issues on 404 page (#873)
* Fix cross-origin issues on 404 page

* Add integrity hashes and use stable boostrap

* Restore absolute paths for css
2021-03-08 08:05:12 +10:00
Thomas Rittson
08fc18192d update jslib (#872) 2021-03-05 09:55:59 +10:00
Joseph Flinn
b01c71f579 adding docker to the rc branch workflow (#870) 2021-03-04 13:28:32 -08:00
Matt Gibson
a6c98f462a Show spinner when downloading file for Send Accesses (#869) 2021-03-04 14:58:45 -06:00
Thomas Rittson
473dd8739a Expand encrypted export warning (#866)
* Add additional warnings about encrypted export

* Allow html body in showDialog
2021-03-05 06:56:25 +10:00
Joseph Flinn
722bcfc31b Add rc auto (#868)
* adding new rc docker tag

* adding rc docker push

* updating task name
2021-03-04 10:07:59 -08:00
Thomas Rittson
929c3d7662 Disable save button and add spinner after submit (#867) 2021-03-04 11:23:32 -05:00
Addison Beck
e25a8e051a updated jslib (#865) 2021-03-03 16:25:13 -05:00
Vincent Salucci
4a1b46dd41 [Send] Hide identifier if unavailable (#864) 2021-03-03 12:30:19 -06:00
Thomas Rittson
16877521e7 Exclude owners and admins from single organization policy when creating new org (#855)
* Fix single org policy when creating organization

Exclude owners and admins from policy when creating new org

* Remove looping async calls and fix linting
2021-03-03 08:16:04 +10:00
Addison Beck
a16abb94cd Date time fallback fixes (#863)
* changed required state of expiration date for edit vs create modes

* updated jslib
2021-03-02 17:04:04 -05:00
Addison Beck
5c8e9a990c changed send access creator identifier string (#862) 2021-03-02 16:17:56 -05:00
Addison Beck
c2515ed3ae added date/time fallbacks for safari/ff (#861)
* added date/time fallbacks for safari/ff

* updated jslib
2021-03-02 14:02:15 -05:00
Matt Gibson
227f457409 Use download link requests (#859)
* Use download link requests

* Update jslib

* Update jslib
2021-03-02 11:11:27 -06:00
vachan-maker
2e4a3501a2 Update change-kdf.component.html (#860) 2021-03-02 11:21:34 -05:00
Addison Beck
fade7f1713 updated jslib (#858) 2021-03-01 12:06:33 -05:00
Vincent Salucci
de84468ad8 [Send] Updated current access input type to text (#857) 2021-03-01 09:41:46 -06:00
Addison Beck
2e20978cee updated send access component (#852)
* updated send access component

* updated jslib

* bump jslib version (#851)

* code review fixes

* updated send access component

* updated jslib

* code review fixes

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
2021-02-25 17:25:53 -05:00
Thomas Rittson
2cc24335ef bump jslib version (#851) 2021-02-26 08:11:13 +10:00
Vincent Salucci
721a9f5f69 [Send] Web cleanup (#850) 2021-02-25 11:23:52 -06:00
Jens Spanier
4ebbefa181 Use v2 of 2fa.directory API (#849) 2021-02-25 11:25:48 -05:00
Addison Beck
6ad930c609 Web send updates (#848)
* updated send link copy toast message

* added a show/hide toggle for Send options
2021-02-24 13:27:07 -05:00
Thomas Rittson
85856d8390 Improve import error messages (#841)
* Display server import errors in modal

* Fix UI text and modal appearance

* Fix loading spinner behaviour

* Fix linting

* Update jslib version
2021-02-24 05:48:30 +10:00
Michael Loßin
a975f6df2b Change all "twofactorauth.org" references to "2fa.directory" (#840) 2021-02-22 12:45:23 -06:00
Limon Monte
d2f1e39a9b chore: bump sweetalert2 to latest (#835) 2021-02-18 16:48:58 -05:00
vachan-maker
8ef7944077 Add Emergency Access to Get Premium (#800)
* Add Emergency Access to Get Premium

* Added Emergency Access to messages.json

* Update premium.component.html

* Added opening tag

* Update messages.json
2021-02-17 10:22:51 -05:00
Thomas Rittson
2a19189f04 Show grantee email in modal if name is null (#831) 2021-02-17 12:07:13 +10:00
Vincent Salucci
cb4f318419 [Send] Update jslib and Send component (#826)
* changes made affected by jslib update

* Update jslib (380b28d -> 0951424)
2021-02-12 10:38:55 -06:00
Thomas Rittson
f239b0cd34 Improved handling of grantor access to organizations after takeover (#820)
* Add emergency access warning for Owners of orgs

* Add master password policy enforcement

* Only show password policy if taking over an Owner

* Fix linting errors

* Fix code style and typos

* Fix implicit 'any' type

* Get grantor policies in separate api call

* Update jslib
2021-02-12 09:58:22 +10:00
Thomas Rittson
9d1b2b9f60 Add warning when importing to organization (#825) 2021-02-12 08:13:29 +10:00
Vincent Salucci
168f9a5525 [Send] Update jslib and init function (#823)
* Update jslib to ee164bebc65aa56e41a122eb4ece8971eb23119b

* Overloaded ngOnInit to call this.load

* Updated import groupings/order based on lint warnings
2021-02-08 16:53:48 -06:00
Kyle Spearrin
13a04976fd send UX improvements (#822)
* send UX improvements

* typo
2021-02-08 16:07:40 -05:00
Matt Gibson
84d03158b5 Add show hide password to send (#821)
* Add password toggle to add-edit

* Fix remove password accessible from disabled send

* Update jslib
2021-02-05 13:23:40 -06:00
Matt Gibson
af7e2edbf0 Implement disable send policy (#819)
* Implement disable send policy

* Update jslib reference

* PR review

* Lower case enterprise policy
2021-02-04 13:08:16 -06:00
Matt Gibson
2e7b88f149 Fix glob processing in npm. Ban single param parens (#818) 2021-02-03 11:41:33 -06:00
Oscar Hinton
5010736ca3 Add support for viewing attachments in emergency access (#814) 2021-02-01 17:37:32 +01:00
Vincent Salucci
986f27294a [Send] Port web base components (#817)
* Initial commit of send base components

* update jslib (9ddec9b -> 859f317)
2021-02-01 10:30:27 -06:00
Matt Gibson
1b8cddede8 Set WebVaultUrl if dev (#813)
* Set WebVaultUrl if dev

* Add new jslib dependency
2021-01-29 13:07:55 -05:00
Addison Beck
66c814296b Lunr search bug (#810)
* changed hrtime library

* updated jslib
2021-01-25 15:11:51 -05:00
Kyle Spearrin
b14cdfcc72 New translations messages.json (Polish) (#809) 2021-01-25 13:10:05 -05:00
Kyle Spearrin
aba2c70ad7 New Crowdin updates (#807)
* New translations messages.json (Romanian)

* New translations messages.json (Polish)

* New translations messages.json (Malayalam)

* New translations messages.json (English, United Kingdom)

* New translations messages.json (Estonian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)

* New translations messages.json (Swedish)

* New translations messages.json (Serbian (Cyrillic))

* New translations messages.json (Russian)

* New translations messages.json (Dutch)

* New translations messages.json (French)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (Finnish)

* New translations messages.json (Greek)

* New translations messages.json (German)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Catalan)

* New translations messages.json (Afrikaans)

* New translations messages.json (Spanish)

* New translations messages.json (English, India)
2021-01-25 12:54:23 -05:00
Chad Scharf
46e9158323 Revert "Correct URL to emergency access doc (#805)" (#808)
This reverts commit 8449cdca75.
2021-01-25 11:59:10 -05:00
Gjermund Jensvoll
8449cdca75 Correct URL to emergency access doc (#805) 2021-01-25 09:49:27 -05:00
Thomas Rittson
8c0bc023b7 Allow U2F on Edge (#804) 2021-01-25 10:21:03 +10:00
Thomas Rittson
bcd488bb87 Clearer keyboard focus on input elements (#780)
* Add visible border/shadow if buttons have :focus

* Fix obscured outlines when elements have :focus
2021-01-25 06:24:09 +10:00
Thomas Rittson
a7b7c716d4 Add branded 404 page to replace Github Pages 404 (#798) 2021-01-25 06:23:18 +10:00
Addison Beck
6b29bb8468 Send Sync Notifications (#799)
* enabled send and added send sync notifications

* updated jslib
2021-01-22 17:03:55 -05:00
Thomas Rittson
3ffc035db3 make inline buttons accessible with tab button (#779) 2021-01-22 09:00:03 +10:00
Oscar Hinton
d93392ba8b Update emergency access user-access link (#797)
The help link for user-access incorrectly linked to the wrong page. Changed to the correct link.
2021-01-21 17:52:20 +01:00
vachan-maker
137b3b3490 Update learn more link for Emergency Access (#796) 2021-01-21 11:35:08 -05:00
Matt Gibson
99c8082866 Fix context copy buttons work only with TOTP present (#794) 2021-01-20 15:15:19 -06:00
Oscar Hinton
24af5aca55 Fix emergency access confirm not working with two-factor enabled (#792) 2021-01-20 17:33:03 +01:00
Kyle Spearrin
1429cb3f76 New Crowdin updates (#786)
* New translations messages.json (Romanian)

* New translations messages.json (Indonesian)

* New translations messages.json (Swedish)

* New translations messages.json (Turkish)

* New translations messages.json (Ukrainian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Vietnamese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Croatian)

* New translations messages.json (Slovenian)

* New translations messages.json (Estonian)

* New translations messages.json (Latvian)

* New translations messages.json (English, United Kingdom)

* New translations messages.json (Esperanto)

* New translations messages.json (Malayalam)

* New translations messages.json (Sinhala)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Serbian (Latin))

* New translations messages.json (Serbian (Cyrillic))

* New translations messages.json (Slovak)

* New translations messages.json (French)

* New translations messages.json (Greek)

* New translations messages.json (Spanish)

* New translations messages.json (Afrikaans)

* New translations messages.json (Belarusian)

* New translations messages.json (Bulgarian)

* New translations messages.json (Catalan)

* New translations messages.json (Czech)

* New translations messages.json (Danish)

* New translations messages.json (German)

* New translations messages.json (Finnish)

* New translations messages.json (Russian)

* New translations messages.json (Hebrew)

* New translations messages.json (Hungarian)

* New translations messages.json (Italian)

* New translations messages.json (Japanese)

* New translations messages.json (Korean)

* New translations messages.json (Dutch)

* New translations messages.json (Polish)

* New translations messages.json (Portuguese)

* New translations messages.json (English, India)
2021-01-19 21:51:17 -05:00
Thomas Rittson
13cbba3e99 Fix typo in selectedPlanInterval (#785) 2021-01-20 09:25:01 +10:00
Chad Scharf
73d24162a0 disable send (#784) 2021-01-19 17:03:34 -05:00
Chad Scharf
4964ebd31e Version bump to 2.18.0 (#783) 2021-01-19 16:18:52 -05:00
Kyle Spearrin
5a8198a878 fixes to plan/pricing page (#776) 2021-01-15 14:46:25 -05:00
Addison Beck
03aa806af6 fixed various Permissions UI issues (#775) 2021-01-14 18:08:26 -05:00
Addison Beck
023bf0474c Showed sales tax amount on Go Premium screen (#774) 2021-01-14 17:53:46 -05:00
Oscar Hinton
2e22ca9216 Remove grantee email from accept-emergency component (#773) 2021-01-14 20:22:49 +01:00
Addison Beck
5a540bba9e Fixed trailing comma lint warnings (#772)
* Fixed trailing comma lint warnings

* Specified options on tslint comma rule
2021-01-13 15:34:06 -05:00
Vincent Salucci
2047a6378b [Policy] Update Personal Ownership checkbox description (#767)
* Initial commit of enabled checkbox description update

* Updated requested changes

* Fixed merge conflict
2021-01-12 17:13:59 -06:00
Addison Beck
dc87510a7a Implemented Custom role and permissions (#750)
* Implemented Custom role and permissions

* converted Permissions interface into a class

* fixed a merge issue

* updated jslib

* code review cleanup for Permissions

* trailing commas
2021-01-12 15:31:22 -05:00
Khánh Hoàng
c3f4c6c03b Fix #759 for Print Code button in Two-step Login (#760) 2021-01-12 12:33:56 -05:00
Matt Gibson
e8b72477c9 Update jslib (#770) 2021-01-08 12:14:20 -06:00
Kyle Spearrin
862874c2ae ui updates for send add/edit component (#768)
* ui updates for send add/edit component

* move messaging.service import
2021-01-07 17:13:25 -05:00
Vincent Salucci
4d2d686078 [Policy] Personal Ownership banner (#764)
* Updated banner position and message

* updated capitalization
2021-01-06 10:01:34 -06:00
Kyle Spearrin
6d458646fa add predefined time frames for delete and expire (#765) 2021-01-05 14:45:23 -05:00
Matt Gibson
a1345488d0 Update jslib (#763) 2021-01-04 11:38:16 -06:00
Kyle Spearrin
c43012a5f2 send improvements and bug fixes (#757)
* send improvements and bug fixes

* update jslib

* update jslib

* update jslib

* update jslib ref

* Hide match uri overflow (#758)

match descriptions are overflowing in german and causing the uri delete
button to overflow off of the cipher view modal

* update jslib

* jslib ref

* update jslib

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
2021-01-04 10:57:53 -05:00
Kyle Spearrin
577cab24c4 update jslib ref 2021-01-04 10:55:58 -05:00
Vincent Salucci
5c0a77aec8 update jslib (48144a7) (#755) 2020-12-30 20:35:21 -06:00
Matt Gibson
a0904b14ed Hide match uri overflow (#758)
match descriptions are overflowing in german and causing the uri delete
button to overflow off of the cipher view modal
2020-12-30 17:12:01 -06:00
Kyle Spearrin
6774ae0ef3 warning dialog is now handled in base component (#751) 2020-12-22 16:37:50 -05:00
Oscar Hinton
5a76ca4676 Fix linting errors (#749)
* Fix linting errors

* Added back the form promise
2020-12-22 11:28:58 -05:00
Oscar Hinton
3c5a972bc9 Add support for Emergency Access (#707)
* Add support for Emergency Access

* Cleanup & Bugfix

* Apply suggestions from code review

Co-authored-by: Addison Beck <addisonbeck1@gmail.com>

* Cleanup some more imports

* Restrict emergency access invite to premium users

* Restrict editing existing emergency accesses to premium account.

* Handle changes in jslib

* Add some info messages for when you haven't been granted or invited emergency contacts

* Resolve review comments

* Update jslib

Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
2020-12-22 10:57:44 -05:00
Joseph Flinn
54b68ac543 fixing incorrect secret name (#747) 2020-12-21 12:58:51 -08:00
Joseph Flinn
ff378f05fe Moving appveyor to actions (#746)
* initial build testing

* fixing the release event

* fixing typo

* adding windows build

* fixing yaml

* fixing yaml again...

* fixing the windows build
2020-12-21 09:46:36 -08:00
Chad Scharf
f207aa3a9d show caret or icon, not just "blank" space (#745) 2020-12-21 10:59:07 -05:00
Vincent Salucci
7b43dcb6a1 [Policy] Single Org dependency chain (#739)
* Initial commit of Single Org downstream policy checks

* Moved comments
2020-12-17 14:20:45 -06:00
Matt Gibson
c487cf3284 Add message for missing event type (#740)
* Add message for missing event type

* update jslib reference
2020-12-16 18:29:19 -06:00
Chad Scharf
c2e1d325f2 update jslib and fix webPlatformUtils (#741) 2020-12-16 16:40:10 -05:00
Mithilesh Zavar
f090e8febf Password History Overflow (#743) 2020-12-16 15:47:37 -05:00
Wyatt Childers
087c84bcfb Only show carats for items with children (#410)
* Only show carats for folders with children

* Only show carats for collections with children
2020-12-15 16:32:49 -05:00
Mithilesh Zavar
a457c83242 Attachments with long file names go beyond the window. #695 (#702) 2020-12-15 16:03:17 -05:00
Matt Gibson
bcd8963e8b Add totp copy to clipboard button to cipher view (#737)
* Add totp copy to clipboard button to cipher view

* Align totp copy privs with cipher view

* Enforce TOTP as premium feature

* Update jslib reference
2020-12-15 10:25:52 -06:00
Matt Gibson
1464e0fbe8 Add ConsoleLogService dependency from jslib (#735)
* Pre-emptively add new jslib dependency

* Add consoleLogService dependency definition

* Update jslib

* PR Review

Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-12-14 12:27:32 -06:00
Kyle Spearrin
ec2b048289 enable send 2020-12-11 16:43:28 -05:00
Joshua McCauley
04811c934f Updated readme to note how to run the app against production (#706)
* Noted upper limit of Node.js verion support for the application: the SCSS dependency v4.13.1 only supports Node.js up to v13.9.0.
Added note for npm commands for running the application against local APIs versus production. The correct npm command for running against production was found here https://github.com/bitwarden/web/issues/666.
Added more lines to the services.module.ts example to better reflect the actual file.

* Added CORS common issue and solution to README.md

* Changed Node.js version notes for real this time.
2020-12-10 12:08:31 -05:00
Vincent Salucci
f84ee30b9d Fixed casing for error message (#734) 2020-12-09 13:30:55 -06:00
Vincent Salucci
218caa28b0 [Policy] Personal Ownership (#722)
* Initial commit of personal ownership policy

* Added event handling for modifying policies

* I didn't save the merge conflict fix...

* Removed unused import

* Updated jslib (dcbd09e -> 2d62e10)
2020-12-08 13:24:59 -06:00
Matt Gibson
a8af807650 Add help text for jslib's mac importer (#731)
Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-12-08 09:16:50 -06:00
vachan-maker
826170507e Updated Firefox Import Instructions (#728) 2020-12-07 16:51:53 -05:00
Kyle Spearrin
c37979e48d update getImporter signature pass org id (#730) 2020-12-07 13:02:56 -05:00
Addison Beck
7ebb046cd8 updated jslib (#727) 2020-12-04 12:39:41 -05:00
Addison Beck
7c4d0a15dd Implemented tax collection for subscriptions (#723)
* Implemented tax collection for subscriptions

* Cleanup for Sales Tax

* Code review fixes for Tax Rate implementation

* Code review fixes for Tax Rate implementation
2020-12-04 12:05:44 -05:00
Kyle Spearrin
512b9e0a92 encrypted json export option for user and orgs (#726)
* encrypted json export option for user and orgs

* move org id to base export component
2020-12-04 09:58:26 -05:00
Kyle Spearrin
5e95a8565c disable send 2020-12-02 15:23:54 -05:00
Kyle Spearrin
e83d0f2a9d bump version 2020-12-02 15:16:39 -05:00
Kyle Spearrin
eaebbcf6c8 New Crowdin updates (#725)
* New translations messages.json (Romanian)

* New translations messages.json (Indonesian)

* New translations messages.json (Swedish)

* New translations messages.json (Turkish)

* New translations messages.json (Ukrainian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Vietnamese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Croatian)

* New translations messages.json (Slovak)

* New translations messages.json (Estonian)

* New translations messages.json (Latvian)

* New translations messages.json (English, United Kingdom)

* New translations messages.json (Esperanto)

* New translations messages.json (Malayalam)

* New translations messages.json (Sinhala)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Serbian (Latin))

* New translations messages.json (Serbian (Cyrillic))

* New translations messages.json (Russian)

* New translations messages.json (French)

* New translations messages.json (German)

* New translations messages.json (Spanish)

* New translations messages.json (Afrikaans)

* New translations messages.json (Belarusian)

* New translations messages.json (Bulgarian)

* New translations messages.json (Catalan)

* New translations messages.json (Czech)

* New translations messages.json (Danish)

* New translations messages.json (Greek)

* New translations messages.json (Portuguese)

* New translations messages.json (Finnish)

* New translations messages.json (Hebrew)

* New translations messages.json (Hungarian)

* New translations messages.json (Italian)

* New translations messages.json (Japanese)

* New translations messages.json (Korean)

* New translations messages.json (Dutch)

* New translations messages.json (Polish)

* New translations messages.json (English, India)
2020-12-02 15:13:29 -05:00
Matt Gibson
7df5ed9b35 Terser minimizer requires option to include maps (#721)
Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-30 11:14:21 -06:00
Matt Gibson
6b66f14319 Update web sso content to indicate window OK to close (#720)
* Update web sso content to indicate window OK to close

This is done after the authResult handoff message is delivered to the
extension. It is not possible to close the window from javascript as
closing a window is limited to the script that opened it.

If we maintain a reference to the web window, it should be possible to
subscribe to the authResult message and close the web windows from the
browser.

* Use i18n for close tab message

* delete cookie after it is used

Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-25 15:57:11 -06:00
Chad Scharf
2db1684b3c Exclude deleted items from any/all reports (#700) 2020-11-24 12:36:40 -05:00
Matt Gibson
4625b44703 WIP: dirty fix to SSO web vs browser redirect logic split (#719)
* WIP: dirty fix to SSO web vs browser redirect logic split

* Use includes for clientId identification

routing determination more robust to future state string changes

Co-authored-by: Addison Beck <abeck@bitwarden.com>

Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
Co-authored-by: Addison Beck <abeck@bitwarden.com>
2020-11-24 11:29:04 -06:00
Chad Scharf
0356ecc17b update jslib (#717) 2020-11-23 17:36:17 -05:00
Oscar Hinton
1e7c27fba1 Change supportsSecureStorage to false (#716) 2020-11-23 15:56:22 -05:00
Vincent Salucci
03f575f66f [Bug] Update 2fa navigate action to pass along Org Identifier (#714)
* Add identifer in 2fa navigate action

* Update jslib (6563dcc -> d9d13bb)

* fixed breaking changes from jslib update
2020-11-23 09:12:12 -06:00
Matt Gibson
82b36c1b70 Use mobile's trash message for item delete (#710)
Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
2020-11-19 11:38:53 -06:00
Kyle Spearrin
6878ab51fb send service implementation (#708)
* send service implementation

* update jslib
2020-11-18 15:18:13 -05:00
Chad Scharf
8662033979 re-enable send (#709) 2020-11-18 12:44:21 -05:00
Kyle Spearrin
ef61652fba bump version 2020-11-12 22:49:30 -05:00
Kyle Spearrin
933a66b24c disable send 2020-11-12 22:05:50 -05:00
Kyle Spearrin
e2c6a5f8cd New Crowdin updates (#704)
* New translations messages.json (French)

* New translations messages.json (Slovak)

* New translations messages.json (Malayalam)

* New translations messages.json (English, United Kingdom)

* New translations messages.json (Indonesian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)

* New translations messages.json (Serbian (Cyrillic))

* New translations messages.json (Russian)

* New translations messages.json (Spanish)

* New translations messages.json (Polish)

* New translations messages.json (Dutch)

* New translations messages.json (Hungarian)

* New translations messages.json (Finnish)

* New translations messages.json (German)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Catalan)

* New translations messages.json (Bulgarian)
2020-11-12 21:44:42 -05:00
Kyle Spearrin
a818e7dd40 New Crowdin updates (#697)
* New translations messages.json (Romanian)

* New translations messages.json (Indonesian)

* New translations messages.json (Swedish)

* New translations messages.json (Turkish)

* New translations messages.json (Ukrainian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Vietnamese)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Croatian)

* New translations messages.json (Slovak)

* New translations messages.json (Estonian)

* New translations messages.json (Latvian)

* New translations messages.json (English, United Kingdom)

* New translations messages.json (Esperanto)

* New translations messages.json (Malayalam)

* New translations messages.json (Sinhala)

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Serbian (Latin))

* New translations messages.json (Serbian (Cyrillic))

* New translations messages.json (Russian)

* New translations messages.json (French)

* New translations messages.json (German)

* New translations messages.json (Spanish)

* New translations messages.json (Afrikaans)

* New translations messages.json (Belarusian)

* New translations messages.json (Bulgarian)

* New translations messages.json (Catalan)

* New translations messages.json (Czech)

* New translations messages.json (Danish)

* New translations messages.json (Greek)

* New translations messages.json (Portuguese)

* New translations messages.json (Finnish)

* New translations messages.json (Hebrew)

* New translations messages.json (Hungarian)

* New translations messages.json (Italian)

* New translations messages.json (Japanese)

* New translations messages.json (Korean)

* New translations messages.json (Dutch)

* New translations messages.json (Polish)

* New translations messages.json (English, India)
2020-11-10 17:19:12 -05:00
Addison Beck
759dc647e5 Implement User-based API Keys (#688)
* refactored api key modal for multiple key types

* Added support for viewing and rotating user API keys

* Fixed the API key component references in app.module

* Implemented User ApiKey viewing/rotating

* Changed ApiKey grant_type display to client_credentials

* Hopefully put jslib back

* Added new localization strings for user API keys

* Toggled button text based on if viewing or rotating an api key

* updated jslib

* Reverted jslib

* Trying to fix jslib

* Reverted jslib from commit hash

* Reupdated jslib
2020-11-10 16:13:42 -05:00
Matthew
37cf46d581 Add 'Copy Username' button (#691)
This adds a 'Copy Username' button above the 'Copy Password' button in
the dropdown for individual entries in the safe. This matches the
capabilities of the desktop app, where you can right-click on any entry
and get options for both 'copy password' and 'copy username'.
2020-11-10 14:26:38 -05:00
Vincent Salucci
407032114e [Exemption] Updated policy messages (#692)
* Updated mesages // added callout for require sso

* removed unused string

* updated strings - futureproofing
2020-11-10 09:53:57 -06:00
eliykat
94aece134c Docs contrib (#696)
* expand contributing guide

* fix typo
2020-11-10 10:52:09 -05:00
Christian Oliff
7532bf9825 HTTPS link to EditorConfig.org (#694) 2020-11-09 15:30:33 -05:00
Kyle Spearrin
0f4f541b11 some filtering logic for sends (#689) 2020-11-05 14:41:54 -05:00
Kyle Spearrin
07a3d38bef fix compile errors 2020-11-04 16:30:19 -05:00
Kyle Spearrin
e9273ff79a Send initial implementation (#687)
* send work

* Bump version to 2.16.2 (#668)

* [SSO] New User Provision flow jslib update (f30d6f8 -> d84d6da) (#672)

* Update jslib (f30d6f8 -> d84d6da)

* Updated imports/constructor to super

* OnlyOrg Policy (#669)

* added localization strings needed for the OnlyOrg policy

* added deprecation warning to policies page

* allowed OnlyOrg policy configuration

* blocked creating new orgs if already in an org with OnlyOrg enabled

* code review cleanup for onlyOrg

* removed a blank line

* code review cleanup for onlyOrg

* send listing actions

* updates

* access id

* update jslib

* re-work key and password derivation

* update jslib

* makeSendKey

* update access path

* store max access count

* update jslib

* l10n work

* l10n for access page

* l10n and cleanup

* fix l10n

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Addison Beck <abeck@bitwarden.com>
2020-11-04 14:49:08 -05:00
Vincent Salucci
1aa708aed4 [GDPR] Adjusted TOS/Privacy acceptance (#684)
* initial commit for GDPR terms/privacy acceptance

* updated styling/formatting

* Fixed line break in blockquote

* removed unused submit message

* Removed variables/logic now found in superclass

* update jslib (76c0964 -> 5e50aa1)
2020-11-02 16:33:15 -06:00
Addison Beck
ebe5a6030e Only org to single org (#680)
* change OnlyOrg references to SingleOrg

* updated jslib

* change OnlyOrg references to SingleOrg

* missed a reference to OnlyOrg in messages
2020-10-27 10:28:57 -04:00
Chad Scharf
f6946085d8 Use properly transpiled SweetAlert2 lib (#682) 2020-10-26 17:52:01 -04:00
Vincent Salucci
beebe7c98b [Require SSO] Enterprise policy adjustment (#676)
* Commits for policies/edit/strings

* more initial commits of policy/edit/strings

* More changes for require sso

* Updated strings to match policy string patterns

* Updated false enable on error

* Removed sso prevalidate prereq // adjusted callout

* Updated policy array creation and added display value
2020-10-26 11:56:02 -05:00
Addison Beck
a51331d6b2 OnlyOrg Policy (#669)
* added localization strings needed for the OnlyOrg policy

* added deprecation warning to policies page

* allowed OnlyOrg policy configuration

* blocked creating new orgs if already in an org with OnlyOrg enabled

* code review cleanup for onlyOrg

* removed a blank line

* code review cleanup for onlyOrg
2020-10-16 15:36:06 -04:00
Vincent Salucci
b7b970e654 [SSO] New User Provision flow jslib update (f30d6f8 -> d84d6da) (#672)
* Update jslib (f30d6f8 -> d84d6da)

* Updated imports/constructor to super
2020-10-14 11:13:13 -05:00
248 changed files with 59342 additions and 7189 deletions

View File

@@ -1,4 +1,4 @@
# EditorConfig is awesome: http://EditorConfig.org
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
@@ -13,3 +13,6 @@ insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
[*.{ts}]
quote_type = single

202
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,202 @@
name: build
on:
push:
branches-ignore:
- 'l10n_master'
- 'gh-pages'
release:
types:
- published
jobs:
cloc:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Set up cloc
run: |
sudo apt update
sudo apt -y install cloc
- name: Print lines of code
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
ubuntu:
runs-on: ubuntu-latest
steps:
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Print environment
run: |
whoami
node --version
npm --version
gulp --version
docker --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Login to Azure
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "docker-password,
docker-username,
dct-delegate-2-repo-passphrase,
dct-delegate-2-key"
- name: Log into docker
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
env:
DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }}
DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }}
- name: Setup Docker Trust
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
run: |
mkdir -p ~/.docker/trust/private
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
env:
DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c"
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
- name: Checkout repo
uses: actions/checkout@v2
- name: Restore
run: dotnet tool restore
- name: Build
run: |
echo -e "# Building Web\n"
echo "Building app"
echo "npm version $(npm --version)"
npm install
npm run dist:selfhost
echo -e "\nBuilding docker image"
docker --version
docker build -t bitwarden/web .
- name: Tag rc branch
if: github.ref == 'refs/heads/rc'
run: docker tag bitwarden/web bitwarden/web:rc
- name: Tag dev
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: docker tag bitwarden/web bitwarden/web:dev
- name: Tag beta
if: github.event_name == 'release'
run: docker tag bitwarden/web bitwarden/web:beta
- name: Tag version
if: github.event_name == 'release'
run: docker tag bitwarden/web bitwarden/web:$($env:RELEASE_TAG_NAME.trimStart('v'))
shell: pwsh
env:
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
- name: List docker images
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
run: docker images
- name: Push rc images
if: github.ref == 'refs/heads/rc'
run: docker push bitwarden/web:rc
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
- name: Push dev images
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
run: docker push bitwarden/web:dev
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
- name: Push beta images
if: github.event_name == 'release'
run: docker push bitwarden/web:beta
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
- name: Push latest images
if: github.event_name == 'release'
run: docker push bitwarden/web:latest
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
- name: Push version images
if: github.event_name == 'release'
run: docker push bitwarden/web:$($env:RELEASE_TAG_NAME.trimStart('v'))
shell: pwsh
env:
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
- name: Log out of docker
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
run: docker logout
windows:
runs-on: windows-latest
steps:
- name: Set up NuGet
uses: nuget/setup-nuget@v1
with:
nuget-version: 'latest'
- name: Set up MSBuild
uses: microsoft/setup-msbuild@v1
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Print environment
run: |
nuget help
msbuild -version
dotnet --info
node --version
npm --version
Write-Output "GitHub ref: $env:GITHUB_REF"
Write-Output "GitHub event: $env:GITHUB_EVENT"
shell: pwsh
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Checkout repo
uses: actions/checkout@v2
- name: npm install
run: npm install
- name: npm build
run: npm run build:prod

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ dist/
*.zip
build/
!dev-server.shared.pem
config/development.json

View File

@@ -1,4 +1,32 @@
Code contributions are welcome! Please commit any pull requests against the `master` branch.
# How to Contribute
Contributions of all kinds are welcome!
Please visit our [Community Forums](https://community.bitwarden.com/) for general community discussion and the development roadmap.
Here is how you can get involved:
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
* **Report a bug or submit a bugfix:** Use Github issues and pull requests
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
* **Translate:** See the localization (l10n) section below
## Contributor Agreement
Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/web) if you intend on contributing to any Github repository. Pull requests cannot be accepted and merged unless the author has signed the Contributor Agreement.
## Pull Request Guidelines
* use `npm run lint` and fix any linting suggestions before submitting a pull request
* commit any pull requests against the `master` branch
* include a link to your Community Forums post
# Localization (l10n)

View File

@@ -5,8 +5,8 @@
The Bitwarden web project is an Angular application that powers the web vault (https://vault.bitwarden.com/).
</p>
<p align="center">
<a href="https://ci.appveyor.com/project/bitwarden/web/branch/master" target="_blank">
<img src="https://ci.appveyor.com/api/projects/status/github/bitwarden/web?branch=master&svg=true" alt="appveyor build" />
<a href="https://github.com/bitwarden/web/actions?query=branch:master" target="_blank">
<img src="https://github.com/bitwarden/web/actions/workflows/build.yml/badge.svg?branch=master" alt="Github Workflow build on master" />
</a>
<a href="https://crowdin.com/project/bitwarden-web" target="_blank">
<img src="https://d322cqt584bo4o.cloudfront.net/bitwarden-web/localized.svg" alt="Crowdin" />
@@ -27,34 +27,43 @@
### Run the app
For local development, run the app with:
```
npm install
npm run build:watch
```
You can now access the web vault in your browser at `https://localhost:8080`. You can adjust your API endpoint settings in `src/app/services/services.module.ts` by altering the `apiService.setUrls` call. For example:
You can now access the web vault in your browser at `https://localhost:8080`.
```typescript
await apiService.setUrls({
base: isDev ? null : window.location.origin,
api: isDev ? 'http://mylocalapi' : null,
identity: isDev ? 'http://mylocalidentity' : null,
});
If you want to point the development web vault to the production APIs, you can run using:
```
npm install
ENV=production npm run build:watch
```
If you want to point the development web vault to the production APIs, you can set:
You can also manually adjusting your API endpoint settings by adding `config/development.js` overriding any of the values in `config/base.json`. For example:
```typescript
await apiService.setUrls({
base: null,
api: 'https://api.bitwarden.com',
identity: 'https://identity.bitwarden.com',
});
{
"proxyApi": "http://your-api-url",
"proxyIdentity": "http://your-identity-url",
"proxyEvents": "http://your-events-url",
"proxyNotifications": "http://your-notifications-url",
"proxyPortal": "http://your-portal-url",
"allowedHosts": ["hostnames-to-allow-in-webpack"]
}
```
To pick up the overrides in the newly created `config/development.js` file, run the app with:
```
npm run build:dev:watch
```
## Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch.
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.

View File

@@ -1,83 +0,0 @@
image:
- Visual Studio 2017
- Ubuntu1804
branches:
except:
- l10n_master
- gh-pages
services:
- docker
stack: node 10
init:
- ps: |
if($isWindows) {
Install-Product node 10
}
install:
- ps: |
$env:PACKAGE_VERSION = (Get-Content -Raw -Path .\package.json | ConvertFrom-Json).version
$env:PUSH_DOCKER = "false"
$env:PROD_DEPLOY = "false"
$env:TAG_NAME = ""
if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") {
$env:PROD_DEPLOY = "true"
$env:TAG_NAME = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v")
echo "This is a production deployment for ${env:TAG_NAME}."
}
if("${env:DOCKER_USERNAME}" -ne "" -and "${env:DOCKER_PASSWORD}" -ne "") {
$env:PUSH_DOCKER = "true"
}
if($isWindows) {
choco install cloc --no-progress
cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
}
before_build:
- node --version
- npm --version
- sh: |
if [ "${PUSH_DOCKER}" == "true" ]
then
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
fi
- cmd: set "GIT_PATH=C:\Program Files\Git\mingw64\libexec\git-core"
- cmd: set "PATH=%GIT_PATH%;%PATH%"
build_script:
- sh: chmod +x ./build.sh
- ps: |
if($isLinux) {
./build.sh
./build.sh tag dev
if($env:PROD_DEPLOY -eq "true") {
./build.sh tag beta
./build.sh tag $env:TAG_NAME
}
docker images
if($env:PUSH_DOCKER -eq "true") {
./build.sh push dev
if($env:PROD_DEPLOY -eq "true") {
./build.sh push beta
./build.sh push latest
./build.sh push $env:TAG_NAME
}
}
}
- cmd: npm install
- cmd: npm run build:prod
after_build:
- sh: |
if [ "${PUSH_DOCKER}" == "true" ]
then
docker logout
fi

View File

@@ -1,33 +0,0 @@
#!/usr/bin/env bash
set -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
echo ""
if [ $# -gt 1 -a "$1" == "push" ]
then
TAG=$2
echo "# Pushing Web ($TAG)"
echo ""
docker push bitwarden/web:$TAG
elif [ $# -gt 1 -a "$1" == "tag" ]
then
TAG=$2
echo "Tagging Web as '$TAG'"
docker tag bitwarden/web bitwarden/web:$TAG
else
echo "# Building Web"
echo ""
echo "Building app"
echo "npm version $(npm --version)"
npm install
npm run sub:update
npm run dist:selfhost
echo ""
echo "Building docker image"
docker --version
docker build -t bitwarden/web $DIR/.
fi

29
config.js Normal file
View File

@@ -0,0 +1,29 @@
function load(envName) {
const envOverrides = {
'production': () => require('./config/production.json'),
'qa': () => require('./config/qa.json'),
'development': () => require('./config/development.json'),
};
const baseConfig = require('./config/base.json');
const overrideConfig = envOverrides.hasOwnProperty(envName) ? envOverrides[envName]() : {};
return {
...baseConfig,
...overrideConfig
};
}
function log(configObj) {
const repeatNum = 50
console.log(`${"=".repeat(repeatNum)}\nenvConfig`)
Object.entries(configObj).map(([key, value]) => {
console.log(` ${key}: ${value}`)
})
console.log(`${"=".repeat(repeatNum)}`)
}
module.exports = {
load,
log
};

8
config/base.json Normal file
View File

@@ -0,0 +1,8 @@
{
"proxyApi": "http://localhost:4000",
"proxyIdentity": "http://localhost:33656",
"proxyEvents": "http://localhost:46273",
"proxyNotifications": "http://localhost:61840",
"proxyPortal": "http://localhost:52313",
"allowedHosts": []
}

7
config/production.json Normal file
View File

@@ -0,0 +1,7 @@
{
"proxyApi": "https://api.bitwarden.com",
"proxyIdentity": "https://identity.bitwarden.com",
"proxyEvents": "https://events.bitwarden.com",
"proxyNotifications": "https://notifications.bitwarden.com",
"proxyPortal": "https://portal.bitwarden.com"
}

7
config/qa.json Normal file
View File

@@ -0,0 +1,7 @@
{
"proxyApi": "https://api.qa.bitwarden.com",
"proxyIdentity": "https://identity.qa.bitwarden.com",
"proxyEvents": "https://events.qa.bitwarden.com",
"proxyNotifications": "https://notifications.qa.bitwarden.com",
"proxyPortal": "https://portal.qa.bitwarden.com"
}

2
jslib

Submodule jslib updated: f30d6f8027...f6d91e2d92

1932
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "bitwarden-web",
"version": "2.16.2",
"version": "2.20.1",
"license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web",
"scripts": {
@@ -13,8 +13,12 @@
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"build": "gulp prebuild && webpack",
"build:watch": "gulp prebuild && webpack-dev-server",
"build:prod": "gulp prebuild && cross-env NODE_ENV=production webpack",
"build:prod:watch": "gulp prebuild && cross-env NODE_ENV=production webpack-dev-server",
"build:dev": "gulp prebuild && cross-env ENV=development webpack",
"build:dev:watch": "gulp prebuild && cross-env ENV=development webpack-dev-server",
"build:qa": "gulp prebuild && cross-env NODE_ENV=production ENV=qa webpack",
"build:qa:watch": "gulp prebuild && cross-env NODE_ENV=production ENV=qa webpack-dev-server",
"build:prod": "gulp prebuild && cross-env NODE_ENV=production ENV=production webpack",
"build:prod:watch": "gulp prebuild && cross-env NODE_ENV=production ENV=production webpack-dev-server",
"build:selfhost": "gulp prebuild && cross-env SELF_HOST=true webpack-dev-server",
"build:selfhost:watch": "gulp prebuild && cross-env SELF_HOST=true webpack-dev-server",
"build:selfhost:prod": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack",
@@ -24,23 +28,23 @@
"dist:selfhost": "npm run build:selfhost:prod && gulp postdist",
"deploy": "npm run dist && gh-pages -d build",
"deploy:dev": "npm run dist && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
"lint": "tslint src/**/*.ts || true",
"lint:fix": "tslint src/**/*.ts --fix"
"lint": "tslint 'src/**/*.ts' || true",
"lint:fix": "tslint 'src/**/*.ts' --fix"
},
"devDependencies": {
"@angular/compiler-cli": "^9.1.12",
"@ngtools/webpack": "^9.1.12",
"@types/jquery": "^3.5.1",
"@types/jquery": "^3.5.5",
"@types/lunr": "^2.3.3",
"@types/node": "^10.17.28",
"@types/node-forge": "^0.7.5",
"@types/papaparse": "^4.5.3",
"@types/node-forge": "^0.9.7",
"@types/papaparse": "^5.2.0",
"@types/webcrypto": "^0.0.28",
"@types/webpack": "^4.4.11",
"@types/zxcvbn": "^4.4.0",
"angular2-template-loader": "^0.6.2",
"clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "^4.5.2",
"copy-webpack-plugin": "^5.1.1",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"del": "^3.0.0",
@@ -73,25 +77,26 @@
"@angular/platform-browser": "9.1.12",
"@angular/platform-browser-dynamic": "9.1.12",
"@angular/router": "9.1.12",
"@microsoft/signalr": "3.1.0",
"@microsoft/signalr-protocol-msgpack": "3.1.0",
"@microsoft/signalr": "3.1.13",
"@microsoft/signalr-protocol-msgpack": "3.1.13",
"angular2-toaster": "8.0.0",
"angulartics2": "9.1.0",
"big-integer": "1.6.36",
"big-integer": "1.6.48",
"bootstrap": "4.3.1",
"braintree-web-drop-in": "1.13.0",
"browser-hrtime": "^1.1.8",
"core-js": "2.6.2",
"date-input-polyfill": "^2.14.0",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git#410a9186cc34663c4913b17d6528067cd3331f1d",
"font-awesome": "4.7.0",
"jquery": "3.4.1",
"jquery": "3.6.0",
"lunr": "2.3.3",
"ngx-infinite-scroll": "7.0.1",
"node-forge": "0.7.6",
"papaparse": "4.6.0",
"node-forge": "0.10.0",
"papaparse": "5.2.0",
"popper.js": "1.14.4",
"qrious": "4.0.2",
"rxjs": "6.6.2",
"sweetalert2": "9.8.1",
"sweetalert2": "10.15.4",
"tslib": "^2.0.1",
"web-animations-js": "2.3.1",
"webcrypto-shim": "0.1.4",

50
src/404.html Normal file
View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/404/bootstrap.min.css" rel="stylesheet" type="text/css"
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l">
<link href="/404/font-awesome.min.css" rel="stylesheet" type="text/css"
integrity="sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw==">
<link href="/404/styles.css" rel="stylesheet" type="text/css">
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png">
<link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#175DDC">
<link rel="manifest" href="/manifest.json">
<title>Page not found!</title>
<meta name="description" content="404 Page Not Found">
</head>
<body>
<div class="banner">
<div class="container inner banner">
<div class="row align-items-center">
<div class="col brand">
<i class="fa fa-shield"></i>&nbsp;
<strong>bit</strong>warden</span>
</div>
</div>
</div>
</div>
<div class="container inner content">
<h2>Page not found!</h2>
<p>Sorry, but the page you were looking for could not be found.</p>
<p>
<a href="/">
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%"/>
</a>
</p>
<p>You can <a href="/">return to the web vault</a>, check our <a href="https://status.bitwarden.com/">status page</a>
or <a href="https://bitwarden.com/contact/">contact us</a>.</p>
</div>
<div class="container footer text-muted content">
© Copyright 2021 Bitwarden, Inc.
</div>
</body>
</html>

7
src/404/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

4
src/404/font-awesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

119
src/404/styles.css Normal file
View File

@@ -0,0 +1,119 @@
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: url(../fonts/Open_Sans-italic-300.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: url(../fonts/Open_Sans-italic-400.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: url(../fonts/Open_Sans-italic-600.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: url(../fonts/Open_Sans-italic-700.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
src: url(../fonts/Open_Sans-italic-800.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: url(../fonts/Open_Sans-normal-300.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: url(../fonts/Open_Sans-normal-400.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: url(../fonts/Open_Sans-normal-600.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: url(../fonts/Open_Sans-normal-700.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
src: url(../fonts/Open_Sans-normal-800.woff) format('woff');
unicode-range: U+0-10FFFF;
}
body {
font-family: 'Open Sans';
}
html, body, .row {
height: 100%;
-webkit-font-smoothing: antialiased;
}
h2 {
font-size: 25px;
margin-bottom: 12.5px;
font-weight: 500;
line-height: 1.1;
}
.brand {
font-size: 23px;
line-height: 25px;
color: #fff;
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
}
.banner {
background-color: #175DDC;
height: 56px;
}
.content {
padding-top: 20px;
padding-bottom: 20px;
padding-left: 15px;
padding-right: 15px;
}
.footer {
padding: 40px 0 40px 0;
border-top: 1px solid #dee2e6;
}

View File

@@ -0,0 +1,34 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div>
<img src="../../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
<p class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</p>
</div>
</div>
<div class="container" *ngIf="!loading && !authed">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'emergencyAccess' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<p class="text-center">
{{name}}
</p>
<p>{{'acceptEmergencyAccess' | i18n}}</p>
<hr>
<div class="d-flex">
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
{{'logIn' | i18n}}
</a>
<a routerLink="/register" [queryParams]="{email: email}"
class="btn btn-primary btn-block ml-2 mt-0">
{{'createAccount' | i18n}}
</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,93 @@
import {
Component,
OnInit,
} from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import {
Toast,
ToasterService,
} from 'angular2-toaster';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { StateService } from 'jslib/abstractions/state.service';
import { UserService } from 'jslib/abstractions/user.service';
import { EmergencyAccessAcceptRequest } from 'jslib/models/request/emergencyAccessAcceptRequest';
@Component({
selector: 'app-accept-emergency',
templateUrl: 'accept-emergency.component.html',
})
export class AcceptEmergencyComponent implements OnInit {
loading = true;
authed = false;
name: string;
email: string;
actionPromise: Promise<any>;
constructor(private router: Router, private toasterService: ToasterService,
private i18nService: I18nService, private route: ActivatedRoute,
private apiService: ApiService, private userService: UserService,
private stateService: StateService) { }
ngOnInit() {
let fired = false;
this.route.queryParams.subscribe(async qParams => {
if (fired) {
return;
}
fired = true;
await this.stateService.remove('emergencyInvitation');
let error = qParams.id == null || qParams.name == null || qParams.email == null || qParams.token == null;
let errorMessage: string = null;
if (!error) {
this.authed = await this.userService.isAuthenticated();
if (this.authed) {
const request = new EmergencyAccessAcceptRequest();
request.token = qParams.token;
try {
this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
await this.actionPromise;
const toast: Toast = {
type: 'success',
title: this.i18nService.t('inviteAccepted'),
body: this.i18nService.t('emergencyInviteAcceptedDesc'),
timeout: 10000,
};
this.toasterService.popAsync(toast);
this.router.navigate(['/vault']);
} catch (e) {
error = true;
errorMessage = e.message;
}
} else {
await this.stateService.save('emergencyInvitation', qParams);
this.email = qParams.email;
this.name = qParams.name;
if (this.name != null) {
// Fix URL encoding of space issue with Angular
this.name = this.name.replace(/\+/g, ' ');
}
}
}
if (error) {
const toast: Toast = {
type: 'error',
title: null,
body: errorMessage != null ? this.i18nService.t('emergencyInviteAcceptFailedShort', errorMessage) :
this.i18nService.t('emergencyInviteAcceptFailed'),
timeout: 10000,
};
this.toasterService.popAsync(toast);
this.router.navigate(['/']);
}
this.loading = false;
});
}
}

View File

@@ -37,7 +37,7 @@ export class AcceptOrganizationComponent implements OnInit {
ngOnInit() {
let fired = false;
this.route.queryParams.subscribe(async (qParams) => {
this.route.queryParams.subscribe(async qParams => {
if (fired) {
return;
}

View File

@@ -33,13 +33,6 @@ export class LockComponent extends BaseLockComponent {
async ngOnInit() {
await super.ngOnInit();
const authed = await this.userService.isAuthenticated();
if (!authed) {
this.router.navigate(['/']);
} else if (await this.cryptoService.hasKey()) {
this.router.navigate(['vault']);
}
this.onSuccessfulSubmit = () => {
const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl !== '/' && previousUrl.indexOf('lock') === -1) {

View File

@@ -34,7 +34,7 @@ export class LoginComponent extends BaseLoginComponent {
}
async ngOnInit() {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
this.email = qParams.email;
}
@@ -52,9 +52,12 @@ export class LoginComponent extends BaseLoginComponent {
}
async goAfterLogIn() {
const invite = await this.stateService.get<any>('orgInvitation');
if (invite != null) {
this.router.navigate(['accept-organization'], { queryParams: invite });
const orgInvite = await this.stateService.get<any>('orgInvitation');
const emergencyInvite = await this.stateService.get<any>('emergencyInvitation');
if (orgInvite != null) {
this.router.navigate(['accept-organization'], { queryParams: orgInvite });
} else if (emergencyInvite != null) {
this.router.navigate(['accept-emergency'], { queryParams: emergencyInvite });
} else {
const loginRedirect = await this.stateService.get<any>('loginRedirect');
if (loginRedirect != null) {

View File

@@ -2,7 +2,6 @@ import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -18,8 +17,7 @@ export class RecoverDeleteComponent {
formPromise: Promise<any>;
constructor(private router: Router, private apiService: ApiService,
private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService) {
private toasterService: ToasterService, private i18nService: I18nService) {
}
async submit() {
@@ -28,7 +26,6 @@ export class RecoverDeleteComponent {
request.email = this.email.trim().toLowerCase();
this.formPromise = this.apiService.postAccountRecoverDelete(request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Started Delete Recovery' });
this.toasterService.popAsync('success', null, this.i18nService.t('deleteRecoverEmailSent'));
this.router.navigate(['/']);
} catch { }

View File

@@ -2,7 +2,6 @@ import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service';
@@ -22,9 +21,8 @@ export class RecoverTwoFactorComponent {
formPromise: Promise<any>;
constructor(private router: Router, private apiService: ApiService,
private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService, private cryptoService: CryptoService,
private authService: AuthService) { }
private toasterService: ToasterService, private i18nService: I18nService,
private cryptoService: CryptoService, private authService: AuthService) { }
async submit() {
try {
@@ -35,7 +33,6 @@ export class RecoverTwoFactorComponent {
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
this.formPromise = this.apiService.postTwoFactorRecover(request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Recovered 2FA' });
this.toasterService.popAsync('success', null, this.i18nService.t('twoStepRecoverDisabled'));
this.router.navigate(['/']);
} catch { }

View File

@@ -25,8 +25,8 @@
</cite>
</figcaption>
<blockquote>
"Bitwarden has become a popular choice among open-source software advocates. After using it for a
few months, I can see why." - February 2020
"Bitwarden has become a popular choice among open-source software advocates. After using
it for a few months, I can see why." - February 2020
</blockquote>
</figure>
</div>
@@ -44,14 +44,15 @@
<p class="lead text-center mb-4" *ngIf="!layout">{{'createAccount' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<app-callout title="{{'createOrganizationStep1' | i18n}}" type="info" icon="fa-thumb-tack"
*ngIf="showCreateOrgMessage">
<app-callout title="{{'createOrganizationStep1' | i18n}}" type="info"
icon="fa-thumb-tack" *ngIf="showCreateOrgMessage">
{{'createOrganizationCreatePersonalAccount' | i18n}}
</app-callout>
<div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email"
required [appAutofocus]="email === ''" inputmode="email" appInputVerbatim="false">
required [appAutofocus]="email === ''" inputmode="email"
appInputVerbatim="false">
<small class="form-text text-muted">{{'emailAddressDesc' | i18n}}</small>
</div>
<div class="form-group">
@@ -120,6 +121,19 @@
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
</div>
<div class="form-group" *ngIf="showTerms">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="acceptPolicies"
[(ngModel)]="acceptPolicies" name="AcceptPolicies">
<label class="form-check-label small text-muted" for="acceptPolicies">
{{'acceptPolicies' | i18n}}<br>
<a href="https://bitwarden.com/terms/" target="_blank"
rel="noopener">{{'termsOfService' | i18n}}</a>,
<a href="https://bitwarden.com/privacy/" target="_blank"
rel="noopener">{{'privacyPolicy' | i18n}}</a>
</label>
</div>
</div>
<hr>
<div class="d-flex mb-2">
<button type="submit" class="btn btn-primary btn-block btn-submit"
@@ -132,13 +146,6 @@
{{'cancel' | i18n}}
</a>
</div>
<small class="text-muted" *ngIf="showTerms">
{{'submitAgreePolicies' | i18n}}
<a href="https://bitwarden.com/terms/" target="_blank"
rel="noopener">{{'termsOfService' | i18n}}</a>,
<a href="https://bitwarden.com/privacy/" target="_blank"
rel="noopener">{{'privacyPolicy' | i18n}}</a>
</small>
</div>
</div>
</div>

View File

@@ -27,7 +27,6 @@ import { ReferenceEventRequest } from 'jslib/models/request/referenceEventReques
})
export class RegisterComponent extends BaseRegisterComponent {
showCreateOrgMessage = false;
showTerms = true;
layout = '';
enforcedPolicyOptions: MasterPasswordPolicyOptions;
@@ -40,7 +39,6 @@ export class RegisterComponent extends BaseRegisterComponent {
passwordGenerationService: PasswordGenerationService, private policyService: PolicyService) {
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService,
passwordGenerationService);
this.showTerms = !platformUtilsService.isSelfHost();
}
getPasswordScoreAlertDisplay() {
@@ -64,7 +62,7 @@ export class RegisterComponent extends BaseRegisterComponent {
}
async ngOnInit() {
const queryParamsSub = this.route.queryParams.subscribe((qParams) => {
const queryParamsSub = this.route.queryParams.subscribe(qParams => {
this.referenceData = new ReferenceEventRequest();
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
this.email = qParams.email;
@@ -98,8 +96,8 @@ export class RegisterComponent extends BaseRegisterComponent {
const policies = await this.apiService.getPoliciesByToken(invite.organizationId, invite.token,
invite.email, invite.organizationUserId);
if (policies.data != null) {
const policiesData = policies.data.map((p) => new PolicyData(p));
this.policies = policiesData.map((p) => new Policy(p));
const policiesData = policies.data.map(p => new PolicyData(p));
this.policies = policiesData.map(p => new Policy(p));
}
} catch { }
}

View File

@@ -1,5 +1,8 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
@@ -24,8 +27,8 @@ export class SetPasswordComponent extends BaseSetPasswordComponent {
cryptoService: CryptoService, messagingService: MessagingService,
userService: UserService, passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService, policyService: PolicyService, router: Router,
syncService: SyncService) {
syncService: SyncService, route: ActivatedRoute) {
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
platformUtilsService, policyService, router, apiService, syncService);
platformUtilsService, policyService, router, apiService, syncService, route);
}
}

View File

@@ -36,7 +36,7 @@ export class SsoComponent extends BaseSsoComponent {
async ngOnInit() {
super.ngOnInit();
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
if (qParams.identifier != null) {
this.identifier = qParams.identifier;
} else {
@@ -53,6 +53,9 @@ export class SsoComponent extends BaseSsoComponent {
async submit() {
await this.storageService.save(IdentifierStorageKey, this.identifier);
if (this.clientId === 'browser') {
document.cookie = `ssoHandOffMessage=${this.i18nService.t('ssoHandOff')};SameSite=strict`;
}
super.submit();
}
}

View File

@@ -33,16 +33,10 @@
required appAutofocus appInputVerbatim autocomplete="new-password">
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.U2f">
<p class="text-center" *ngIf="!u2fReady">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</p>
<ng-container *ngIf="u2fReady">
<p class="text-center">{{'insertU2f' | i18n}}</p>
<img src="../../images/u2fkey.jpg" alt="" class="rounded img-fluid mb-3">
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
<div id="web-authn-frame" class="mb-3">
<iframe id="webauthn_iframe"></iframe>
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Duo ||
selectedProviderType === providerType.OrganizationDuo">
@@ -51,7 +45,7 @@
</div>
</ng-container>
<i class="fa fa-spinner text-muted fa-spin pull-right" title="{{'loading' | i18n}}"
*ngIf="form.loading && selectedProviderType === providerType.U2f" aria-hidden="true"></i>
*ngIf="form.loading && selectedProviderType === providerType.WebAuthn" aria-hidden="true"></i>
<div class="form-check" *ngIf="selectedProviderType != null">
<input id="remember" type="checkbox" name="Remember" class="form-check-input"
[(ngModel)]="remember">
@@ -65,7 +59,7 @@
<div class="d-flex mb-3">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading"
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.U2f">
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.WebAuthn">
<span>
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'continue' | i18n}}
</span>
@@ -84,4 +78,3 @@
</div>
</form>
<ng-template #twoFactorOptions></ng-template>
<iframe id="u2f_iframe" hidden></iframe>

View File

@@ -5,7 +5,10 @@ import {
ViewContainerRef,
} from '@angular/core';
import { Router } from '@angular/router';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { TwoFactorOptionsComponent } from './two-factor-options.component';
@@ -34,9 +37,9 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
i18nService: I18nService, apiService: ApiService,
platformUtilsService: PlatformUtilsService, stateService: StateService,
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver,
storageService: StorageService) {
storageService: StorageService, route: ActivatedRoute) {
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
stateService, storageService);
stateService, storageService, route);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}
@@ -57,16 +60,23 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
}
async goAfterLogIn() {
const invite = await this.stateService.get<any>('orgInvitation');
if (invite != null) {
this.router.navigate(['accept-organization'], { queryParams: invite });
const orgInvite = await this.stateService.get<any>('orgInvitation');
const emergencyInvite = await this.stateService.get<any>('emergencyInvitation');
if (orgInvite != null) {
this.router.navigate(['accept-organization'], { queryParams: orgInvite });
} else if (emergencyInvite != null) {
this.router.navigate(['accept-emergency'], { queryParams: emergencyInvite });
} else {
const loginRedirect = await this.stateService.get<any>('loginRedirect');
if (loginRedirect != null) {
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
await this.stateService.remove('loginRedirect');
} else {
this.router.navigate([this.successRoute]);
this.router.navigate([this.successRoute], {
queryParams: {
identifier: this.identifier,
},
});
}
}
}

View File

@@ -26,7 +26,7 @@ export class VerifyEmailTokenComponent implements OnInit {
ngOnInit() {
let fired = false;
this.route.queryParams.subscribe(async (qParams) => {
this.route.queryParams.subscribe(async qParams => {
if (fired) {
return;
}

View File

@@ -8,7 +8,6 @@ import {
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -27,13 +26,13 @@ export class VerifyRecoverDeleteComponent implements OnInit {
private token: string;
constructor(private router: Router, private apiService: ApiService,
private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService, private route: ActivatedRoute) {
private toasterService: ToasterService, private i18nService: I18nService,
private route: ActivatedRoute) {
}
ngOnInit() {
let fired = false;
this.route.queryParams.subscribe(async (qParams) => {
this.route.queryParams.subscribe(async qParams => {
if (fired) {
return;
}
@@ -53,7 +52,6 @@ export class VerifyRecoverDeleteComponent implements OnInit {
const request = new VerifyDeleteRecoverRequest(this.userId, this.token);
this.formPromise = this.apiService.postAccountRecoverDeleteToken(request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Recovered Delete' });
this.toasterService.popAsync('success', this.i18nService.t('accountDeleted'),
this.i18nService.t('accountDeletedDesc'));
this.router.navigate(['/']);

View File

@@ -8,6 +8,7 @@ import { FrontendLayoutComponent } from './layouts/frontend-layout.component';
import { OrganizationLayoutComponent } from './layouts/organization-layout.component';
import { UserLayoutComponent } from './layouts/user-layout.component';
import { AcceptEmergencyComponent } from './accounts/accept-emergency.component';
import { AcceptOrganizationComponent } from './accounts/accept-organization.component';
import { HintComponent } from './accounts/hint.component';
import { LockComponent } from './accounts/lock.component';
@@ -57,6 +58,9 @@ import {
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
import { AccessComponent } from './send/access.component';
import { SendComponent } from './send/send.component';
import { AccountComponent } from './settings/account.component';
import { CreateOrganizationComponent } from './settings/create-organization.component';
import { DomainRulesComponent } from './settings/domain-rules.component';
@@ -83,11 +87,15 @@ import { VaultComponent } from './vault/vault.component';
import { OrganizationGuardService } from './services/organization-guard.service';
import { OrganizationTypeGuardService } from './services/organization-type-guard.service';
import { UnauthGuardService } from './services/unauth-guard.service';
import { AuthGuardService } from 'jslib/angular/services/auth-guard.service';
import { LockGuardService } from 'jslib/angular/services/lock-guard.service';
import { UnauthGuardService } from 'jslib/angular/services/unauth-guard.service';
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
import { Permissions } from 'jslib/enums/permissions';
import { EmergencyAccessViewComponent } from './settings/emergency-access-view.component';
import { EmergencyAccessComponent } from './settings/emergency-access.component';
const routes: Routes = [
{
@@ -115,13 +123,22 @@ const routes: Routes = [
canActivate: [UnauthGuardService],
data: { titleId: 'passwordHint' },
},
{ path: 'lock', component: LockComponent },
{
path: 'lock',
component: LockComponent,
canActivate: [LockGuardService],
},
{ path: 'verify-email', component: VerifyEmailTokenComponent },
{
path: 'accept-organization',
component: AcceptOrganizationComponent,
data: { titleId: 'joinOrganization' },
},
{
path: 'accept-emergency',
component: AcceptEmergencyComponent,
data: { titleId: 'acceptEmergency' },
},
{ path: 'recover', pathMatch: 'full', redirectTo: 'recover-2fa' },
{
path: 'recover-2fa',
@@ -141,6 +158,11 @@ const routes: Routes = [
canActivate: [UnauthGuardService],
data: { titleId: 'deleteAccount' },
},
{
path: 'send/:sendId/:key',
component: AccessComponent,
data: { title: 'Bitwarden Send' },
},
],
},
{
@@ -149,6 +171,7 @@ const routes: Routes = [
canActivate: [AuthGuardService],
children: [
{ path: 'vault', component: VaultComponent, data: { titleId: 'myVault' } },
{ path: 'sends', component: SendComponent, data: { title: 'Send' } },
{
path: 'settings',
component: SettingsComponent,
@@ -171,6 +194,21 @@ const routes: Routes = [
component: CreateOrganizationComponent,
data: { titleId: 'newOrganization' },
},
{
path: 'emergency-access',
children: [
{
path: '',
component: EmergencyAccessComponent,
data: { titleId: 'emergencyAccess'},
},
{
path: ':id',
component: EmergencyAccessViewComponent,
data: { titleId: 'emergencyAccess'},
},
],
},
],
},
{
@@ -227,35 +265,75 @@ const routes: Routes = [
path: 'tools',
component: OrgToolsComponent,
canActivate: [OrganizationTypeGuardService],
data: { allowedTypes: [OrganizationUserType.Owner, OrganizationUserType.Admin] },
data: { permissions: [Permissions.AccessImportExport, Permissions.AccessReports] },
children: [
{ path: '', pathMatch: 'full', redirectTo: 'import' },
{ path: 'import', component: OrgImportComponent, data: { titleId: 'importData' } },
{ path: 'export', component: OrgExportComponent, data: { titleId: 'exportVault' } },
{
path: '',
pathMatch: 'full',
redirectTo: 'import',
},
{
path: 'import',
component: OrgImportComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'importData',
permissions: [Permissions.AccessImportExport],
},
},
{
path: 'export',
component: OrgExportComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'exportVault',
permissions: [Permissions.AccessImportExport],
},
},
{
path: 'exposed-passwords-report',
component: OrgExposedPasswordsReportComponent,
data: { titleId: 'exposedPasswordsReport' },
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'exposedPasswordsReport',
permissions: [Permissions.AccessReports],
},
},
{
path: 'inactive-two-factor-report',
component: OrgInactiveTwoFactorReportComponent,
data: { titleId: 'inactive2faReport' },
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'inactive2faReport',
permissions: [Permissions.AccessReports],
},
},
{
path: 'reused-passwords-report',
component: OrgReusedPasswordsReportComponent,
data: { titleId: 'reusedPasswordsReport' },
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'reusedPasswordsReport',
permissions: [Permissions.AccessReports],
},
},
{
path: 'unsecured-websites-report',
component: OrgUnsecuredWebsitesReportComponent,
data: { titleId: 'unsecuredWebsitesReport' },
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'unsecuredWebsitesReport',
permissions: [Permissions.AccessReports],
},
},
{
path: 'weak-passwords-report',
component: OrgWeakPasswordsReportComponent,
data: { titleId: 'weakPasswordsReport' },
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'weakPasswordsReport',
permissions: [Permissions.AccessReports],
},
},
],
},
@@ -264,26 +342,73 @@ const routes: Routes = [
component: OrgManageComponent,
canActivate: [OrganizationTypeGuardService],
data: {
allowedTypes: [
OrganizationUserType.Owner,
OrganizationUserType.Admin,
OrganizationUserType.Manager,
permissions: [
Permissions.ManageAssignedCollections,
Permissions.ManageAllCollections,
Permissions.AccessEventLogs,
Permissions.ManageGroups,
Permissions.ManageUsers,
Permissions.ManagePolicies,
],
},
children: [
{ path: '', pathMatch: 'full', redirectTo: 'people' },
{ path: 'collections', component: OrgManageCollectionsComponent, data: { titleId: 'collections' } },
{ path: 'events', component: OrgEventsComponent, data: { titleId: 'eventLogs' } },
{ path: 'groups', component: OrgGroupsComponent, data: { titleId: 'groups' } },
{ path: 'people', component: OrgPeopleComponent, data: { titleId: 'people' } },
{ path: 'policies', component: OrgPoliciesComponent, data: { titleId: 'policies' } },
{
path: '',
pathMatch: 'full',
redirectTo: 'people',
},
{
path: 'collections',
component: OrgManageCollectionsComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'collections',
permissions: [Permissions.ManageAssignedCollections, Permissions.ManageAllCollections],
},
},
{
path: 'events',
component: OrgEventsComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'eventLogs',
permissions: [Permissions.AccessEventLogs],
},
},
{
path: 'groups',
component: OrgGroupsComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'groups',
permissions: [Permissions.ManageGroups],
},
},
{
path: 'people',
component: OrgPeopleComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'people',
permissions: [Permissions.ManageUsers],
},
},
{
path: 'policies',
component: OrgPoliciesComponent,
canActivate: [OrganizationTypeGuardService],
data: {
titleId: 'policies',
permissions: [Permissions.ManagePolicies],
},
},
],
},
{
path: 'settings',
component: OrgSettingsComponent,
canActivate: [OrganizationTypeGuardService],
data: { allowedTypes: [OrganizationUserType.Owner] },
data: { permissions: [Permissions.ManageOrganization] },
children: [
{ path: '', pathMatch: 'full', redirectTo: 'account' },
{ path: 'account', component: OrgAccountComponent, data: { titleId: 'myOrganization' } },
@@ -308,6 +433,7 @@ const routes: Routes = [
@NgModule({
imports: [RouterModule.forRoot(routes, {
useHash: true,
paramsInheritanceStrategy: 'always',
/*enableTracing: true,*/
})],
exports: [RouterModule],

View File

@@ -1,15 +1,12 @@
import * as jq from 'jquery';
import Swal from 'sweetalert2/src/sweetalert2.js';
import Swal from 'sweetalert2/dist/sweetalert2.js';
import {
BodyOutputType,
Toast,
ToasterConfig,
ToasterContainerComponent,
ToasterService,
} from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import {
Component,
@@ -70,12 +67,12 @@ export class AppComponent implements OnDestroy, OnInit {
private idleTimer: number = null;
private isIdle = false;
constructor(private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
constructor(
private broadcasterService: BroadcasterService, private userService: UserService,
private tokenService: TokenService, private folderService: FolderService,
private settingsService: SettingsService, private syncService: SyncService,
private passwordGenerationService: PasswordGenerationService, private cipherService: CipherService,
private authService: AuthService, private router: Router, private analytics: Angulartics2,
private authService: AuthService, private router: Router,
private toasterService: ToasterService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private ngZone: NgZone,
private vaultTimeoutService: VaultTimeoutService, private storageService: StorageService,
@@ -139,15 +136,18 @@ export class AppComponent implements OnDestroy, OnInit {
this.router.navigate(['settings/premium']);
}
break;
case 'emailVerificationRequired':
const emailVerificationConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('emailVerificationRequiredDesc'),
this.i18nService.t('emailVerificationRequired'),
this.i18nService.t('learnMore'), this.i18nService.t('cancel'));
if (emailVerificationConfirmed) {
this.platformUtilsService.launchUri('https://bitwarden.com/help/article/create-bitwarden-account/');
}
break;
case 'showToast':
this.showToast(message);
break;
case 'analyticsEventTrack':
this.analytics.eventTrack.next({
action: message.action,
properties: { label: message.label },
});
break;
case 'setFullWidth':
this.setFullWidth();
break;
@@ -157,7 +157,7 @@ export class AppComponent implements OnDestroy, OnInit {
});
});
this.router.events.subscribe((event) => {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
const modals = Array.from(document.querySelectorAll('.modal'));
for (const modal of modals) {
@@ -198,7 +198,6 @@ export class AppComponent implements OnDestroy, OnInit {
this.searchService.clearIndex();
this.authService.logOut(async () => {
this.analytics.eventTrack.next({ action: 'Logged Out' });
if (expired) {
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
this.i18nService.t('loginExpired'));

View File

@@ -1,8 +1,6 @@
import 'core-js';
import { ToasterModule } from 'angular2-toaster';
import { Angulartics2Module } from 'angulartics2';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { AppRoutingModule } from './app-routing.module';
@@ -27,6 +25,7 @@ import { NavbarComponent } from './layouts/navbar.component';
import { OrganizationLayoutComponent } from './layouts/organization-layout.component';
import { UserLayoutComponent } from './layouts/user-layout.component';
import { AcceptEmergencyComponent } from './accounts/accept-emergency.component';
import { AcceptOrganizationComponent } from './accounts/accept-organization.component';
import { HintComponent } from './accounts/hint.component';
import { LockComponent } from './accounts/lock.component';
@@ -60,13 +59,11 @@ import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/m
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
import { AdjustSeatsComponent } from './organizations/settings/adjust-seats.component';
import { ApiKeyComponent as OrgApiKeyComponent } from './organizations/settings/api-key.component';
import { ChangePlanComponent } from './organizations/settings/change-plan.component';
import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component';
import { DownloadLicenseComponent } from './organizations/settings/download-license.component';
import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component';
import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component';
import { RotateApiKeyComponent as OrgRotateApiKeyComponent } from './organizations/settings/rotate-api-key.component';
import { SettingsComponent as OrgSettingComponent } from './organizations/settings/settings.component';
import {
TwoFactorSetupComponent as OrgTwoFactorSetupComponent,
@@ -98,10 +95,15 @@ import { CollectionsComponent as OrgCollectionsComponent } from './organizations
import { GroupingsComponent as OrgGroupingsComponent } from './organizations/vault/groupings.component';
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
import { AccessComponent } from './send/access.component';
import { AddEditComponent as SendAddEditComponent } from './send/add-edit.component';
import { SendComponent } from './send/send.component';
import { AccountComponent } from './settings/account.component';
import { AddCreditComponent } from './settings/add-credit.component';
import { AdjustPaymentComponent } from './settings/adjust-payment.component';
import { AdjustStorageComponent } from './settings/adjust-storage.component';
import { ApiKeyComponent } from './settings/api-key.component';
import { ChangeEmailComponent } from './settings/change-email.component';
import { ChangeKdfComponent } from './settings/change-kdf.component';
import { ChangePasswordComponent } from './settings/change-password.component';
@@ -109,6 +111,13 @@ import { CreateOrganizationComponent } from './settings/create-organization.comp
import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.component';
import { DeleteAccountComponent } from './settings/delete-account.component';
import { DomainRulesComponent } from './settings/domain-rules.component';
import { EmergencyAccessAddEditComponent } from './settings/emergency-access-add-edit.component';
import { EmergencyAccessAttachmentsComponent } from './settings/emergency-access-attachments.component';
import { EmergencyAccessConfirmComponent } from './settings/emergency-access-confirm.component';
import { EmergencyAccessTakeoverComponent } from './settings/emergency-access-takeover.component';
import { EmergencyAccessViewComponent } from './settings/emergency-access-view.component';
import { EmergencyAccessComponent } from './settings/emergency-access.component';
import { EmergencyAddEditComponent } from './settings/emergency-add-edit.component';
import { LinkSsoComponent } from './settings/link-sso.component';
import { OptionsComponent } from './settings/options.component';
import { OrganizationPlansComponent } from './settings/organization-plans.component';
@@ -124,8 +133,8 @@ import { TwoFactorDuoComponent } from './settings/two-factor-duo.component';
import { TwoFactorEmailComponent } from './settings/two-factor-email.component';
import { TwoFactorRecoveryComponent } from './settings/two-factor-recovery.component';
import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
import { TwoFactorU2fComponent } from './settings/two-factor-u2f.component';
import { TwoFactorVerifyComponent } from './settings/two-factor-verify.component';
import { TwoFactorWebAuthnComponent } from './settings/two-factor-webauthn.component';
import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.component';
import { UpdateKeyComponent } from './settings/update-key.component';
import { UpdateLicenseComponent } from './settings/update-license.component';
@@ -156,6 +165,7 @@ import { CiphersComponent } from './vault/ciphers.component';
import { CollectionsComponent } from './vault/collections.component';
import { FolderAddEditComponent } from './vault/folder-add-edit.component';
import { GroupingsComponent } from './vault/groupings.component';
import { SendInfoComponent } from './vault/send-info.component';
import { ShareComponent } from './vault/share.component';
import { VaultComponent } from './vault/vault.component';
@@ -179,7 +189,10 @@ import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
import { registerLocaleData } from '@angular/common';
import {
DatePipe,
registerLocaleData,
} from '@angular/common';
import localeCa from '@angular/common/locales/ca';
import localeCs from '@angular/common/locales/cs';
import localeDa from '@angular/common/locales/da';
@@ -241,17 +254,14 @@ registerLocaleData(localeZhTw, 'zh-TW');
FormsModule,
AppRoutingModule,
ServicesModule,
Angulartics2Module.forRoot({
pageTracking: {
clearQueryParams: true,
},
}),
ToasterModule.forRoot(),
InfiniteScrollModule,
DragDropModule,
],
declarations: [
A11yTitleDirective,
AcceptEmergencyComponent,
AccessComponent,
AcceptOrganizationComponent,
AccountComponent,
SetPasswordComponent,
@@ -261,6 +271,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
AdjustSeatsComponent,
AdjustStorageComponent,
ApiActionDirective,
ApiKeyComponent,
AppComponent,
AttachmentsComponent,
AutofocusDirective,
@@ -287,6 +298,13 @@ registerLocaleData(localeZhTw, 'zh-TW');
DeleteOrganizationComponent,
DomainRulesComponent,
DownloadLicenseComponent,
EmergencyAccessAddEditComponent,
EmergencyAccessAttachmentsComponent,
EmergencyAccessComponent,
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
EmergencyAccessViewComponent,
EmergencyAddEditComponent,
ExportComponent,
ExposedPasswordsReportComponent,
FallbackSrcDirective,
@@ -308,7 +326,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
OptionsComponent,
OrgAccountComponent,
OrgAddEditComponent,
OrgApiKeyComponent,
OrganizationBillingComponent,
OrganizationPlansComponent,
OrganizationSubscriptionComponent,
@@ -332,7 +349,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgPolicyEditComponent,
OrgPoliciesComponent,
OrgReusedPasswordsReportComponent,
OrgRotateApiKeyComponent,
OrgSettingComponent,
OrgToolsComponent,
OrgTwoFactorSetupComponent,
@@ -358,6 +374,9 @@ registerLocaleData(localeZhTw, 'zh-TW');
SearchCiphersPipe,
SearchPipe,
SelectCopyDirective,
SendAddEditComponent,
SendComponent,
SendInfoComponent,
SettingsComponent,
ShareComponent,
SsoComponent,
@@ -373,8 +392,8 @@ registerLocaleData(localeZhTw, 'zh-TW');
TwoFactorOptionsComponent,
TwoFactorRecoveryComponent,
TwoFactorSetupComponent,
TwoFactorU2fComponent,
TwoFactorVerifyComponent,
TwoFactorWebAuthnComponent,
TwoFactorYubiKeyComponent,
UnsecuredWebsitesReportComponent,
UpdateKeyComponent,
@@ -390,6 +409,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
],
entryComponents: [
AddEditComponent,
ApiKeyComponent,
AttachmentsComponent,
BulkActionsComponent,
BulkDeleteComponent,
@@ -400,10 +420,14 @@ registerLocaleData(localeZhTw, 'zh-TW');
DeauthorizeSessionsComponent,
DeleteAccountComponent,
DeleteOrganizationComponent,
EmergencyAccessAddEditComponent,
EmergencyAccessAttachmentsComponent,
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
EmergencyAddEditComponent,
FolderAddEditComponent,
ModalComponent,
OrgAddEditComponent,
OrgApiKeyComponent,
OrgAttachmentsComponent,
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
@@ -411,23 +435,23 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgEntityUsersComponent,
OrgGroupAddEditComponent,
OrgPolicyEditComponent,
OrgRotateApiKeyComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
OrgUserGroupsComponent,
PasswordGeneratorHistoryComponent,
PurgeVaultComponent,
SendAddEditComponent,
ShareComponent,
TwoFactorAuthenticatorComponent,
TwoFactorDuoComponent,
TwoFactorEmailComponent,
TwoFactorOptionsComponent,
TwoFactorRecoveryComponent,
TwoFactorU2fComponent,
TwoFactorWebAuthnComponent,
TwoFactorYubiKeyComponent,
UpdateKeyComponent,
],
providers: [],
providers: [DatePipe],
bootstrap: [AppComponent],
})
export class AppModule { }

View File

@@ -15,8 +15,8 @@ export class FooterComponent implements OnInit {
constructor(private platformUtilsService: PlatformUtilsService) { }
ngOnInit() {
async ngOnInit() {
this.year = new Date().getFullYear().toString();
this.version = this.platformUtilsService.getApplicationVersion();
this.version = await this.platformUtilsService.getApplicationVersion();
}
}

View File

@@ -16,9 +16,9 @@ export class FrontendLayoutComponent implements OnInit, OnDestroy {
constructor(private platformUtilsService: PlatformUtilsService) { }
ngOnInit() {
async ngOnInit() {
this.year = new Date().getFullYear().toString();
this.version = this.platformUtilsService.getApplicationVersion();
this.version = await this.platformUtilsService.getApplicationVersion();
document.body.classList.add('layout_frontend');
}

View File

@@ -8,6 +8,9 @@
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/vault">{{'myVault' | i18n}}</a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/sends">{{'send' | i18n}}</a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/tools">{{'tools' | i18n}}</a>
</li>

View File

@@ -15,21 +15,21 @@
</div>
</div>
</div>
<ul class="nav nav-tabs" *ngIf="organization.isManager">
<ul class="nav nav-tabs" *ngIf="showMenuBar">
<li class="nav-item">
<a class="nav-link" routerLink="vault" routerLinkActive="active">
<i class="fa fa-lock" aria-hidden="true"></i>
{{'vault' | i18n}}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="manage" routerLinkActive="active">
<li class="nav-item" *ngIf="showManageTab">
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
<i class="fa fa-sliders" aria-hidden="true"></i>
{{'manage' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="organization.isAdmin">
<a class="nav-link" routerLink="tools" routerLinkActive="active">
<li class="nav-item" *ngIf="showToolsTab">
<a class="nav-link" [routerLink]="toolsRoute" routerLinkActive="active">
<i class="fa fa-wrench" aria-hidden="true"></i>
{{'tools' | i18n}}
</a>
@@ -43,10 +43,10 @@
</ul>
</div>
<div class="ml-auto d-flex align-items-center">
<button class="btn btn-primary" (click)="goToEnterprisePortal()" #enterpriseBtn
[appApiAction]="enterpriseTokenPromise" *ngIf="organization.useBusinessPortal">
<i class="fa fa-bank fa-fw" [hidden]="enterpriseBtn.loading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin fa-fw" [hidden]="!enterpriseBtn.loading" title="{{'loading' | i18n}}"
<button class="btn btn-primary" (click)="goToBusinessPortal()" #businessBtn
[appApiAction]="businessTokenPromise" *ngIf="showBusinessPortalButton">
<i class="fa fa-bank fa-fw" [hidden]="businessBtn.loading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin fa-fw" [hidden]="!businessBtn.loading" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
{{'businessPortal' | i18n}} →
</button>

View File

@@ -24,9 +24,9 @@ const BroadcasterSubscriptionId = 'OrganizationLayoutComponent';
})
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
organization: Organization;
enterpriseTokenPromise: Promise<any>;
businessTokenPromise: Promise<any>;
private organizationId: string;
private enterpriseUrl: string;
private businessUrl: string;
constructor(private route: ActivatedRoute, private userService: UserService,
private broadcasterService: BroadcasterService, private ngZone: NgZone,
@@ -34,15 +34,15 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
private environmentService: EnvironmentService) { }
ngOnInit() {
this.enterpriseUrl = 'https://portal.bitwarden.com';
this.businessUrl = 'https://portal.bitwarden.com';
if (this.environmentService.enterpriseUrl != null) {
this.enterpriseUrl = this.environmentService.enterpriseUrl;
this.businessUrl = this.environmentService.enterpriseUrl;
} else if (this.environmentService.baseUrl != null) {
this.enterpriseUrl = this.environmentService.baseUrl + '/portal';
this.businessUrl = this.environmentService.baseUrl + '/portal';
}
document.body.classList.remove('layout_frontend');
this.route.params.subscribe(async (params) => {
this.route.params.subscribe(async params => {
this.organizationId = params.organizationId;
await this.load();
});
@@ -65,19 +65,68 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
this.organization = await this.userService.getOrganization(this.organizationId);
}
async goToEnterprisePortal() {
if (this.enterpriseTokenPromise != null) {
async goToBusinessPortal() {
if (this.businessTokenPromise != null) {
return;
}
try {
this.enterpriseTokenPromise = this.apiService.getEnterprisePortalSignInToken();
const token = await this.enterpriseTokenPromise;
this.businessTokenPromise = this.apiService.getEnterprisePortalSignInToken();
const token = await this.businessTokenPromise;
if (token != null) {
const userId = await this.userService.getUserId();
this.platformUtilsService.launchUri(this.enterpriseUrl + '/login?userId=' + userId +
this.platformUtilsService.launchUri(this.businessUrl + '/login?userId=' + userId +
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organization.id);
}
} catch { }
this.enterpriseTokenPromise = null;
this.businessTokenPromise = null;
}
get showMenuBar() {
return this.showManageTab || this.showToolsTab || this.organization.isOwner;
}
get showManageTab(): boolean {
return this.organization.canManageUsers ||
this.organization.canManageAssignedCollections ||
this.organization.canManageAllCollections ||
this.organization.canManageGroups ||
this.organization.canManagePolicies ||
this.organization.canAccessEventLogs;
}
get showToolsTab(): boolean {
return this.organization.canAccessImportExport || this.organization.canAccessReports;
}
get showBusinessPortalButton(): boolean {
return this.organization.useBusinessPortal && this.organization.canAccessBusinessPortal;
}
get toolsRoute(): string {
return this.organization.canAccessImportExport ?
'tools/import' :
'tools/exposed-passwords-report';
}
get manageRoute(): string {
let route: string;
switch (true) {
case this.organization.canManageUsers:
route = 'manage/people';
break;
case this.organization.canManageAssignedCollections || this.organization.canManageAllCollections:
route = 'manage/collections';
break;
case this.organization.canManageGroups:
route = 'manage/groups';
break;
case this.organization.canManagePolicies:
route = 'manage/policies';
break;
case this.organization.canAccessEventLogs:
route = 'manage/events';
break;
}
return route;
}
}

View File

@@ -7,7 +7,6 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
@@ -15,7 +14,7 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CipherString } from 'jslib/models/domain/cipherString';
import { EncString } from 'jslib/models/domain/encString';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
import { CollectionRequest } from 'jslib/models/request/collectionRequest';
import { SelectionReadOnlyRequest } from 'jslib/models/request/selectionReadOnlyRequest';
@@ -46,9 +45,8 @@ export class CollectionAddEditComponent implements OnInit {
private orgKey: SymmetricCryptoKey;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
private userService: UserService) { }
private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService, private userService: UserService) { }
async ngOnInit() {
const organization = await this.userService.getOrganization(this.organizationId);
@@ -56,7 +54,7 @@ export class CollectionAddEditComponent implements OnInit {
this.editMode = this.loading = this.collectionId != null;
if (this.accessGroups) {
const groupsResponse = await this.apiService.getGroups(this.organizationId);
this.groups = groupsResponse.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, 'name'));
this.groups = groupsResponse.data.map(r => r).sort(Utils.getSortFunction(this.i18nService, 'name'));
}
this.orgKey = await this.cryptoService.getOrgKey(this.organizationId);
@@ -65,11 +63,11 @@ export class CollectionAddEditComponent implements OnInit {
this.title = this.i18nService.t('editCollection');
try {
const collection = await this.apiService.getCollectionDetails(this.organizationId, this.collectionId);
this.name = await this.cryptoService.decryptToUtf8(new CipherString(collection.name), this.orgKey);
this.name = await this.cryptoService.decryptToUtf8(new EncString(collection.name), this.orgKey);
this.externalId = collection.externalId;
if (collection.groups != null && this.groups.length > 0) {
collection.groups.forEach((s) => {
const group = this.groups.filter((g) => !g.accessAll && g.id === s.id);
collection.groups.forEach(s => {
const group = this.groups.filter(g => !g.accessAll && g.id === s.id);
if (group != null && group.length > 0) {
(group[0] as any).checked = true;
(group[0] as any).readOnly = s.readOnly;
@@ -82,7 +80,7 @@ export class CollectionAddEditComponent implements OnInit {
this.title = this.i18nService.t('addCollection');
}
this.groups.forEach((g) => {
this.groups.forEach(g => {
if (g.accessAll) {
(g as any).checked = true;
}
@@ -103,7 +101,7 @@ export class CollectionAddEditComponent implements OnInit {
}
selectAll(select: boolean) {
this.groups.forEach((g) => this.check(g, select));
this.groups.forEach(g => this.check(g, select));
}
async submit() {
@@ -114,8 +112,8 @@ export class CollectionAddEditComponent implements OnInit {
const request = new CollectionRequest();
request.name = (await this.cryptoService.encrypt(this.name, this.orgKey)).encryptedString;
request.externalId = this.externalId;
request.groups = this.groups.filter((g) => (g as any).checked && !g.accessAll)
.map((g) => new SelectionReadOnlyRequest(g.id, !!(g as any).readOnly, !!(g as any).hidePasswords));
request.groups = this.groups.filter(g => (g as any).checked && !g.accessAll)
.map(g => new SelectionReadOnlyRequest(g.id, !!(g as any).readOnly, !!(g as any).hidePasswords));
try {
if (this.editMode) {
@@ -124,7 +122,6 @@ export class CollectionAddEditComponent implements OnInit {
this.formPromise = this.apiService.postCollection(this.organizationId, request);
}
await this.formPromise;
this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Collection' : 'Created Collection' });
this.toasterService.popAsync('success', null,
this.i18nService.t(this.editMode ? 'editedCollectionId' : 'createdCollectionId', this.name));
this.onSavedCollection.emit();
@@ -146,7 +143,6 @@ export class CollectionAddEditComponent implements OnInit {
try {
this.deletePromise = this.apiService.deleteCollection(this.organizationId, this.collectionId);
await this.deletePromise;
this.analytics.eventTrack.next({ action: 'Deleted Collection' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedCollectionId', this.name));
this.onDeletedCollection.emit();
} catch { }

View File

@@ -8,7 +8,6 @@ import {
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
@@ -52,15 +51,15 @@ export class CollectionsComponent implements OnInit {
constructor(private apiService: ApiService, private route: ActivatedRoute,
private collectionService: CollectionService, private componentFactoryResolver: ComponentFactoryResolver,
private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
private userService: UserService, private searchService: SearchService) { }
private toasterService: ToasterService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private userService: UserService,
private searchService: SearchService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
await this.load();
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
this.searchText = qParams.search;
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
@@ -72,12 +71,12 @@ export class CollectionsComponent implements OnInit {
async load() {
const organization = await this.userService.getOrganization(this.organizationId);
let response: ListResponse<CollectionResponse>;
if (organization.isAdmin) {
if (organization.canManageAllCollections) {
response = await this.apiService.getCollections(this.organizationId);
} else {
response = await this.apiService.getUserCollections();
}
const collections = response.data.filter((c) => c.organizationId === this.organizationId).map((r) =>
const collections = response.data.filter(c => c.organizationId === this.organizationId).map(r =>
new Collection(new CollectionData(r as CollectionDetailsResponse)));
this.collections = await this.collectionService.decryptMany(collections);
this.resetPaging();
@@ -141,7 +140,6 @@ export class CollectionsComponent implements OnInit {
try {
await this.apiService.deleteCollection(this.organizationId, collection.id);
this.analytics.eventTrack.next({ action: 'Deleted Collection' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedCollectionId', collection.name));
this.removeCollection(collection);
} catch { }

View File

@@ -50,7 +50,7 @@ export class EntityEventsComponent implements OnInit {
async load() {
if (this.showUser) {
const response = await this.apiService.getOrganizationUsers(this.organizationId);
response.data.forEach((u) => {
response.data.forEach(u => {
const name = u.name == null || u.name.trim() === '' ? u.email : u.name;
this.orgUsersIdMap.set(u.id, { name: name, email: u.email });
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });
@@ -94,7 +94,7 @@ export class EntityEventsComponent implements OnInit {
} catch { }
this.continuationToken = response.continuationToken;
const events = response.data.map((r) => {
const events = response.data.map(r => {
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = this.eventService.getEventInfo(r);
const user = this.showUser && userId != null && this.orgUsersUserIdMap.has(userId) ?

View File

@@ -86,6 +86,7 @@
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Custom">{{'custom' | i18n}}</span>
</td>
<td class="text-center" *ngIf="entity === 'collection'">
<input type="checkbox" [(ngModel)]="u.hidePasswords"

View File

@@ -7,7 +7,6 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -42,7 +41,7 @@ export class EntityUsersComponent implements OnInit {
private allUsers: OrganizationUserUserDetailsResponse[] = [];
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService) { }
private toasterService: ToasterService) { }
async ngOnInit() {
await this.loadUsers();
@@ -51,7 +50,7 @@ export class EntityUsersComponent implements OnInit {
get users() {
if (this.showSelected) {
return this.allUsers.filter((u) => (u as any).checked);
return this.allUsers.filter(u => (u as any).checked);
} else {
return this.allUsers;
}
@@ -59,12 +58,12 @@ export class EntityUsersComponent implements OnInit {
async loadUsers() {
const users = await this.apiService.getOrganizationUsers(this.organizationId);
this.allUsers = users.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, 'email'));
this.allUsers = users.data.map(r => r).sort(Utils.getSortFunction(this.i18nService, 'email'));
if (this.entity === 'group') {
const response = await this.apiService.getGroupUsers(this.organizationId, this.entityId);
if (response != null && users.data.length > 0) {
response.forEach((s) => {
const user = users.data.filter((u) => u.id === s);
response.forEach(s => {
const user = users.data.filter(u => u.id === s);
if (user != null && user.length > 0) {
(user[0] as any).checked = true;
}
@@ -73,8 +72,8 @@ export class EntityUsersComponent implements OnInit {
} else if (this.entity === 'collection') {
const response = await this.apiService.getCollectionUsers(this.organizationId, this.entityId);
if (response != null && users.data.length > 0) {
response.forEach((s) => {
const user = users.data.filter((u) => !u.accessAll && u.id === s.id);
response.forEach(s => {
const user = users.data.filter(u => !u.accessAll && u.id === s.id);
if (user != null && user.length > 0) {
(user[0] as any).checked = true;
(user[0] as any).readOnly = s.readOnly;
@@ -84,7 +83,7 @@ export class EntityUsersComponent implements OnInit {
}
}
this.allUsers.forEach((u) => {
this.allUsers.forEach(u => {
if (this.entity === 'collection' && u.accessAll) {
(u as any).checked = true;
}
@@ -121,17 +120,14 @@ export class EntityUsersComponent implements OnInit {
async submit() {
try {
if (this.entity === 'group') {
const selections = this.users.filter((u) => (u as any).checked).map((u) => u.id);
const selections = this.users.filter(u => (u as any).checked).map(u => u.id);
this.formPromise = this.apiService.putGroupUsers(this.organizationId, this.entityId, selections);
} else {
const selections = this.users.filter((u) => (u as any).checked && !u.accessAll)
.map((u) => new SelectionReadOnlyRequest(u.id, !!(u as any).readOnly, !!(u as any).hidePasswords));
const selections = this.users.filter(u => (u as any).checked && !u.accessAll)
.map(u => new SelectionReadOnlyRequest(u.id, !!(u as any).readOnly, !!(u as any).hidePasswords));
this.formPromise = this.apiService.putCollectionUsers(this.organizationId, this.entityId, selections);
}
await this.formPromise;
this.analytics.eventTrack.next({
action: this.entity === 'group' ? 'Edited Group Users' : 'Edited Collection Users',
});
this.toasterService.popAsync('success', null, this.i18nService.t('updatedUsers'));
this.onEditedUsers.emit();
} catch { }

View File

@@ -39,7 +39,7 @@ export class EventsComponent implements OnInit {
private router: Router) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (organization == null || !organization.useEvents) {
@@ -55,7 +55,7 @@ export class EventsComponent implements OnInit {
async load() {
const response = await this.apiService.getOrganizationUsers(this.organizationId);
response.data.forEach((u) => {
response.data.forEach(u => {
const name = u.name == null || u.name.trim() === '' ? u.email : u.name;
this.orgUsersIdMap.set(u.id, { name: name, email: u.email });
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });
@@ -92,7 +92,7 @@ export class EventsComponent implements OnInit {
} catch { }
this.continuationToken = response.continuationToken;
const events = response.data.map((r) => {
const events = response.data.map(r => {
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = this.eventService.getEventInfo(r);
const user = userId != null && this.orgUsersUserIdMap.has(userId) ?

View File

@@ -7,7 +7,6 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
@@ -42,8 +41,8 @@ export class GroupAddEditComponent implements OnInit {
deletePromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService) { }
private toasterService: ToasterService, private collectionService: CollectionService,
private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
this.editMode = this.loading = this.groupId != null;
@@ -58,8 +57,8 @@ export class GroupAddEditComponent implements OnInit {
this.name = group.name;
this.externalId = group.externalId;
if (group.collections != null && this.collections != null) {
group.collections.forEach((s) => {
const collection = this.collections.filter((c) => c.id === s.id);
group.collections.forEach(s => {
const collection = this.collections.filter(c => c.id === s.id);
if (collection != null && collection.length > 0) {
(collection[0] as any).checked = true;
collection[0].readOnly = s.readOnly;
@@ -77,7 +76,7 @@ export class GroupAddEditComponent implements OnInit {
async loadCollections() {
const response = await this.apiService.getCollections(this.organizationId);
const collections = response.data.map((r) =>
const collections = response.data.map(r =>
new Collection(new CollectionData(r as CollectionDetailsResponse)));
this.collections = await this.collectionService.decryptMany(collections);
}
@@ -90,7 +89,7 @@ export class GroupAddEditComponent implements OnInit {
}
selectAll(select: boolean) {
this.collections.forEach((c) => this.check(c, select));
this.collections.forEach(c => this.check(c, select));
}
async submit() {
@@ -99,8 +98,8 @@ export class GroupAddEditComponent implements OnInit {
request.externalId = this.externalId;
request.accessAll = this.access === 'all';
if (!request.accessAll) {
request.collections = this.collections.filter((c) => (c as any).checked)
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
request.collections = this.collections.filter(c => (c as any).checked)
.map(c => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
}
try {
@@ -110,7 +109,6 @@ export class GroupAddEditComponent implements OnInit {
this.formPromise = this.apiService.postGroup(this.organizationId, request);
}
await this.formPromise;
this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Group' : 'Created Group' });
this.toasterService.popAsync('success', null,
this.i18nService.t(this.editMode ? 'editedGroupId' : 'createdGroupId', this.name));
this.onSavedGroup.emit();
@@ -132,7 +130,6 @@ export class GroupAddEditComponent implements OnInit {
try {
this.deletePromise = this.apiService.deleteGroup(this.organizationId, this.groupId);
await this.deletePromise;
this.analytics.eventTrack.next({ action: 'Deleted Group' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedGroupId', this.name));
this.onDeletedGroup.emit();
} catch { }

View File

@@ -11,7 +11,6 @@ import {
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -49,12 +48,12 @@ export class GroupsComponent implements OnInit {
constructor(private apiService: ApiService, private route: ActivatedRoute,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private analytics: Angulartics2, private toasterService: ToasterService,
private platformUtilsService: PlatformUtilsService, private userService: UserService,
private router: Router, private searchService: SearchService) { }
private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService,
private userService: UserService, private router: Router,
private searchService: SearchService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (organization == null || !organization.useGroups) {
@@ -62,7 +61,7 @@ export class GroupsComponent implements OnInit {
return;
}
await this.load();
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
this.searchText = qParams.search;
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
@@ -136,7 +135,6 @@ export class GroupsComponent implements OnInit {
try {
await this.apiService.deleteGroup(this.organizationId, group.id);
this.analytics.eventTrack.next({ action: 'Deleted Group' });
this.toasterService.popAsync('success', null, this.i18nService.t('deletedGroupId', group.name));
this.removeGroup(group);
} catch { }

View File

@@ -5,22 +5,23 @@
<div class="card-header">{{'manage' | i18n}}</div>
<div class="list-group list-group-flush">
<a routerLink="people" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin">
*ngIf="organization.canManageUsers">
{{'people' | i18n}}
</a>
<a routerLink="collections" class="list-group-item" routerLinkActive="active">
<a routerLink="collections" class="list-group-item" routerLinkActive="active"
*ngIf="organization.canManageAssignedCollections || organization.canManageAllCollections">
{{'collections' | i18n}}
</a>
<a routerLink="groups" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin && accessGroups">
*ngIf="organization.canManageGroups && accessGroups">
{{'groups' | i18n}}
</a>
<a routerLink="policies" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin && accessPolicies">
*ngIf="organization.canManagePolicies && accessPolicies">
{{'policies' | i18n}}
</a>
<a routerLink="events" class="list-group-item" routerLinkActive="active"
*ngIf="organization.isAdmin && accessEvents">
*ngIf="organization.canAccessEventLogs && accessEvents">
{{'eventLogs' | i18n}}
</a>
</div>

View File

@@ -21,7 +21,7 @@ export class ManageComponent implements OnInit {
constructor(private route: ActivatedRoute, private userService: UserService) { }
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
this.route.parent.params.subscribe(async params => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.accessPolicies = this.organization.usePolicies;
this.accessEvents = this.organization.useEvents;

View File

@@ -69,6 +69,7 @@
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Custom">{{'custom' | i18n}}</span>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>

View File

@@ -11,8 +11,8 @@ import {
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ValidationService } from 'jslib/angular/services/validation.service';
import { ConstantsService } from 'jslib/services/constants.service';
import { ApiService } from 'jslib/abstractions/api.service';
@@ -70,16 +70,16 @@ export class PeopleComponent implements OnInit {
constructor(private apiService: ApiService, private route: ActivatedRoute,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private platformUtilsService: PlatformUtilsService, private analytics: Angulartics2,
private toasterService: ToasterService, private cryptoService: CryptoService,
private userService: UserService, private router: Router,
private storageService: StorageService, private searchService: SearchService) { }
private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService,
private cryptoService: CryptoService, private userService: UserService, private router: Router,
private storageService: StorageService, private searchService: SearchService,
private validationService: ValidationService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (!organization.isAdmin) {
if (!organization.canManageUsers) {
this.router.navigate(['../collections'], { relativeTo: this.route });
return;
}
@@ -87,10 +87,10 @@ export class PeopleComponent implements OnInit {
this.accessGroups = organization.useGroups;
await this.load();
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
this.searchText = qParams.search;
if (qParams.viewEvents != null) {
const user = this.users.filter((u) => u.id === qParams.viewEvents);
const user = this.users.filter(u => u.id === qParams.viewEvents);
if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) {
this.events(user[0]);
}
@@ -107,7 +107,7 @@ export class PeopleComponent implements OnInit {
this.statusMap.clear();
this.allUsers = response.data != null && response.data.length > 0 ? response.data : [];
this.allUsers.sort(Utils.getSortFunction(this.i18nService, 'email'));
this.allUsers.forEach((u) => {
this.allUsers.forEach(u => {
if (!this.statusMap.has(u.status)) {
this.statusMap.set(u.status, [u]);
} else {
@@ -231,7 +231,6 @@ export class PeopleComponent implements OnInit {
try {
await this.apiService.deleteOrganizationUser(this.organizationId, user.id);
this.analytics.eventTrack.next({ action: 'Deleted User' });
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', user.name || user.email));
this.removeUser(user);
} catch { }
@@ -243,7 +242,6 @@ export class PeopleComponent implements OnInit {
}
this.actionPromise = this.apiService.postOrganizationUserReinvite(this.organizationId, user.id);
await this.actionPromise;
this.analytics.eventTrack.next({ action: 'Reinvited User' });
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenReinvited', user.name || user.email));
this.actionPromise = null;
}
@@ -258,6 +256,20 @@ export class PeopleComponent implements OnInit {
}
}
const confirmUser = async (publicKey: Uint8Array) => {
try {
this.actionPromise = this.doConfirmation(user, publicKey);
await this.actionPromise;
updateUser(this);
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', user.name || user.email));
} catch (e) {
this.validationService.showError(e);
throw e;
} finally {
this.actionPromise = null;
}
};
if (this.actionPromise != null) {
return;
}
@@ -277,9 +289,14 @@ export class PeopleComponent implements OnInit {
childComponent.organizationId = this.organizationId;
childComponent.organizationUserId = user != null ? user.id : null;
childComponent.userId = user != null ? user.userId : null;
childComponent.onConfirmedUser.subscribe(() => {
this.modal.close();
updateUser(this);
childComponent.onConfirmedUser.subscribe(async (publicKey: Uint8Array) => {
try {
await confirmUser(publicKey);
this.modal.close();
} catch (e) {
// tslint:disable-next-line
console.error('Handled exception:', e);
}
});
this.modal.onClosed.subscribe(() => {
@@ -288,12 +305,19 @@ export class PeopleComponent implements OnInit {
return;
}
this.actionPromise = this.doConfirmation(user);
await this.actionPromise;
updateUser(this);
this.analytics.eventTrack.next({ action: 'Confirmed User' });
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', user.name || user.email));
this.actionPromise = null;
try {
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
try {
// tslint:disable-next-line
console.log('User\'s fingerprint: ' +
(await this.cryptoService.getFingerprint(user.userId, publicKey.buffer)).join('-'));
} catch { }
await confirmUser(publicKey);
} catch (e) {
// tslint:disable-next-line
console.error('Handled exception:', e);
}
}
async events(user: OrganizationUserUserDetailsResponse) {
@@ -334,15 +358,8 @@ export class PeopleComponent implements OnInit {
return !searching && this.users && this.users.length > this.pageSize;
}
private async doConfirmation(user: OrganizationUserUserDetailsResponse) {
private async doConfirmation(user: OrganizationUserUserDetailsResponse, publicKey: Uint8Array) {
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
try {
// tslint:disable-next-line
console.log('User\'s fingerprint: ' +
(await this.cryptoService.getFingerprint(user.userId, publicKey.buffer)).join('-'));
} catch { }
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
const request = new OrganizationUserConfirmRequest();
request.key = key.encryptedString;

View File

@@ -1,3 +1,8 @@
<app-callout *ngIf="userCanAccessBusinessPortal" [type]="'warning'">
<p>{{'webPoliciesDeprecationWarning' | i18n}}</p>
<button type="button" class="btn btn-outline-secondary"
(click)="goToEnterprisePortal()">{{'businessPortal' | i18n}}</button>
</app-callout>
<div class="page-header d-flex">
<h1>{{'policies' | i18n}}</h1>
</div>
@@ -8,7 +13,7 @@
<table class="table table-hover table-list" *ngIf="!loading">
<tbody>
<tr *ngFor="let p of policies">
<td>
<td *ngIf="p.display">
<a href="#" appStopClick (click)="edit(p)">{{p.name}}</a>
<span class="badge badge-success" *ngIf="p.enabled">{{'enabled' | i18n}}</span>
<small class="text-muted d-block">{{p.description}}</small>

View File

@@ -12,6 +12,7 @@ import {
import { PolicyType } from 'jslib/enums/policyType';
import { EnvironmentService } from 'jslib/abstractions';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
@@ -34,6 +35,12 @@ export class PoliciesComponent implements OnInit {
organizationId: string;
policies: any[];
// Remove when removing deprecation warning
enterpriseTokenPromise: Promise<any>;
userCanAccessBusinessPortal = false;
private enterpriseUrl: string;
private modal: ModalComponent = null;
private orgPolicies: PolicyResponse[];
private policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
@@ -41,48 +48,116 @@ export class PoliciesComponent implements OnInit {
constructor(private apiService: ApiService, private route: ActivatedRoute,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private platformUtilsService: PlatformUtilsService, private userService: UserService,
private router: Router) {
this.policies = [
{
name: i18nService.t('twoStepLogin'),
description: i18nService.t('twoStepLoginPolicyDesc'),
type: PolicyType.TwoFactorAuthentication,
enabled: false,
},
{
name: i18nService.t('masterPass'),
description: i18nService.t('masterPassPolicyDesc'),
type: PolicyType.MasterPassword,
enabled: false,
},
{
name: i18nService.t('passwordGenerator'),
description: i18nService.t('passwordGeneratorPolicyDesc'),
type: PolicyType.PasswordGenerator,
enabled: false,
},
];
}
private router: Router, private environmentService: EnvironmentService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (organization == null || !organization.usePolicies) {
this.router.navigate(['/organizations', this.organizationId]);
return;
}
this.userCanAccessBusinessPortal = organization.canAccessBusinessPortal;
this.policies = [
{
name: this.i18nService.t('twoStepLogin'),
description: this.i18nService.t('twoStepLoginPolicyDesc'),
type: PolicyType.TwoFactorAuthentication,
enabled: false,
display: true,
},
{
name: this.i18nService.t('masterPass'),
description: this.i18nService.t('masterPassPolicyDesc'),
type: PolicyType.MasterPassword,
enabled: false,
display: true,
},
{
name: this.i18nService.t('passwordGenerator'),
description: this.i18nService.t('passwordGeneratorPolicyDesc'),
type: PolicyType.PasswordGenerator,
enabled: false,
display: true,
},
{
name: this.i18nService.t('singleOrg'),
description: this.i18nService.t('singleOrgDesc'),
type: PolicyType.SingleOrg,
enabled: false,
display: true,
},
{
name: this.i18nService.t('requireSso'),
description: this.i18nService.t('requireSsoPolicyDesc'),
type: PolicyType.RequireSso,
enabled: false,
display: organization.useSso,
},
{
name: this.i18nService.t('personalOwnership'),
description: this.i18nService.t('personalOwnershipPolicyDesc'),
type: PolicyType.PersonalOwnership,
enabled: false,
display: true,
},
{
name: this.i18nService.t('disableSend'),
description: this.i18nService.t('disableSendPolicyDesc'),
type: PolicyType.DisableSend,
enabled: false,
display: true,
},
{
name: this.i18nService.t('sendOptions'),
description: this.i18nService.t('sendOptionsPolicyDesc'),
type: PolicyType.SendOptions,
enabled: false,
display: true,
},
];
await this.load();
// Handle policies component launch from Event message
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
if (qParams.policyId != null) {
const policyIdFromEvents: string = qParams.policyId;
for (const orgPolicy of this.orgPolicies) {
if (orgPolicy.id === policyIdFromEvents) {
for (let i = 0; i < this.policies.length; i++) {
if (this.policies[i].type === orgPolicy.type) {
this.edit(this.policies[i]);
break;
}
}
break;
}
}
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
});
// Remove when removing deprecation warning
this.enterpriseUrl = 'https://portal.bitwarden.com';
if (this.environmentService.enterpriseUrl != null) {
this.enterpriseUrl = this.environmentService.enterpriseUrl;
} else if (this.environmentService.baseUrl != null) {
this.enterpriseUrl = this.environmentService.baseUrl + '/portal';
}
}
async load() {
const response = await this.apiService.getPolicies(this.organizationId);
this.orgPolicies = response.data != null && response.data.length > 0 ? response.data : [];
this.orgPolicies.forEach((op) => {
this.orgPolicies.forEach(op => {
this.policiesEnabledMap.set(op.type, op.enabled);
});
this.policies.forEach((p) => {
this.policies.forEach(p => {
p.enabled = this.policiesEnabledMap.has(p.type) && this.policiesEnabledMap.get(p.type);
});
this.loading = false;
@@ -102,6 +177,7 @@ export class PoliciesComponent implements OnInit {
childComponent.description = p.description;
childComponent.type = p.type;
childComponent.organizationId = this.organizationId;
childComponent.policiesEnabledMap = this.policiesEnabledMap;
childComponent.onSavedPolicy.subscribe(() => {
this.modal.close();
this.load();
@@ -111,4 +187,22 @@ export class PoliciesComponent implements OnInit {
this.modal = null;
});
}
// Remove when removing deprecation warning
async goToEnterprisePortal() {
if (this.enterpriseTokenPromise != null) {
return;
}
try {
this.enterpriseTokenPromise = this.apiService.getEnterprisePortalSignInToken();
const token = await this.enterpriseTokenPromise;
if (token != null) {
const userId = await this.userService.getUserId();
this.platformUtilsService.launchUri(this.enterpriseUrl + '/login?userId=' + userId +
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organizationId);
}
} catch { }
this.enterpriseTokenPromise = null;
}
}

View File

@@ -17,11 +17,32 @@
title="{{'warning' | i18n}}" icon="fa-warning">
{{'twoStepLoginPolicyWarning' | i18n}}
</app-callout>
<app-callout type="warning" *ngIf="type === policyType.SingleOrg" title="{{'warning' | i18n}}"
icon="fa-warning">
{{'singleOrgPolicyWarning' | i18n}}
</app-callout>
<ng-container *ngIf="type === policyType.RequireSso">
<app-callout type="tip" title="{{'prerequisite' | i18n}}">
{{'requireSsoPolicyReq' | i18n}}
</app-callout>
<app-callout type="warning">
{{'requireSsoExemption' | i18n}}
</app-callout>
</ng-container>
<app-callout type="warning" *ngIf="type === policyType.PersonalOwnership">
{{'personalOwnershipExemption' | i18n}}
</app-callout>
<app-callout type="warning" *ngIf="type === policyType.DisableSend">
{{'disableSendExemption' | i18n}}
</app-callout>
<app-callout type="warning" *ngIf="type === policyType.SendOptions">
{{'sendOptionsExemption' | i18n}}
</app-callout>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"
name="Enabled">
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
<label class="form-check-label" for="enabled">{{checkboxDesc}}</label>
</div>
</div>
<ng-container *ngIf="type === policyType.MasterPassword">
@@ -129,6 +150,14 @@
<label class="form-check-label" for="passGenIncludeNumber">{{'includeNumber' | i18n}}</label>
</div>
</ng-container>
<ng-container *ngIf="type === policyType.SendOptions">
<h3 class="mt-4">{{'options' | i18n}}</h3>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sendDisableHideEmail" [(ngModel)]="sendDisableHideEmail"
name="SendDisableHideEmail">
<label class="form-check-label" for="sendDisableHideEmail">{{'disableHideEmail' | i18n}}</label>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">

View File

@@ -7,7 +7,6 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -27,6 +26,7 @@ export class PolicyEditComponent implements OnInit {
@Input() description: string;
@Input() type: PolicyType;
@Input() organizationId: string;
@Input() policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
@Output() onSavedPolicy = new EventEmitter();
policyType = PolicyType;
@@ -37,7 +37,6 @@ export class PolicyEditComponent implements OnInit {
defaultTypes: any[];
// Master password
masterPassMinComplexity?: number = null;
masterPassMinLength?: number;
masterPassRequireUpper?: number;
@@ -46,7 +45,6 @@ export class PolicyEditComponent implements OnInit {
masterPassRequireSpecial?: number;
// Password generator
passGenDefaultType?: string;
passGenMinLength?: number;
passGenUseUpper?: boolean;
@@ -59,10 +57,13 @@ export class PolicyEditComponent implements OnInit {
passGenCapitalize?: boolean;
passGenIncludeNumber?: boolean;
// Send options
sendDisableHideEmail?: boolean;
private policy: PolicyResponse;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService) {
private toasterService: ToasterService) {
this.passwordScores = [
{ name: '-- ' + i18nService.t('select') + ' --', value: null },
{ name: i18nService.t('weak') + ' (0)', value: 0 },
@@ -112,6 +113,9 @@ export class PolicyEditComponent implements OnInit {
this.masterPassRequireNumbers = this.policy.data.requireNumbers;
this.masterPassRequireSpecial = this.policy.data.requireSpecial;
break;
case PolicyType.SendOptions:
this.sendDisableHideEmail = this.policy.data.disableHideEmail;
break;
default:
break;
}
@@ -127,45 +131,89 @@ export class PolicyEditComponent implements OnInit {
}
async submit() {
const request = new PolicyRequest();
request.enabled = this.enabled;
request.type = this.type;
request.data = null;
switch (this.type) {
case PolicyType.PasswordGenerator:
request.data = {
defaultType: this.passGenDefaultType,
minLength: this.passGenMinLength || null,
useUpper: this.passGenUseUpper,
useLower: this.passGenUseLower,
useNumbers: this.passGenUseNumbers,
useSpecial: this.passGenUseSpecial,
minNumbers: this.passGenMinNumbers || null,
minSpecial: this.passGenMinSpecial || null,
minNumberWords: this.passGenMinNumberWords || null,
capitalize: this.passGenCapitalize,
includeNumber: this.passGenIncludeNumber,
};
break;
case PolicyType.MasterPassword:
request.data = {
minComplexity: this.masterPassMinComplexity || null,
minLength: this.masterPassMinLength || null,
requireUpper: this.masterPassRequireUpper,
requireLower: this.masterPassRequireLower,
requireNumbers: this.masterPassRequireNumbers,
requireSpecial: this.masterPassRequireSpecial,
};
break;
default:
break;
if (this.preValidate()) {
const request = new PolicyRequest();
request.enabled = this.enabled;
request.type = this.type;
request.data = null;
switch (this.type) {
case PolicyType.PasswordGenerator:
request.data = {
defaultType: this.passGenDefaultType,
minLength: this.passGenMinLength || null,
useUpper: this.passGenUseUpper,
useLower: this.passGenUseLower,
useNumbers: this.passGenUseNumbers,
useSpecial: this.passGenUseSpecial,
minNumbers: this.passGenMinNumbers || null,
minSpecial: this.passGenMinSpecial || null,
minNumberWords: this.passGenMinNumberWords || null,
capitalize: this.passGenCapitalize,
includeNumber: this.passGenIncludeNumber,
};
break;
case PolicyType.MasterPassword:
request.data = {
minComplexity: this.masterPassMinComplexity || null,
minLength: this.masterPassMinLength || null,
requireUpper: this.masterPassRequireUpper,
requireLower: this.masterPassRequireLower,
requireNumbers: this.masterPassRequireNumbers,
requireSpecial: this.masterPassRequireSpecial,
};
break;
case PolicyType.SendOptions:
request.data = {
disableHideEmail: this.sendDisableHideEmail,
};
break;
default:
break;
}
try {
this.formPromise = this.apiService.putPolicy(this.organizationId, this.type, request);
await this.formPromise;
this.toasterService.popAsync('success', null, this.i18nService.t('editedPolicyId', this.name));
this.onSavedPolicy.emit();
} catch { }
}
}
get checkboxDesc(): string {
return this.type === PolicyType.PersonalOwnership ? this.i18nService.t('personalOwnershipCheckboxDesc') :
this.i18nService.t('enabled');
}
private preValidate(): boolean {
switch (this.type) {
case PolicyType.RequireSso:
// Don't need prevalidation checks if submitting to disable
if (!this.enabled) {
return true;
}
// Have SingleOrg policy enabled?
if (!(this.policiesEnabledMap.has(PolicyType.SingleOrg)
&& this.policiesEnabledMap.get(PolicyType.SingleOrg))) {
this.toasterService.popAsync('error', null, this.i18nService.t('requireSsoPolicyReqError'));
return false;
}
return true;
case PolicyType.SingleOrg:
// Don't need prevalidation checks if submitting to enable
if (this.enabled) {
return true;
}
// If RequireSso Policy is enabled prevent submittal
if (this.policiesEnabledMap.has(PolicyType.RequireSso)
&& this.policiesEnabledMap.get(PolicyType.RequireSso)) {
this.toasterService.popAsync('error', null, this.i18nService.t('disableRequireSsoError'));
return false;
}
return true;
default:
return true;
}
try {
this.formPromise = this.apiService.putPolicy(this.organizationId, this.type, request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Edited Policy' });
this.toasterService.popAsync('success', null, this.i18nService.t('editedPolicyId', this.name));
this.onSavedPolicy.emit();
} catch { }
}
}

View File

@@ -63,8 +63,136 @@
<small>{{'ownerDesc' | i18n}}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="userTypeCustom"
[value]="organizationUserType.Custom" [(ngModel)]="type">
<label class="form-check-label" for="userTypeCustom">
{{'custom' | i18n}}
<small>{{'customDesc' | i18n}}</small>
</label>
</div>
<ng-container *ngIf="customUserTypeSelected">
<h3 class="mt-4 d-flex">
{{'permissions' | i18n}}
</h3>
<div class="row">
<div class="col-6">
<div class="mb-3">
<label class="font-weight-bold mb-0">Manager Permissions</label>
<hr class="my-0 mr-2" />
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageAssignedCollections"
id="manageAssignedCollections"
[(ngModel)]="permissions.manageAssignedCollections">
<label class="form-check-label font-weight-normal"
for="manageAssignedCollections">
{{'manageAssignedCollections' | i18n}}
</label>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="mb-3">
<label class="font-weight-bold mb-0">Admin Permissions</label>
<hr class="my-0 mr-2" />
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="accessBusinessPortal"
id="accessBusinessPortal" [(ngModel)]="permissions.accessBusinessPortal">
<label class="form-check-label font-weight-normal" for="accessBusinessPortal">
{{'accessBusinessPortal' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="accessEventLogs"
id="accessEventLogs" [(ngModel)]="permissions.accessEventLogs">
<label class="form-check-label font-weight-normal" for="accessEventLogs">
{{'accessEventLogs' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="accessImportExport"
id="accessImportExport" [(ngModel)]="permissions.accessImportExport">
<label class="form-check-label font-weight-normal" for="accessImportExport">
{{'accessImportExport' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="accessReports"
id="accessReports" [(ngModel)]="permissions.accessReports">
<label class="form-check-label font-weight-normal" for="accessReports">
{{'accessReports' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageAllCollections"
id="manageAllCollections" [(ngModel)]="permissions.manageAllCollections">
<label class="form-check-label font-weight-normal" for="manageAllCollections">
{{'manageAllCollections' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageGroups"
id="manageGroups" [(ngModel)]="permissions.manageGroups">
<label class="form-check-label font-weight-normal" for="manageGroups">
{{'manageGroups' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageSso"
id="managePolicies" [(ngModel)]="permissions.manageSso">
<label class="form-check-label font-weight-normal" for="manageSso">
{{'manageSso' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="managePolicies"
id="managePolicies" [(ngModel)]="permissions.managePolicies">
<label class="form-check-label font-weight-normal" for="managePolicies">
{{'managePolicies' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageUsers"
id="manageUsers" [(ngModel)]="permissions.manageUsers">
<label class="form-check-label font-weight-normal" for="manageUsers">
{{'manageUsers' | i18n}}
</label>
</div>
</div>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageResetPassword"
id="manageResetPassword" [(ngModel)]="permissions.manageResetPassword">
<label class="form-check-label font-weight-normal" for="manageResetPassword">
{{'manageResetPassword' | i18n}}
</label>
</div>
</div>
</div>
</div>
</div>
</ng-container>
<h3 class="mt-4 d-flex">
<div class="mb-2">
<div class="mb-3">
{{'accessControl' | i18n}}
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
href="https://bitwarden.com/help/article/user-types-access-control/#access-control">
@@ -136,8 +264,9 @@
<i class="fa fa-spinner fa-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>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{'cancel' | i18n}}
</button>
<div class="ml-auto">
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"

View File

@@ -7,7 +7,6 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CollectionService } from 'jslib/abstractions/collection.service';
@@ -23,6 +22,7 @@ import { CollectionDetailsResponse } from 'jslib/models/response/collectionRespo
import { CollectionView } from 'jslib/models/view/collectionView';
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
import { PermissionsApi } from 'jslib/models/api/permissionsApi';
@Component({
selector: 'app-user-add-edit',
@@ -40,15 +40,21 @@ export class UserAddEditComponent implements OnInit {
title: string;
emails: string;
type: OrganizationUserType = OrganizationUserType.User;
permissions = new PermissionsApi();
showCustom = false;
access: 'all' | 'selected' = 'selected';
collections: CollectionView[] = [];
formPromise: Promise<any>;
deletePromise: Promise<any>;
organizationUserType = OrganizationUserType;
get customUserTypeSelected(): boolean {
return this.type === OrganizationUserType.Custom;
}
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService) { }
private toasterService: ToasterService, private collectionService: CollectionService,
private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
this.editMode = this.loading = this.organizationUserId != null;
@@ -61,9 +67,12 @@ export class UserAddEditComponent implements OnInit {
const user = await this.apiService.getOrganizationUser(this.organizationId, this.organizationUserId);
this.access = user.accessAll ? 'all' : 'selected';
this.type = user.type;
if (user.type === OrganizationUserType.Custom) {
this.permissions = user.permissions;
}
if (user.collections != null && this.collections != null) {
user.collections.forEach((s) => {
const collection = this.collections.filter((c) => c.id === s.id);
user.collections.forEach(s => {
const collection = this.collections.filter(c => c.id === s.id);
if (collection != null && collection.length > 0) {
(collection[0] as any).checked = true;
collection[0].readOnly = s.readOnly;
@@ -81,7 +90,7 @@ export class UserAddEditComponent implements OnInit {
async loadCollections() {
const response = await this.apiService.getCollections(this.organizationId);
const collections = response.data.map((r) =>
const collections = response.data.map(r =>
new Collection(new CollectionData(r as CollectionDetailsResponse)));
this.collections = await this.collectionService.decryptMany(collections);
}
@@ -94,14 +103,51 @@ export class UserAddEditComponent implements OnInit {
}
selectAll(select: boolean) {
this.collections.forEach((c) => this.check(c, select));
this.collections.forEach(c => this.check(c, select));
}
setRequestPermissions(p: PermissionsApi, clearPermissions: boolean) {
p.accessBusinessPortal = clearPermissions ?
false :
this.permissions.accessBusinessPortal;
p.accessEventLogs = this.permissions.accessEventLogs = clearPermissions ?
false :
this.permissions.accessEventLogs;
p.accessImportExport = clearPermissions ?
false :
this.permissions.accessImportExport;
p.accessReports = clearPermissions ?
false :
this.permissions.accessReports;
p.manageAllCollections = clearPermissions ?
false :
this.permissions.manageAllCollections;
p.manageAssignedCollections = clearPermissions ?
false :
this.permissions.manageAssignedCollections;
p.manageGroups = clearPermissions ?
false :
this.permissions.manageGroups;
p.manageSso = clearPermissions ?
false :
this.permissions.manageSso;
p.managePolicies = clearPermissions ?
false :
this.permissions.managePolicies;
p.manageUsers = clearPermissions ?
false :
this.permissions.manageUsers;
p.manageResetPassword = clearPermissions ?
false :
this.permissions.manageResetPassword;
return p;
}
async submit() {
let collections: SelectionReadOnlyRequest[] = null;
if (this.access !== 'all') {
collections = this.collections.filter((c) => (c as any).checked)
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
collections = this.collections.filter(c => (c as any).checked)
.map(c => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
}
try {
@@ -110,6 +156,7 @@ export class UserAddEditComponent implements OnInit {
request.accessAll = this.access === 'all';
request.type = this.type;
request.collections = collections;
request.permissions = this.setRequestPermissions(request.permissions ?? new PermissionsApi(), request.type !== OrganizationUserType.Custom);
this.formPromise = this.apiService.putOrganizationUser(this.organizationId, this.organizationUserId,
request);
} else {
@@ -117,11 +164,11 @@ export class UserAddEditComponent implements OnInit {
request.emails = this.emails.trim().split(/\s*,\s*/);
request.accessAll = this.access === 'all';
request.type = this.type;
request.permissions = this.setRequestPermissions(request.permissions ?? new PermissionsApi(), request.type !== OrganizationUserType.Custom);
request.collections = collections;
this.formPromise = this.apiService.postOrganizationUserInvite(this.organizationId, request);
}
await this.formPromise;
this.analytics.eventTrack.next({ action: this.editMode ? 'Edited User' : 'Invited User' });
this.toasterService.popAsync('success', null,
this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
this.onSavedUser.emit();
@@ -143,9 +190,9 @@ export class UserAddEditComponent implements OnInit {
try {
this.deletePromise = this.apiService.deleteOrganizationUser(this.organizationId, this.organizationUserId);
await this.deletePromise;
this.analytics.eventTrack.next({ action: 'Deleted User' });
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', this.name));
this.onDeletedUser.emit();
} catch { }
}
}

View File

@@ -1,6 +1,6 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<form class="modal-content" #form (ngSubmit)="submit()">
<div class="modal-header">
<h2 class="modal-title" id="confirmUserTitle">
{{'confirmUser' | i18n}}

View File

@@ -6,18 +6,12 @@ import {
Output,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ConstantsService } from 'jslib/services/constants.service';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
import { Utils } from 'jslib/misc/utils';
@Component({
@@ -34,13 +28,11 @@ export class UserConfirmComponent implements OnInit {
dontAskAgain = false;
loading = true;
fingerprint: string;
formPromise: Promise<any>;
private publicKey: Uint8Array = null;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private storageService: StorageService) { }
constructor(private apiService: ApiService, private cryptoService: CryptoService,
private storageService: StorageService) { }
async ngOnInit() {
try {
@@ -65,20 +57,6 @@ export class UserConfirmComponent implements OnInit {
await this.storageService.save(ConstantsService.autoConfirmFingerprints, true);
}
try {
this.formPromise = this.doConfirmation();
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Confirmed User' });
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', this.name));
this.onConfirmedUser.emit();
} catch { }
}
private async doConfirmation() {
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
const key = await this.cryptoService.rsaEncrypt(orgKey.key, this.publicKey.buffer);
const request = new OrganizationUserConfirmRequest();
request.key = key.encryptedString;
await this.apiService.postOrganizationUserConfirm(this.organizationId, this.organizationUserId, request);
this.onConfirmedUser.emit(this.publicKey);
}
}

View File

@@ -7,7 +7,6 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -32,11 +31,11 @@ export class UserGroupsComponent implements OnInit {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService) { }
private toasterService: ToasterService) { }
async ngOnInit() {
const groupsResponse = await this.apiService.getGroups(this.organizationId);
const groups = groupsResponse.data.map((r) => r);
const groups = groupsResponse.data.map(r => r);
groups.sort(Utils.getSortFunction(this.i18nService, 'name'));
this.groups = groups;
@@ -44,8 +43,8 @@ export class UserGroupsComponent implements OnInit {
const userGroups = await this.apiService.getOrganizationUserGroups(
this.organizationId, this.organizationUserId);
if (userGroups != null && this.groups != null) {
userGroups.forEach((ug) => {
const group = this.groups.filter((g) => g.id === ug);
userGroups.forEach(ug => {
const group = this.groups.filter(g => g.id === ug);
if (group != null && group.length > 0) {
(group[0] as any).checked = true;
}
@@ -64,18 +63,17 @@ export class UserGroupsComponent implements OnInit {
}
selectAll(select: boolean) {
this.groups.forEach((g) => this.check(g, select));
this.groups.forEach(g => this.check(g, select));
}
async submit() {
const request = new OrganizationUserUpdateGroupsRequest();
request.groupIds = this.groups.filter((g) => (g as any).checked).map((g) => g.id);
request.groupIds = this.groups.filter(g => (g as any).checked).map(g => g.id);
try {
this.formPromise = this.apiService.putOrganizationUserGroups(this.organizationId, this.organizationUserId,
request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Edited User Groups' });
this.toasterService.popAsync('success', null, this.i18nService.t('editedGroupsForUser', this.name));
this.onSavedUser.emit();
} catch { }

View File

@@ -7,7 +7,6 @@ import {
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -18,11 +17,10 @@ import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpda
import { OrganizationResponse } from 'jslib/models/response/organizationResponse';
import { ModalComponent } from '../../modal.component';
import { ApiKeyComponent } from '../../settings/api-key.component';
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
import { TaxInfoComponent } from '../../settings/tax-info.component';
import { ApiKeyComponent } from './api-key.component';
import { DeleteOrganizationComponent } from './delete-organization.component';
import { RotateApiKeyComponent } from './rotate-api-key.component';
@Component({
selector: 'app-org-account',
@@ -47,13 +45,12 @@ export class AccountComponent {
constructor(private componentFactoryResolver: ComponentFactoryResolver,
private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private route: ActivatedRoute, private syncService: SyncService,
private platformUtilsService: PlatformUtilsService) { }
private toasterService: ToasterService, private route: ActivatedRoute,
private syncService: SyncService, private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost();
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
try {
this.org = await this.apiService.getOrganization(this.organizationId);
@@ -74,7 +71,6 @@ export class AccountComponent {
return this.syncService.fullSync(true);
});
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Updated Organization Settings' });
this.toasterService.popAsync('success', null, this.i18nService.t('organizationUpdated'));
} catch { }
}
@@ -82,7 +78,6 @@ export class AccountComponent {
async submitTaxInfo() {
this.taxFormPromise = this.taxInfo.submitTaxInfo();
await this.taxFormPromise;
this.analytics.eventTrack.next({ action: 'Updated Organization Tax Info' });
this.toasterService.popAsync('success', null, this.i18nService.t('taxInfoUpdated'));
}
@@ -125,7 +120,14 @@ export class AccountComponent {
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.apiKeyModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.apiKeyModalRef);
childComponent.organizationId = this.organizationId;
childComponent.keyType = 'organization';
childComponent.entityId = this.organizationId;
childComponent.postKey = this.apiService.postOrganizationApiKey.bind(this.apiService);
childComponent.scope = 'api.organization';
childComponent.grantType = 'client_credentials';
childComponent.apiKeyTitle = 'apiKey';
childComponent.apiKeyWarning = 'apiKeyWarning';
childComponent.apiKeyDescription = 'apiKeyDesc';
this.modal.onClosed.subscribe(async () => {
this.modal = null;
@@ -139,8 +141,16 @@ export class AccountComponent {
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.rotateApiKeyModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<RotateApiKeyComponent>(RotateApiKeyComponent, this.rotateApiKeyModalRef);
childComponent.organizationId = this.organizationId;
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.rotateApiKeyModalRef);
childComponent.keyType = 'organization';
childComponent.isRotation = true;
childComponent.entityId = this.organizationId;
childComponent.postKey = this.apiService.postOrganizationRotateApiKey.bind(this.apiService);
childComponent.scope = 'api.organization';
childComponent.grantType = 'client_credentials';
childComponent.apiKeyTitle = 'apiKey';
childComponent.apiKeyWarning = 'apiKeyWarning';
childComponent.apiKeyDescription = 'apiKeyRotateDesc';
this.modal.onClosed.subscribe(async () => {
this.modal = null;

View File

@@ -12,7 +12,6 @@ import {
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -39,8 +38,8 @@ export class AdjustSeatsComponent {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private router: Router, private activatedRoute: ActivatedRoute) { }
private toasterService: ToasterService, private router: Router,
private activatedRoute: ActivatedRoute) { }
async submit() {
try {
@@ -63,7 +62,6 @@ export class AdjustSeatsComponent {
};
this.formPromise = action();
await this.formPromise;
this.analytics.eventTrack.next({ action: this.add ? 'Added Seats' : 'Removed Seats' });
this.onAdjusted.emit(this.seatAdjustment);
if (paymentFailed) {
this.toasterService.popAsync({

View File

@@ -28,7 +28,6 @@ export class ChangePlanComponent {
async submit() {
try {
this.platformUtilsService.eventTrack('Changed Plan');
this.onChanged.emit();
} catch { }
}

View File

@@ -2,7 +2,6 @@ import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
@@ -21,8 +20,8 @@ export class DeleteOrganizationComponent {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private router: Router) { }
private toasterService: ToasterService, private cryptoService: CryptoService,
private router: Router) { }
async submit() {
if (this.masterPassword == null || this.masterPassword === '') {
@@ -36,7 +35,6 @@ export class DeleteOrganizationComponent {
try {
this.formPromise = this.apiService.deleteOrganization(this.organizationId, request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Deleted Organization' });
this.toasterService.popAsync('success', this.i18nService.t('organizationDeleted'),
this.i18nService.t('organizationDeletedDesc'));
this.router.navigate(['/']);

View File

@@ -32,7 +32,6 @@ export class DownloadLicenseComponent {
const license = await this.formPromise;
const licenseString = JSON.stringify(license, null, 2);
this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_organization_license.json');
this.platformUtilsService.eventTrack('Downloaded License');
this.onDownloaded.emit();
} catch { }
}

View File

@@ -5,7 +5,6 @@ import {
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
@@ -18,14 +17,13 @@ import { UserBillingComponent } from '../../settings/user-billing.component';
templateUrl: '../../settings/user-billing.component.html',
})
export class OrganizationBillingComponent extends UserBillingComponent implements OnInit {
constructor(apiService: ApiService, i18nService: I18nService,
analytics: Angulartics2, toasterService: ToasterService,
constructor(apiService: ApiService, i18nService: I18nService, toasterService: ToasterService,
private route: ActivatedRoute, platformUtilsService: PlatformUtilsService) {
super(apiService, i18nService, analytics, toasterService, platformUtilsService);
super(apiService, i18nService, toasterService, platformUtilsService);
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
await this.load();
this.firstLoaded = true;

View File

@@ -5,7 +5,6 @@ import {
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { OrganizationSubscriptionResponse } from 'jslib/models/response/organizationSubscriptionResponse';
@@ -38,14 +37,13 @@ export class OrganizationSubscriptionComponent implements OnInit {
reinstatePromise: Promise<any>;
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private analytics: Angulartics2,
private toasterService: ToasterService, private messagingService: MessagingService,
private route: ActivatedRoute) {
private i18nService: I18nService, private toasterService: ToasterService,
private messagingService: MessagingService, private route: ActivatedRoute) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
await this.load();
this.firstLoaded = true;
@@ -75,7 +73,6 @@ export class OrganizationSubscriptionComponent implements OnInit {
try {
this.reinstatePromise = this.apiService.postOrganizationReinstate(this.organizationId);
await this.reinstatePromise;
this.analytics.eventTrack.next({ action: 'Reinstated Plan' });
this.toasterService.popAsync('success', null, this.i18nService.t('reinstated'));
this.load();
} catch { }
@@ -95,7 +92,6 @@ export class OrganizationSubscriptionComponent implements OnInit {
try {
this.cancelPromise = this.apiService.postOrganizationCancel(this.organizationId);
await this.cancelPromise;
this.analytics.eventTrack.next({ action: 'Canceled Plan' });
this.toasterService.popAsync('success', null, this.i18nService.t('canceledSubscription'));
this.load();
} catch { }

View File

@@ -1,48 +0,0 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="rotateKeyTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="rotateKeyTitle">{{'rotateApiKey' | 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>{{'apiKeyRotateDesc' | i18n}}</p>
<ng-container *ngIf="!clientSecret">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
</ng-container>
<app-callout type="warning" *ngIf="clientSecret">{{'apiKeyWarning' | i18n}}</app-callout>
<app-callout type="info" title="{{'oauth2ClientCredentials' | i18n}}" icon="fa-key"
*ngIf="clientSecret">
<p class="mb-1">
<strong>client_id:</strong><br>
<code>{{clientId}}</code>
</p>
<p class="mb-1">
<strong>client_secret:</strong><br>
<code>{{clientSecret}}</code>
</p>
<p class="mb-1">
<strong>scope:</strong><br>
<code>{{scope}}</code>
</p>
<p class="mb-0">
<strong>grant_type:</strong><br>
<code>client_credentials</code>
</p>
</app-callout>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"
*ngIf="!clientSecret">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'rotateApiKey' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
</div>
</div>

View File

@@ -1,50 +0,0 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse';
@Component({
selector: 'app-rotate-api-key',
templateUrl: 'rotate-api-key.component.html',
})
export class RotateApiKeyComponent {
organizationId: string;
masterPassword: string;
formPromise: Promise<ApiKeyResponse>;
clientId: string;
clientSecret: string;
scope: string;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private router: Router) { }
async submit() {
if (this.masterPassword == null || this.masterPassword === '') {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return;
}
const request = new PasswordVerificationRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
try {
this.formPromise = this.apiService.postOrganizationRotateApiKey(this.organizationId, request);
const response = await this.formPromise;
this.clientSecret = response.apiKey;
this.clientId = 'organization.' + this.organizationId;
this.scope = 'api.organization';
this.analytics.eventTrack.next({ action: 'Rotated Organization API Key' });
} catch { }
}
}

View File

@@ -16,7 +16,7 @@ export class SettingsComponent {
private platformUtilsService: PlatformUtilsService) { }
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
this.route.parent.params.subscribe(async params => {
this.selfHosted = await this.platformUtilsService.isSelfHost();
const organization = await this.userService.getOrganization(params.organizationId);
this.access2fa = organization.use2fa;

View File

@@ -26,7 +26,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
await super.ngOnInit();
});

View File

@@ -16,8 +16,6 @@ import { EventType } from 'jslib/enums/eventType';
templateUrl: '../../tools/export.component.html',
})
export class ExportComponent extends BaseExportComponent {
organizationId: string;
constructor(cryptoService: CryptoService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, exportService: ExportService,
eventService: EventService, private route: ActivatedRoute) {
@@ -25,7 +23,7 @@ export class ExportComponent extends BaseExportComponent {
}
ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
});
}

View File

@@ -13,6 +13,7 @@ import {
ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent,
} from '../../tools/exposed-passwords-report.component';
import { Cipher } from 'jslib/models/domain/cipher';
import { CipherView } from 'jslib/models/view/cipherView';
@Component({
@@ -20,6 +21,8 @@ import { CipherView } from 'jslib/models/view/cipherView';
templateUrl: '../../tools/exposed-passwords-report.component.html',
})
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
manageableCiphers: Cipher[];
constructor(cipherService: CipherService, auditService: AuditService,
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
userService: UserService, private route: ActivatedRoute) {
@@ -27,8 +30,9 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
}
ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
super.ngOnInit();
});
}
@@ -36,4 +40,8 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
canManageCipher(c: CipherView): boolean {
return this.manageableCiphers.some(x => x.id === c.id);
}
}

View File

@@ -5,10 +5,11 @@ import {
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { ImportService } from 'jslib/abstractions/import.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
import { ImportComponent as BaseImportComponent } from '../../tools/import.component';
@@ -17,17 +18,32 @@ import { ImportComponent as BaseImportComponent } from '../../tools/import.compo
templateUrl: '../../tools/import.component.html',
})
export class ImportComponent extends BaseImportComponent {
constructor(i18nService: I18nService, analytics: Angulartics2,
toasterService: ToasterService, importService: ImportService,
router: Router, private route: ActivatedRoute) {
super(i18nService, analytics, toasterService, importService, router);
organizationName: string;
constructor(i18nService: I18nService, toasterService: ToasterService,
importService: ImportService, router: Router, private route: ActivatedRoute,
platformUtilsService: PlatformUtilsService,
private userService: UserService) {
super(i18nService, toasterService, importService, router, platformUtilsService);
}
ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
this.successNavigate = ['organizations', this.organizationId, 'vault'];
super.ngOnInit();
});
const organization = await this.userService.getOrganization(this.organizationId);
this.organizationName = organization.name;
}
async submit() {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('importWarning', this.organizationName),
this.i18nService.t('warning'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
super.submit();
}
}

View File

@@ -26,7 +26,7 @@ export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorRepor
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organization = await this.userService.getOrganization(params.organizationId);
await super.ngOnInit();
});

View File

@@ -8,6 +8,8 @@ import { CipherService } from 'jslib/abstractions/cipher.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
import { Cipher } from 'jslib/models/domain/cipher';
import { CipherView } from 'jslib/models/view/cipherView';
import {
@@ -19,6 +21,8 @@ import {
templateUrl: '../../tools/reused-passwords-report.component.html',
})
export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent {
manageableCiphers: Cipher[];
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
messagingService: MessagingService, userService: UserService,
private route: ActivatedRoute) {
@@ -26,8 +30,9 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
await super.ngOnInit();
});
}
@@ -35,4 +40,8 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
canManageCipher(c: CipherView): boolean {
return this.manageableCiphers.some(x => x.id === c.id);
}
}

View File

@@ -1,48 +1,54 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card mb-4">
<div class="card-header">{{'tools' | i18n}}</div>
<div class="list-group list-group-flush">
<a routerLink="import" class="list-group-item" routerLinkActive="active">
{{'importData' | i18n}}
</a>
<a routerLink="export" class="list-group-item" routerLinkActive="active">
{{'exportVault' | i18n}}
</a>
</div>
</div>
<div class="card">
<div class="card-header d-flex">
{{'reports' | i18n}}
<div class="ml-auto">
<a href="#" appStopClick class="badge badge-primary" *ngIf="!accessReports"
(click)="upgradeOrganization()">
{{'upgrade' | i18n}}
<ng-container *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</ng-container>
<ng-container *ngIf="!loading">
<div class="row">
<div class="col-3">
<div class="card mb-4" *ngIf="organization.canAccessImportExport">
<div class="card-header">{{'tools' | i18n}}</div>
<div class="list-group list-group-flush">
<a routerLink="import" class="list-group-item" routerLinkActive="active">
{{'importData' | i18n}}
</a>
<a routerLink="export" class="list-group-item" routerLinkActive="active">
{{'exportVault' | i18n}}
</a>
</div>
</div>
<div class="list-group list-group-flush">
<a routerLink="exposed-passwords-report" class="list-group-item" routerLinkActive="active">
{{'exposedPasswordsReport' | i18n}}
</a>
<a routerLink="reused-passwords-report" class="list-group-item" routerLinkActive="active">
{{'reusedPasswordsReport' | i18n}}
</a>
<a routerLink="weak-passwords-report" class="list-group-item" routerLinkActive="active">
{{'weakPasswordsReport' | i18n}}
</a>
<a routerLink="unsecured-websites-report" class="list-group-item" routerLinkActive="active">
{{'unsecuredWebsitesReport' | i18n}}
</a>
<a routerLink="inactive-two-factor-report" class="list-group-item" routerLinkActive="active">
{{'inactive2faReport' | i18n}}
</a>
<div class="card" *ngIf="organization.canAccessReports">
<div class="card-header d-flex">
{{'reports' | i18n}}
<div class="ml-auto">
<a href="#" appStopClick class="badge badge-primary" *ngIf="!accessReports"
(click)="upgradeOrganization()">
{{'upgrade' | i18n}}
</a>
</div>
</div>
<div class="list-group list-group-flush">
<a routerLink="exposed-passwords-report" class="list-group-item" routerLinkActive="active">
{{'exposedPasswordsReport' | i18n}}
</a>
<a routerLink="reused-passwords-report" class="list-group-item" routerLinkActive="active">
{{'reusedPasswordsReport' | i18n}}
</a>
<a routerLink="weak-passwords-report" class="list-group-item" routerLinkActive="active">
{{'weakPasswordsReport' | i18n}}
</a>
<a routerLink="unsecured-websites-report" class="list-group-item" routerLinkActive="active">
{{'unsecuredWebsitesReport' | i18n}}
</a>
<a routerLink="inactive-two-factor-report" class="list-group-item" routerLinkActive="active">
{{'inactive2faReport' | i18n}}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</ng-container>
</div>

View File

@@ -13,16 +13,18 @@ import { UserService } from 'jslib/abstractions/user.service';
export class ToolsComponent {
organization: Organization;
accessReports = false;
loading = true;
constructor(private route: ActivatedRoute, private userService: UserService,
private messagingService: MessagingService) { }
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
this.route.parent.params.subscribe(async params => {
this.organization = await this.userService.getOrganization(params.organizationId);
// TODO: Maybe we want to just make sure they are not on a free plan? Just compare useTotp for now
// since all paid plans include useTotp
this.accessReports = this.organization.useTotp;
this.loading = false;
});
}

View File

@@ -26,7 +26,7 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organization = await this.userService.getOrganization(params.organizationId);
await super.ngOnInit();
});

View File

@@ -9,6 +9,8 @@ import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { UserService } from 'jslib/abstractions/user.service';
import { Cipher } from 'jslib/models/domain/cipher';
import { CipherView } from 'jslib/models/view/cipherView';
import {
@@ -20,6 +22,8 @@ import {
templateUrl: '../../tools/weak-passwords-report.component.html',
})
export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent {
manageableCiphers: Cipher[];
constructor(cipherService: CipherService, passwordGenerationService: PasswordGenerationService,
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
userService: UserService, private route: ActivatedRoute) {
@@ -27,8 +31,9 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.route.parent.parent.params.subscribe(async params => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
await super.ngOnInit();
});
}
@@ -36,4 +41,8 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
canManageCipher(c: CipherView): boolean {
return this.manageableCiphers.some(x => x.id === c.id);
}
}

View File

@@ -10,6 +10,7 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { StateService } from 'jslib/abstractions/state.service';
import { TotpService } from 'jslib/abstractions/totp.service';
import { UserService } from 'jslib/abstractions/user.service';
@@ -36,16 +37,16 @@ export class AddEditComponent extends BaseAddEditComponent {
userService: UserService, collectionService: CollectionService,
totpService: TotpService, passwordGenerationService: PasswordGenerationService,
private apiService: ApiService, messagingService: MessagingService,
eventService: EventService) {
eventService: EventService, policyService: PolicyService) {
super(cipherService, folderService, i18nService, platformUtilsService, auditService, stateService,
userService, collectionService, totpService, passwordGenerationService, messagingService,
eventService);
eventService, policyService);
}
protected allowOwnershipAssignment() {
if (this.ownershipOptions != null && this.ownershipOptions.length > 1) {
if (this.ownershipOptions != null && (this.ownershipOptions.length > 1 || !this.allowPersonal)) {
if (this.organization != null) {
return this.cloneMode && this.organization.isAdmin;
return this.cloneMode && this.organization.canManageAllCollections;
} else {
return !this.editMode || this.cloneMode;
}
@@ -54,14 +55,14 @@ export class AddEditComponent extends BaseAddEditComponent {
}
protected loadCollections() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.loadCollections();
}
return Promise.resolve(this.collections);
}
protected async loadCipher() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
@@ -71,14 +72,14 @@ export class AddEditComponent extends BaseAddEditComponent {
}
protected encryptCipher() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.encryptCipher();
}
return this.cipherService.encrypt(this.cipher, null, this.originalCipher);
}
protected async saveCipher(cipher: Cipher) {
if (!this.organization.isAdmin || cipher.organizationId == null) {
if (!this.organization.canManageAllCollections || cipher.organizationId == null) {
return super.saveCipher(cipher);
}
if (this.editMode && !this.cloneMode) {
@@ -91,7 +92,7 @@ export class AddEditComponent extends BaseAddEditComponent {
}
protected async deleteCipher() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.deleteCipher();
}
return this.cipher.isDeleted ? this.apiService.deleteCipherAdmin(this.cipherId)

View File

@@ -20,22 +20,23 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from '../../vault/at
templateUrl: '../../vault/attachments.component.html',
})
export class AttachmentsComponent extends BaseAttachmentsComponent {
viewOnly = false;
organization: Organization;
constructor(cipherService: CipherService, i18nService: I18nService,
cryptoService: CryptoService, userService: UserService,
platformUtilsService: PlatformUtilsService, private apiService: ApiService) {
super(cipherService, i18nService, cryptoService, userService, platformUtilsService);
platformUtilsService: PlatformUtilsService, apiService: ApiService) {
super(cipherService, i18nService, cryptoService, userService, platformUtilsService, apiService);
}
protected async reupload(attachment: AttachmentView) {
if (this.organization.isAdmin && this.showFixOldAttachments(attachment)) {
if (this.organization.canManageAllCollections && this.showFixOldAttachments(attachment)) {
await super.reuploadCipherAttachment(attachment, true);
}
}
protected async loadCipher() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
@@ -43,17 +44,17 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
}
protected saveCipherAttachment(file: File) {
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, this.organization.isAdmin);
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, this.organization.canManageAllCollections);
}
protected deleteCipherAttachment(attachmentId: string) {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.deleteCipherAttachment(attachmentId);
}
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
}
protected showFixOldAttachments(attachment: AttachmentView) {
return attachment.key == null && this.organization.isAdmin;
return attachment.key == null && this.organization.canManageAllCollections;
}
}

View File

@@ -5,7 +5,6 @@ import {
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
@@ -13,6 +12,8 @@ import { EventService } from 'jslib/abstractions/event.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { TotpService } from 'jslib/abstractions/totp.service';
import { UserService } from 'jslib/abstractions/user.service';
import { Organization } from 'jslib/models/domain/organization';
import { CipherView } from 'jslib/models/view/cipherView';
@@ -31,27 +32,27 @@ export class CiphersComponent extends BaseCiphersComponent {
protected allCiphers: CipherView[] = [];
constructor(searchService: SearchService, analytics: Angulartics2,
toasterService: ToasterService, i18nService: I18nService,
constructor(searchService: SearchService, toasterService: ToasterService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, cipherService: CipherService,
private apiService: ApiService, eventService: EventService) {
super(searchService, analytics, toasterService, i18nService, platformUtilsService,
cipherService, eventService);
private apiService: ApiService, eventService: EventService, totpService: TotpService, userService: UserService) {
super(searchService, toasterService, i18nService, platformUtilsService, cipherService,
eventService, totpService, userService);
}
async load(filter: (cipher: CipherView) => boolean = null) {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
await super.load(filter, this.deleted);
return;
}
this.accessEvents = this.organization.useEvents;
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
this.applyFilter(filter);
await this.searchService.indexCiphers(this.organization.id, this.allCiphers);
await this.applyFilter(filter);
this.loaded = true;
}
async applyFilter(filter: (cipher: CipherView) => boolean = null) {
if (this.organization.isAdmin) {
if (this.organization.canManageAllCollections) {
await super.applyFilter(filter);
} else {
const f = (c: CipherView) => c.organizationId === this.organization.id && (filter == null || filter(c));
@@ -60,40 +61,20 @@ export class CiphersComponent extends BaseCiphersComponent {
}
async search(timeout: number = null) {
if (!this.organization.isAdmin) {
return super.search(timeout);
}
this.searchPending = false;
let filteredCiphers = this.allCiphers;
if (this.searchText == null || this.searchText.trim().length < 2) {
this.ciphers = filteredCiphers.filter((c) => {
if (c.isDeleted !== this.deleted) {
return false;
}
return this.filter == null || this.filter(c);
});
} else {
if (this.filter != null) {
filteredCiphers = filteredCiphers.filter(this.filter);
}
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText, this.deleted);
}
await this.resetPaging();
super.search(timeout, this.allCiphers);
}
events(c: CipherView) {
this.onEventsClicked.emit(c);
}
protected deleteCipher(id: string) {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.deleteCipher(id, this.deleted);
}
return this.deleted ? this.apiService.deleteCipherAdmin(id) : this.apiService.putDeleteCipherAdmin(id);
}
protected showFixOldAttachments(c: CipherView) {
return this.organization.isAdmin && c.hasOldAttachments;
return this.organization.canManageAllCollections && c.hasOldAttachments;
}
}

View File

@@ -28,7 +28,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
}
protected async loadCipher() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
@@ -36,21 +36,21 @@ export class CollectionsComponent extends BaseCollectionsComponent {
}
protected loadCipherCollections() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.loadCipherCollections();
}
return this.collectionIds;
}
protected loadCollections() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
return super.loadCollections();
}
return Promise.resolve(this.collections);
}
protected saveCollections() {
if (this.organization.isAdmin) {
if (this.organization.canManageAllCollections) {
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);
return this.apiService.putCipherCollectionsAdmin(this.cipherId, request);
} else {

View File

@@ -29,14 +29,14 @@ export class GroupingsComponent extends BaseGroupingsComponent {
}
async loadCollections() {
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
await super.loadCollections(this.organization.id);
return;
}
const collections = await this.apiService.getCollections(this.organization.id);
if (collections != null && collections.data != null && collections.data.length) {
const collectionDomains = collections.data.map((r) =>
const collectionDomains = collections.data.map(r =>
new Collection(new CollectionData(r as CollectionDetailsResponse)));
this.collections = await this.collectionService.decryptMany(collectionDomains);
} else {

View File

@@ -29,6 +29,9 @@
</button>
</div>
</div>
<app-callout type="warning" *ngIf="deleted" icon="fa-warning">
{{trashCleanupWarning}}
</app-callout>
<app-org-vault-ciphers (onCipherClicked)="editCipher($event)"
(onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
(onCollectionsClicked)="editCipherCollections($event)" (onEventsClicked)="viewEvents($event)"

View File

@@ -15,6 +15,7 @@ import {
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { UserService } from 'jslib/abstractions/user.service';
@@ -52,6 +53,7 @@ export class VaultComponent implements OnInit, OnDestroy {
collectionId: string = null;
type: CipherType = null;
deleted: boolean = false;
trashCleanupWarning: string = null;
modal: ModalComponent = null;
@@ -59,17 +61,22 @@ export class VaultComponent implements OnInit, OnDestroy {
private router: Router, private changeDetectorRef: ChangeDetectorRef,
private syncService: SyncService, private i18nService: I18nService,
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService,
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
private broadcasterService: BroadcasterService, private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService) { }
ngOnInit() {
const queryParams = this.route.parent.params.subscribe(async (params) => {
this.trashCleanupWarning = this.i18nService.t(
this.platformUtilsService.isSelfHost() ? 'trashCleanupWarningSelfHosted' : 'trashCleanupWarning'
);
const queryParams = this.route.parent.params.subscribe(async params => {
this.organization = await this.userService.getOrganization(params.organizationId);
this.groupingsComponent.organization = this.organization;
this.ciphersComponent.organization = this.organization;
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search;
if (!this.organization.isAdmin) {
if (!this.organization.canManageAllCollections) {
await this.syncService.fullSync(false);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
@@ -110,7 +117,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
if (qParams.viewEvents != null) {
const cipher = this.ciphersComponent.ciphers.filter((c) => c.id === qParams.viewEvents);
const cipher = this.ciphersComponent.ciphers.filter(c => c.id === qParams.viewEvents);
if (cipher.length > 0) {
this.viewEvents(cipher[0]);
}
@@ -233,9 +240,9 @@ export class VaultComponent implements OnInit, OnDestroy {
this.modal = this.collectionsModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<CollectionsComponent>(CollectionsComponent, this.collectionsModalRef);
if (this.organization.isAdmin) {
if (this.organization.canManageAllCollections) {
childComponent.collectionIds = cipher.collectionIds;
childComponent.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
childComponent.collections = this.groupingsComponent.collections.filter(c => !c.readOnly);
}
childComponent.organization = this.organization;
childComponent.cipherId = cipher.id;
@@ -253,8 +260,8 @@ export class VaultComponent implements OnInit, OnDestroy {
const component = this.editCipher(null);
component.organizationId = this.organization.id;
component.type = this.type;
if (this.organization.isAdmin) {
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
if (this.organization.canManageAllCollections) {
component.collections = this.groupingsComponent.collections.filter(c => !c.readOnly);
}
if (this.collectionId != null) {
component.collectionIds = [this.collectionId];
@@ -296,8 +303,8 @@ export class VaultComponent implements OnInit, OnDestroy {
const component = this.editCipher(cipher);
component.cloneMode = true;
component.organizationId = this.organization.id;
if (this.organization.isAdmin) {
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
if (this.organization.canManageAllCollections) {
component.collections = this.groupingsComponent.collections.filter(c => !c.readOnly);
}
// Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value
// in the add-edit componenet

View File

@@ -19,4 +19,5 @@ if (process.env.ENV === 'production') {
// Other polyfills
require('whatwg-fetch');
require('webcrypto-shim');
require('date-input-polyfill');
/* tslint:enable */

View File

@@ -0,0 +1,92 @@
<form #form (ngSubmit)="load()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-center mt-5">
<div class="col-12">
<p class="lead text-center mb-4">Bitwarden Send</p>
</div>
<div class="col-12 text-center" *ngIf="creatorIdentifier != null">
<p>{{'sendCreatorIdentifier' | i18n: creatorIdentifier }}</p>
</div>
<div class="col-8" *ngIf="hideEmail">
<app-callout type="warning" title="{{'warning' | i18n}}">
{{'viewSendHiddenEmailWarning' | i18n }}
<a href="https://bitwarden.com/help/article/receive-send/" target="_blank">{{'learnMore' | i18n}}</a>.
</app-callout>
</div>
</div>
<div class="row justify-content-center">
<div class="col-5">
<div class="card d-block">
<div class="card-body" *ngIf="loading" class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</div>
<div class="card-body" *ngIf="!loading && passwordRequired">
<p>{{'sendProtectedPassword' | i18n}}</p>
<p>{{'sendProtectedPasswordDontKnow' | i18n}}</p>
<div class="form-group">
<label for="password">{{'password' | i18n}}</label>
<input id="password" type="password" name="Password" class="text-monospace form-control"
[(ngModel)]="password" required appInputVerbatim appAutofocus>
</div>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span>
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'continue' | i18n}}
</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="card-body" *ngIf="!loading && unavailable">
{{'sendAccessUnavailable' | i18n}}
</div>
<div class="card-body" *ngIf="!loading && error">
{{'unexpectedError' | i18n}}
</div>
<div class="card-body" *ngIf="!loading && !passwordRequired && send">
<p class="text-center"><b>{{send.name}}</b></p>
<hr>
<!-- Text -->
<ng-container *ngIf="send.type === sendType.Text">
<app-callout *ngIf="send.text.hidden" type="tip">{{'sendHiddenByDefault' | i18n}}</app-callout>
<div class="form-group">
<textarea id="text" rows="8" name="Text" [(ngModel)]="sendText" class="form-control"
readonly></textarea>
</div>
<button class="btn btn-block btn-link" type="button" (click)="toggleText()"
*ngIf="send.text.hidden">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showText, 'fa-eye-slash': showText}"></i>
{{'toggleVisibility' | i18n}}
</button>
<button class="btn btn-block btn-link" type="button" (click)="copyText()">
<i class="fa fa-copy" aria-hidden="true"></i> {{'copyValue' | i18n}}
</button>
</ng-container>
<!-- File -->
<ng-container *ngIf="send.type === sendType.File">
<p>{{send.file.fileName}}</p>
<button class="btn btn-primary btn-block" type="button" (click)="download()" *ngIf="!downloading">
<i class="fa fa-download" aria-hidden="true"></i>
{{'downloadFile' | i18n}} ({{send.file.sizeName}})</button>
<button class="btn btn-primary btn-block" type="button" *ngIf="downloading" disabled="true">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</ng-container>
<p *ngIf="expirationDate" class="text-center text-muted">Expires:
{{expirationDate | date: 'medium'}}</p>
</div>
</div>
</div>
<div class="col-12 text-center mt-5 text-muted">
<p class="mb-0">{{'sendAccessTaglineProductDesc' | i18n}}<br>
{{'sendAccessTaglineLearnMore' | i18n}} <a
href="https://www.bitwarden.com/products/send?source=web-vault" target="_blank">Bitwarden Send</a>
{{'sendAccessTaglineOr' | i18n}} <a
href="https://vault.bitwarden.com/#/register" target="_blank">{{'sendAccessTaglineSignUp' | i18n}}</a>
{{'sendAccessTaglineTryToday' | i18n}}
</p>
</div>
</div>
</form>

View File

@@ -0,0 +1,169 @@
import {
Component,
OnInit,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { Utils } from 'jslib/misc/utils';
import { SendAccess } from 'jslib/models/domain/sendAccess';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
import { SendAccessView } from 'jslib/models/view/sendAccessView';
import { SendType } from 'jslib/enums/sendType';
import { SendAccessRequest } from 'jslib/models/request/sendAccessRequest';
import { ErrorResponse } from 'jslib/models/response/errorResponse';
import { SendAccessResponse } from 'jslib/models/response/sendAccessResponse';
@Component({
selector: 'app-send-access',
templateUrl: 'access.component.html',
})
export class AccessComponent implements OnInit {
send: SendAccessView;
sendType = SendType;
downloading = false;
loading = true;
passwordRequired = false;
formPromise: Promise<SendAccessResponse>;
password: string;
showText = false;
unavailable = false;
error = false;
hideEmail = false;
private id: string;
private key: string;
private decKey: SymmetricCryptoKey;
private accessRequest: SendAccessRequest;
constructor(private i18nService: I18nService, private cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
private route: ActivatedRoute, private cryptoService: CryptoService) {
}
get sendText() {
if (this.send == null || this.send.text == null) {
return null;
}
return this.showText ? this.send.text.text : this.send.text.maskedText;
}
get expirationDate() {
if (this.send == null || this.send.expirationDate == null) {
return null;
}
return this.send.expirationDate;
}
get creatorIdentifier() {
if (this.send == null || this.send.creatorIdentifier == null) {
return null;
}
return this.send.creatorIdentifier;
}
ngOnInit() {
this.route.params.subscribe(async params => {
this.id = params.sendId;
this.key = params.key;
if (this.key == null || this.id == null) {
return;
}
await this.load();
});
}
async download() {
if (this.send == null || this.decKey == null) {
return;
}
if (this.downloading) {
return;
}
const downloadData = await this.apiService.getSendFileDownloadData(this.send, this.accessRequest);
if (Utils.isNullOrWhitespace(downloadData.url)) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('missingSendFile'));
return;
}
this.downloading = true;
const response = await fetch(new Request(downloadData.url, { cache: 'no-store' }));
if (response.status !== 200) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
this.downloading = false;
return;
}
try {
const buf = await response.arrayBuffer();
const decBuf = await this.cryptoService.decryptFromBytes(buf, this.decKey);
this.platformUtilsService.saveFile(window, decBuf, null, this.send.file.fileName);
} catch (e) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
}
this.downloading = false;
}
copyText() {
this.platformUtilsService.copyToClipboard(this.send.text.text);
this.platformUtilsService.showToast('success', null,
this.i18nService.t('valueCopied', this.i18nService.t('sendTypeText')));
}
toggleText() {
this.showText = !this.showText;
}
async load() {
this.unavailable = false;
this.error = false;
this.hideEmail = false;
const keyArray = Utils.fromUrlB64ToArray(this.key);
this.accessRequest = new SendAccessRequest();
if (this.password != null) {
const passwordHash = await this.cryptoFunctionService.pbkdf2(this.password, keyArray, 'sha256', 100000);
this.accessRequest.password = Utils.fromBufferToB64(passwordHash);
}
try {
let sendResponse: SendAccessResponse = null;
if (this.loading) {
sendResponse = await this.apiService.postSendAccess(this.id, this.accessRequest);
} else {
this.formPromise = this.apiService.postSendAccess(this.id, this.accessRequest);
sendResponse = await this.formPromise;
}
this.passwordRequired = false;
const sendAccess = new SendAccess(sendResponse);
this.decKey = await this.cryptoService.makeSendKey(keyArray);
this.send = await sendAccess.decrypt(this.decKey);
this.showText = this.send.text != null ? !this.send.text.hidden : true;
} catch (e) {
if (e instanceof ErrorResponse) {
if (e.statusCode === 401) {
this.passwordRequired = true;
} else if (e.statusCode === 404) {
this.unavailable = true;
} else {
this.error = true;
}
}
}
this.loading = false;
this.hideEmail = this.creatorIdentifier == null && !this.passwordRequired && !this.loading && !this.unavailable;
}
}

View File

@@ -0,0 +1,257 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="sendAddEditTitle">
<div class="modal-dialog modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate
autocomplete="off">
<div class="modal-header">
<h2 class="modal-title" id="sendAddEditTitle">{{title}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="send">
<app-callout *ngIf="disableSend">
<span>{{'sendDisabledWarning' | i18n}}</span>
</app-callout>
<app-callout *ngIf="!disableSend && disableHideEmail">
<span>{{'sendOptionsPolicyInEffect' | i18n}}</span>
<ul class="mb-0">
<li>{{'sendDisableHideEmailInEffect' | i18n}}</li>
</ul>
</app-callout>
<div class="row">
<div class="col-6 form-group">
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="send.name" required
[readOnly]="disableSend">
<small class="form-text text-muted">{{'sendNameDesc' | i18n}}</small>
</div>
</div>
<div class="row" *ngIf="!editMode">
<div class="col-6 form-group">
<label>{{'whatTypeOfSend' | i18n}}</label>
<div class="form-check" *ngFor="let o of typeOptions">
<input class="form-check-input" type="radio" [(ngModel)]="send.type" name="Type_{{o.value}}"
id="type_{{o.value}}" [value]="o.value" (change)="typeChanged(o)"
[checked]="send.type === o.value">
<label class="form-check-label" for="type_{{o.value}}">
{{o.name}}
</label>
</div>
</div>
</div>
<!-- Text -->
<ng-container *ngIf="send.type === sendType.Text">
<div class="form-group">
<label for="text">{{'sendTypeText' | i18n}}</label>
<textarea id="text" name="Text.Text" rows="6" [(ngModel)]="send.text.text" class="form-control"
[readOnly]="disableSend"></textarea>
<small class="form-text text-muted">{{'sendTextDesc' | i18n}}</small>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="send.text.hidden"
id="text-hidden" name="Text.Hidden" [disabled]="disableSend">
<label class="form-check-label" for="text-hidden">{{'textHiddenByDefault' | i18n}}</label>
</div>
</div>
</ng-container>
<!-- File -->
<ng-container *ngIf="send.type === sendType.File">
<div class="form-group">
<div *ngIf="editMode">
<strong class="d-block">{{'file' | i18n}}</strong>
{{send.file.fileName}} ({{send.file.sizeName}})
</div>
<div *ngIf="!editMode">
<label for="file">{{'file' | i18n}}</label>
<input type="file" id="file" class="form-control-file" name="file" required
[disabled]="disableSend">
<small class="form-text text-muted">{{'sendFileDesc' | i18n}} {{'maxFileSize' |
i18n}}</small>
</div>
</div>
</ng-container>
<h3 class="mt-5">{{'share' | i18n}}</h3>
<div class="form-group" *ngIf="link">
<label for="link">{{'sendLinkLabel' | i18n}}</label>
<input type="text" readonly id="link" name="Link" [(ngModel)]="link" class="form-control">
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="copyLink" id="copy-link"
name="CopyLink">
<label class="form-check-label" for="copy-link">{{'copySendLinkOnSave' | i18n}}</label>
</div>
</div>
<div id="options-header" class="section-header d-flex flex-row align-items-center mt-5"
(click)="toggleOptions()">
<h3 class="mb-0 mr-2">{{'options' | i18n}}</h3>
<a class="mb-1" href="#" appStopClick role="button">
<i class="fa" aria-hidden="true"
[ngClass]="{'fa-chevron-down': !showOptions, 'fa-chevron-up': showOptions}"></i>
</a>
</div>
<div id="options" [hidden]="!showOptions">
<div class="row">
<div class="col-6 form-group">
<label for="deletionDate">{{'deletionDate' | i18n}}</label>
<ng-template #deletionDateCustom>
<ng-container *ngIf="isDateTimeLocalSupported">
<input id="deletionDateCustom" class="form-control mt-1" type="datetime-local"
name="DeletionDate" [(ngModel)]="deletionDate" required
placeholder="MM/DD/YYYY HH:MM AM/PM" [readOnly]="disableSend">
</ng-container>
<div *ngIf="!isDateTimeLocalSupported" class="d-flex justify-content-around">
<input id="deletionDateCustomFallback" class="form-control mt-1" type="date"
name="DeletionDateFallback" [(ngModel)]="deletionDateFallback" required
placeholder="MM/DD/YYYY" [readOnly]="disableSend" data-date-format="mm/dd/yyyy">
<select *ngIf="isSafari" id="deletionTimeCustomFallback" class="form-control mt-1 ml-1" [required]="!editMode"
[(ngModel)]="safariDeletionTime" name="SafariDeletionTime">
<option *ngFor="let o of safariDeletionTimeOptions" [value]="o.military">{{o.standard}}</option>
</select>
<input *ngIf="!isSafari" id="deletionTimeCustomFallback" class="form-control mt-1 ml-1" type="time"
name="DeletionTimeDate" [(ngModel)]="deletionTimeFallback" required
placeholder="HH:MM AM/PM" [readOnly]="disableSend">
</div>
</ng-template>
<div *ngIf="!editMode">
<select id="deletionDate" name="DeletionDateSelect" [(ngModel)]="deletionDateSelect"
class="form-control" required>
<option *ngFor="let o of deletionDateOptions" [ngValue]="o.value">{{o.name}}
</option>
</select>
<ng-container *ngIf="deletionDateSelect === 0">
<ng-container *ngTemplateOutlet="deletionDateCustom">
</ng-container>
</ng-container>
</div>
<div *ngIf="editMode">
<ng-container *ngTemplateOutlet="deletionDateCustom">
</ng-container>
</div>
<div class="form-text text-muted small">{{'deletionDateDesc' | i18n}}</div>
</div>
<div class="col-6 form-group">
<div class="d-flex">
<label for="expirationDate">{{'expirationDate' | i18n}}</label>
<a href="#" appStopClick (click)="clearExpiration()" class="ml-auto"
*ngIf="editMode && !disableSend">
{{'clear' | i18n}}
</a>
</div>
<ng-template #expirationDateCustom>
<ng-container *ngIf="isDateTimeLocalSupported">
<input id="expirationDateCustom" class="form-control mt-1" type="datetime-local"
name="ExpirationDate" [(ngModel)]="expirationDate" placeholder="MM/DD/YYYY HH:MM AM/PM" [readOnly]="disableSend">
</ng-container>
<div class="d-flex justify-content-around" *ngIf="!isDateTimeLocalSupported">
<input id="expirationDateCustomFallback" class="form-control mt-1" type="date"
name="ExpirationDateFallback" [(ngModel)]="expirationDateFallback" [required]="!editMode"
placeholder="MM/DD/YYYY" [readOnly]="disableSend" data-date-format="mm/dd/yyyy" (change)="expirationDateFallbackChanged()">
<select *ngIf="isSafari" id="expirationTimeCustomFallback" class="form-control mt-1 ml-1" [required]="!editMode"
[(ngModel)]="safariExpirationTime" name="SafariExpirationTime">
<option *ngFor="let o of safariExpirationTimeOptions" [ngValue]="o.military">{{o.standard}}</option>
</select>
<input *ngIf="!isSafari" id="expirationTimeCustomFallback" class="form-control mt-1 ml-1" type="time"
name="ExpirationTimeFallback" [(ngModel)]="expirationTimeFallback" [required]="!editMode"
placeholder="HH:MM AM/PM" [readOnly]="disableSend">
</div>
</ng-template>
<div *ngIf="!editMode">
<select id="expirationDate" name="ExpirationDateSelect"
[(ngModel)]="expirationDateSelect" class="form-control" required>
<option *ngFor="let o of expirationDateOptions" [ngValue]="o.value">{{o.name}}
</option>
</select>
<ng-container *ngIf="expirationDateSelect === 0">
<ng-container *ngTemplateOutlet="expirationDateCustom">
</ng-container>
</ng-container>
</div>
<div *ngIf="editMode">
<ng-container *ngTemplateOutlet="expirationDateCustom">
</ng-container>
</div>
<div class="form-text text-muted small">{{'expirationDateDesc' | i18n}}</div>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="maxAccessCount">{{'maxAccessCount' | i18n}}</label>
<input id="maxAccessCount" class="form-control" type="number" name="MaxAccessCount"
[(ngModel)]="send.maxAccessCount" min="1" [readOnly]="disableSend">
<div class="form-text text-muted small">{{'maxAccessCountDesc' | i18n}}</div>
</div>
<div class="col-6 form-group" *ngIf="editMode">
<label for="accessCount">{{'currentAccessCount' | i18n}}</label>
<input id="accessCount" class="form-control" type="text" name="AccessCount" readonly
[(ngModel)]="send.accessCount">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="password" *ngIf="!hasPassword">{{'password' | i18n}}</label>
<label for="password" *ngIf="hasPassword">{{'newPassword' | i18n}}</label>
<div class="input-group">
<input id="password" class="form-control text-monospace"
type="{{showPassword ? 'text' : 'password'}}" name="Password" [(ngModel)]="password"
[readOnly]="disableSend">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePasswordVisible()">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
<div class="form-text text-muted small">{{'sendPasswordDesc' | i18n}}</div>
</div>
</div>
<div class="form-group">
<label for="notes">{{'notes' | i18n}}</label>
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="send.notes" class="form-control"
[readOnly]="disableSend"></textarea>
<div class="form-text text-muted small">{{'sendNotesDesc' | i18n}}</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="send.hideEmail" id="hideEmail"
name="HideEmail" [disabled]="(disableHideEmail && !send.hideEmail) || disableSend">
<label class="form-check-label" for="hideEmail">
{{'hideEmail' | i18n}}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="send.disabled" id="disabled"
name="Disabled" [disabled]="disableSend">
<label class="form-check-label" for="disabled">{{'disableThisSend' | i18n}}</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary disabled" disabled=true *ngIf="disableSend">
<span>{{'save' | i18n}}</span>
</button>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading" *ngIf="!disableSend">
<i class="fa fa-spinner fa-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" *ngIf="send">
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
[appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,33 @@
import { DatePipe } from '@angular/common';
import { Component } from '@angular/core';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { SendService } from 'jslib/abstractions/send.service';
import { UserService } from 'jslib/abstractions/user.service';
import { AddEditComponent as BaseAddEditComponent } from 'jslib/angular/components/send/add-edit.component';
@Component({
selector: 'app-send-add-edit',
templateUrl: 'add-edit.component.html',
})
export class AddEditComponent extends BaseAddEditComponent {
constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService, datePipe: DatePipe,
sendService: SendService, userService: UserService,
messagingService: MessagingService, policyService: PolicyService) {
super(i18nService, platformUtilsService, environmentService, datePipe, sendService, userService,
messagingService, policyService);
}
copyLinkToClipboard(link: string) {
// Copy function on web depends on the modal being open or not. Since this event occurs during a transition
// of the modal closing we need to add a small delay to make sure state of the DOM is consistent.
window.setTimeout(() => super.copyLinkToClipboard(link), 500);
}
}

View File

@@ -0,0 +1,142 @@
<div class="container page-content">
<div class="row card border-warning mb-4" *ngIf="disableSend">
<div class="card-header bg-warning text-white">
<i class="fa fa-warning fa-fw" aria-hidden="true"></i> {{'sendDisabled' | i18n}}
</div>
<div class="card-body">
<span>{{'sendDisabledWarning' | i18n}}</span>
</div>
</div>
<div class="row">
<div class="col-3 groupings">
<div class="card vault-filters">
<div class="card-header d-flex">
{{'filters' | i18n}}
</div>
<div class="card-body">
<input type="search" placeholder="{{searchPlaceholder || ('searchSends' | i18n)}}" id="search"
class="form-control" [(ngModel)]="searchText" (input)="searchTextChanged()" autocomplete="off"
appAutofocus>
<ul class="fa-ul card-ul">
<li [ngClass]="{active: selectedAll}">
<a href="#" appStopClick (click)="selectAll()">
<i class="fa-li fa fa-fw fa-th"></i>{{'allSends' | i18n}}
</a>
</li>
</ul>
<h3>{{'types' | i18n}}</h3>
<ul class="fa-ul card-ul">
<li [ngClass]="{active: selectedType === sendType.Text}">
<a href="#" appStopClick (click)="selectType(sendType.Text)">
<i class="fa-li fa fa-fw fa-file-text-o"></i>{{'sendTypeText' | i18n}}
</a>
</li>
<li [ngClass]="{active: selectedType === sendType.File}">
<a href="#" appStopClick (click)="selectType(sendType.File)">
<i class="fa-li fa fa-fw fa-file-o"></i>{{'sendTypeFile' | i18n}}
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-9">
<div class="page-header d-flex">
<h1>
{{'send' | i18n}}
<small #actionSpinner [appApiAction]="actionPromise">
<ng-container *ngIf="actionSpinner.loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</ng-container>
</small>
</h1>
<div class="ml-auto d-flex">
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addSend()"
[disabled]="disableSend">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'createSend' | i18n}}
</button>
</div>
</div>
<!--Listing Table-->
<table class="table table-hover table-list" *ngIf="filteredSends && filteredSends.length">
<tbody>
<tr *ngFor="let s of filteredSends">
<td class="table-list-icon">
<div class="icon" aria-hidden="true">
<i class="fa fa-fw fa-lg fa-file-o" *ngIf="s.type == sendType.File"></i>
<i class="fa fa-fw fa-lg fa-file-text-o" *ngIf="s.type == sendType.Text"></i>
</div>
</td>
<td class="reduced-lh wrap">
<a href="#" appStopClick appStopProp (click)="editSend(s)">{{s.name}}</a>
<ng-container *ngIf="s.disabled">
<i class="fa fa-warning" appStopProp title="{{'disabled' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'disabled' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.password">
<i class="fa fa-key" appStopProp title="{{'password' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'password' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.maxAccessCountReached">
<i class="fa fa-ban" appStopProp title="{{'maxAccessCountReached' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'maxAccessCountReached' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.expired">
<i class="fa fa-clock-o" appStopProp title="{{'expired' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'expired' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.pendingDelete">
<i class="fa fa-trash" appStopProp title="{{'pendingDeletion' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'pendingDeletion' | i18n}}</span>
</ng-container>
<br>
<small appStopProp>{{s.deletionDate | date:'medium'}}</small>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#" appStopClick (click)="copy(s)">
<i class="fa fa-fw fa-copy" aria-hidden="true"></i>
{{'copySendLink' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="removePassword(s)"
*ngIf="s.password && !disableSend">
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
{{'removePassword' | i18n}}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(s)">
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
{{'delete' | i18n}}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<div class="no-items" *ngIf="filteredSends && !filteredSends.length">
<ng-container *ngIf="!loaded">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</ng-container>
<ng-container *ngIf="loaded">
<p>{{'noSendsInList' | i18n}}</p>
<button (click)="addSend()" class="btn btn-outline-primary" [disabled]="disableSend">
<i class="fa fa-plus fa-fw"></i>{{'createSend' | i18n}}</button>
</ng-container>
</div>
</div>
</div>
</div>
<ng-template #sendAddEdit></ng-template>

View File

@@ -0,0 +1,103 @@
import {
Component,
ComponentFactoryResolver,
NgZone,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { SendView } from 'jslib/models/view/sendView';
import { SendComponent as BaseSendComponent } from 'jslib/angular/components/send/send.component';
import { AddEditComponent } from './add-edit.component';
import { ModalComponent } from '../modal.component';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { SendService } from 'jslib/abstractions/send.service';
import { UserService } from 'jslib/abstractions/user.service';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
const BroadcasterSubscriptionId = 'SendComponent';
@Component({
selector: 'app-send',
templateUrl: 'send.component.html',
})
export class SendComponent extends BaseSendComponent {
@ViewChild('sendAddEdit', { read: ViewContainerRef, static: true }) sendAddEditModalRef: ViewContainerRef;
modal: ModalComponent = null;
constructor(sendService: SendService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
ngZone: NgZone, searchService: SearchService, policyService: PolicyService, userService: UserService,
private componentFactoryResolver: ComponentFactoryResolver, private broadcasterService: BroadcasterService) {
super(sendService, i18nService, platformUtilsService, environmentService, ngZone, searchService,
policyService, userService);
}
async ngOnInit() {
await super.ngOnInit();
await this.load();
// Broadcaster subscription - load if sync completes in the background
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case 'syncCompleted':
if (message.successfully) {
await this.load();
}
break;
}
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
addSend() {
if (this.disableSend) {
return;
}
const component = this.editSend(null);
component.type = this.type;
}
editSend(send: SendView) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.sendAddEditModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<AddEditComponent>(
AddEditComponent, this.sendAddEditModalRef);
childComponent.sendId = send == null ? null : send.id;
childComponent.onSavedSend.subscribe(async (s: SendView) => {
this.modal.close();
await this.load();
});
childComponent.onDeletedSend.subscribe(async (s: SendView) => {
this.modal.close();
await this.load();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
return childComponent;
}
}

View File

@@ -153,6 +153,15 @@ export class EventService {
case EventType.OrganizationUser_UpdatedGroups:
msg = this.i18nService.t('editedGroupsForUser', this.formatOrgUserId(ev));
break;
case EventType.OrganizationUser_UnlinkedSso:
msg = this.i18nService.t('unlinkedSsoUser', this.formatOrgUserId(ev));
break;
case EventType.OrganizationUser_ResetPassword_Enroll:
msg = this.i18nService.t('eventEnrollPasswordReset', this.formatOrgUserId(ev));
break;
case EventType.OrganizationUser_ResetPassword_Withdraw:
msg = this.i18nService.t('eventWithdrawPasswordReset', this.formatOrgUserId(ev));
break;
// Org
case EventType.Organization_Updated:
msg = this.i18nService.t('editedOrgSettings');
@@ -165,6 +174,11 @@ export class EventService {
msg = this.i18nService.t('exportedOrganizationVault');
break;
*/
// Policies
case EventType.Policy_Updated:
msg = this.i18nService.t('modifiedPolicy', this.formatPolicyId(ev));
break;
default:
break;
}
@@ -251,6 +265,13 @@ export class EventService {
return a.outerHTML;
}
private formatPolicyId(ev: EventResponse) {
const shortId = this.getShortId(ev.policyId);
const a = this.makeAnchor(shortId);
a.setAttribute('href', '#/organizations/' + ev.organizationId + '/manage/policies?policyId=' + ev.policyId);
return a.outerHTML;
}
private makeAnchor(shortId: string) {
const a = document.createElement('a');
a.title = this.i18nService.t('view');

View File

@@ -7,20 +7,32 @@ import {
import { UserService } from 'jslib/abstractions/user.service';
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
import { Permissions } from 'jslib/enums/permissions';
@Injectable()
export class OrganizationTypeGuardService implements CanActivate {
constructor(private userService: UserService, private router: Router) { }
async canActivate(route: ActivatedRouteSnapshot) {
const org = await this.userService.getOrganization(route.parent.params.organizationId);
const allowedTypes = route.data == null ? null : route.data.allowedTypes as OrganizationUserType[];
if (allowedTypes == null || allowedTypes.indexOf(org.type) === -1) {
this.router.navigate(['/organizations', org.id]);
return false;
const org = await this.userService.getOrganization(route.params.organizationId);
const permissions = route.data == null ? null : route.data.permissions as Permissions[];
if (
(permissions.indexOf(Permissions.AccessBusinessPortal) !== -1 && org.canAccessBusinessPortal) ||
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && org.canAccessEventLogs) ||
(permissions.indexOf(Permissions.AccessImportExport) !== -1 && org.canAccessImportExport) ||
(permissions.indexOf(Permissions.AccessReports) !== -1 && org.canAccessReports) ||
(permissions.indexOf(Permissions.ManageAllCollections) !== -1 && org.canManageAllCollections) ||
(permissions.indexOf(Permissions.ManageAssignedCollections) !== -1 && org.canManageAssignedCollections) ||
(permissions.indexOf(Permissions.ManageGroups) !== -1 && org.canManageGroups) ||
(permissions.indexOf(Permissions.ManageOrganization) !== -1 && org.isOwner) ||
(permissions.indexOf(Permissions.ManagePolicies) !== -1 && org.canManagePolicies) ||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && org.canManageUsers)
) {
return true;
}
return true;
this.router.navigate(['/organizations', org.id]);
return false;
}
}

View File

@@ -16,7 +16,7 @@ export class RouterService {
constructor(private router: Router, private activatedRoute: ActivatedRoute,
private titleService: Title, i18nService: I18nService) {
this.currentUrl = this.router.url;
router.events.subscribe((event) => {
router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.previousUrl = this.currentUrl;
this.currentUrl = event.url;

View File

@@ -16,32 +16,34 @@ import { EventService } from './event.service';
import { OrganizationGuardService } from './organization-guard.service';
import { OrganizationTypeGuardService } from './organization-type-guard.service';
import { RouterService } from './router.service';
import { UnauthGuardService } from './unauth-guard.service';
import { AuthGuardService } from 'jslib/angular/services/auth-guard.service';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
import { LockGuardService } from 'jslib/angular/services/lock-guard.service';
import { UnauthGuardService } from 'jslib/angular/services/unauth-guard.service';
import { ValidationService } from 'jslib/angular/services/validation.service';
import { Analytics } from 'jslib/misc/analytics';
import { ApiService } from 'jslib/services/api.service';
import { AppIdService } from 'jslib/services/appId.service';
import { AuditService } from 'jslib/services/audit.service';
import { AuthService } from 'jslib/services/auth.service';
import { CipherService } from 'jslib/services/cipher.service';
import { CollectionService } from 'jslib/services/collection.service';
import { ConsoleLogService } from 'jslib/services/consoleLog.service';
import { ConstantsService } from 'jslib/services/constants.service';
import { ContainerService } from 'jslib/services/container.service';
import { CryptoService } from 'jslib/services/crypto.service';
import { EnvironmentService } from 'jslib/services/environment.service';
import { EventService as EventLoggingService } from 'jslib/services/event.service';
import { ExportService } from 'jslib/services/export.service';
import { FileUploadService } from 'jslib/services/fileUpload.service';
import { FolderService } from 'jslib/services/folder.service';
import { ImportService } from 'jslib/services/import.service';
import { NotificationsService } from 'jslib/services/notifications.service';
import { PasswordGenerationService } from 'jslib/services/passwordGeneration.service';
import { PolicyService } from 'jslib/services/policy.service';
import { SearchService } from 'jslib/services/search.service';
import { SendService } from 'jslib/services/send.service';
import { SettingsService } from 'jslib/services/settings.service';
import { StateService } from 'jslib/services/state.service';
import { SyncService } from 'jslib/services/sync.service';
@@ -62,6 +64,7 @@ import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib
import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib/abstractions/environment.service';
import { EventService as EventLoggingServiceAbstraction } from 'jslib/abstractions/event.service';
import { ExportService as ExportServiceAbstraction } from 'jslib/abstractions/export.service';
import { FileUploadService as FileUploadServiceAbstraction } from 'jslib/abstractions/fileUpload.service';
import { FolderService as FolderServiceAbstraction } from 'jslib/abstractions/folder.service';
import { I18nService as I18nServiceAbstraction } from 'jslib/abstractions/i18n.service';
import { ImportService as ImportServiceAbstraction } from 'jslib/abstractions/import.service';
@@ -74,6 +77,7 @@ import {
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib/abstractions/platformUtils.service';
import { PolicyService as PolicyServiceAbstraction } from 'jslib/abstractions/policy.service';
import { SearchService as SearchServiceAbstraction } from 'jslib/abstractions/search.service';
import { SendService as SendServiceAbstraction } from 'jslib/abstractions/send.service';
import { SettingsService as SettingsServiceAbstraction } from 'jslib/abstractions/settings.service';
import { StateService as StateServiceAbstraction } from 'jslib/abstractions/state.service';
import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/storage.service';
@@ -92,8 +96,10 @@ const storageService: StorageServiceAbstraction = new HtmlStorageService(platfor
const secureStorageService: StorageServiceAbstraction = new MemoryStorageService();
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new WebCryptoFunctionService(window,
platformUtilsService);
const consoleLogService = new ConsoleLogService(false);
const cryptoService = new CryptoService(storageService,
platformUtilsService.isDev() ? storageService : secureStorageService, cryptoFunctionService);
platformUtilsService.isDev() ? storageService : secureStorageService, cryptoFunctionService, platformUtilsService,
consoleLogService);
const tokenService = new TokenService(storageService);
const appIdService = new AppIdService(storageService);
const apiService = new ApiService(tokenService, platformUtilsService,
@@ -101,61 +107,56 @@ const apiService = new ApiService(tokenService, platformUtilsService,
const userService = new UserService(tokenService, storageService);
const settingsService = new SettingsService(userService, storageService);
export let searchService: SearchService = null;
const fileUploadService = new FileUploadService(consoleLogService, apiService);
const cipherService = new CipherService(cryptoService, userService, settingsService,
apiService, storageService, i18nService, () => searchService);
apiService, fileUploadService, storageService, i18nService, () => searchService);
const folderService = new FolderService(cryptoService, userService, apiService, storageService,
i18nService, cipherService);
const collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
searchService = new SearchService(cipherService);
searchService = new SearchService(cipherService, consoleLogService, i18nService);
const policyService = new PolicyService(userService, storageService);
const sendService = new SendService(cryptoService, userService, apiService, fileUploadService, storageService,
i18nService, cryptoFunctionService);
const vaultTimeoutService = new VaultTimeoutService(cipherService, folderService, collectionService,
cryptoService, platformUtilsService, storageService, messagingService, searchService, userService, tokenService,
null, async () => messagingService.send('logout', { expired: false }));
const syncService = new SyncService(userService, apiService, settingsService,
folderService, cipherService, cryptoService, collectionService, storageService, messagingService, policyService,
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
sendService, async (expired: boolean) => messagingService.send('logout', { expired: expired }));
const passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, policyService);
const totpService = new TotpService(storageService, cryptoFunctionService);
const containerService = new ContainerService(cryptoService);
const authService = new AuthService(cryptoService, apiService,
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService);
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService,
consoleLogService);
const exportService = new ExportService(folderService, cipherService, apiService);
const importService = new ImportService(cipherService, folderService, apiService, i18nService, collectionService);
const importService = new ImportService(cipherService, folderService, apiService, i18nService, collectionService,
platformUtilsService);
const notificationsService = new NotificationsService(userService, syncService, appIdService,
apiService, vaultTimeoutService, async () => messagingService.send('logout', { expired: true }));
apiService, vaultTimeoutService, async () => messagingService.send('logout', { expired: true }), consoleLogService);
const environmentService = new EnvironmentService(apiService, storageService, notificationsService);
const auditService = new AuditService(cryptoFunctionService, apiService);
const eventLoggingService = new EventLoggingService(storageService, apiService, userService, cipherService);
const analytics = new Analytics(window, () => platformUtilsService.isDev() || platformUtilsService.isSelfHost(),
platformUtilsService, storageService, appIdService);
containerService.attachToWindow(window);
export function initFactory(): Function {
return async () => {
await (storageService as HtmlStorageService).init();
const isDev = platformUtilsService.isDev();
if (!isDev && platformUtilsService.isSelfHost()) {
if (isDev || platformUtilsService.isSelfHost()) {
environmentService.baseUrl = window.location.origin;
} else {
environmentService.notificationsUrl = isDev ? 'http://localhost:61840' :
'https://notifications.bitwarden.com'; // window.location.origin + '/notifications';
environmentService.enterpriseUrl = isDev ? 'http://localhost:52313' :
'https://portal.bitwarden.com'; // window.location.origin + '/portal';
environmentService.notificationsUrl = 'https://notifications.bitwarden.com';
environmentService.enterpriseUrl = 'https://portal.bitwarden.com';
}
apiService.setUrls({
base: isDev ? null : window.location.origin,
api: isDev ? 'http://localhost:4000' : null,
identity: isDev ? 'http://localhost:33656' : null,
events: isDev ? 'http://localhost:46273' : null,
// Uncomment these (and comment out the above) if you want to target production
// servers for local development.
// base: null,
// api: 'https://api.bitwarden.com',
// identity: 'https://identity.bitwarden.com',
// events: 'https://events.bitwarden.com',
base: window.location.origin,
api: null,
identity: null,
events: null,
});
setTimeout(() => notificationsService.init(environmentService), 3000);
@@ -190,6 +191,7 @@ export function initFactory(): Function {
UnauthGuardService,
RouterService,
EventService,
LockGuardService,
{ provide: AuditServiceAbstraction, useValue: auditService },
{ provide: AuthServiceAbstraction, useValue: authService },
{ provide: CipherServiceAbstraction, useValue: cipherService },
@@ -203,6 +205,7 @@ export function initFactory(): Function {
{ provide: PlatformUtilsServiceAbstraction, useValue: platformUtilsService },
{ provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService },
{ provide: ApiServiceAbstraction, useValue: apiService },
{ provide: FileUploadServiceAbstraction, useValue: fileUploadService },
{ provide: SyncServiceAbstraction, useValue: syncService },
{ provide: UserServiceAbstraction, useValue: userService },
{ provide: MessagingServiceAbstraction, useValue: messagingService },
@@ -218,6 +221,7 @@ export function initFactory(): Function {
{ provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService },
{ provide: EventLoggingServiceAbstraction, useValue: eventLoggingService },
{ provide: PolicyServiceAbstraction, useValue: policyService },
{ provide: SendServiceAbstraction, useValue: sendService },
{
provide: APP_INITIALIZER,
useFactory: initFactory,

Some files were not shown because too many files have changed in this diff Show More