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

Compare commits

...

360 Commits

Author SHA1 Message Date
Oscar Hinton
d566c963c1 Bump version to 2.20.3 (#989) 2021-05-21 15:55:41 +02:00
Oscar Hinton
1098adc03d Correctly handle dash in locale, and add a fallback to en. (#988) 2021-05-21 13:07:33 +02:00
Thomas Rittson
e34e4728d0 Fix accessibility (a11y) on swal2 modals (#986)
* Remove tabindex on bootstrap modals if swal open

* fix linting
2021-05-21 06:52:44 +10:00
Matt Gibson
35346613d8 Version bump for org search hot fix (#985) 2021-05-19 13:13:55 -05:00
Oscar Hinton
0fd89e06c6 Rename Ciphers -> Items in trash cleanup message (#984) 2021-05-19 19:42:06 +02:00
Matt Gibson
1c5ce23d35 Set search index for limited collection org users (#983) 2021-05-19 11:11:11 -05:00
Oscar Hinton
45c31aa089 Bulk remove organization users (#970)
* Add support for bulk removal of org users

* Rename to UserBulkDeleteRequest

* Use OrganizationUserBulkRequest

* Bump jslib

* Fix linting
2021-05-18 10:27:52 +02:00
Vince Grassia
34be07c220 Pin versions of actions in workflow (#980) 2021-05-17 11:18:45 -04:00
Oscar Hinton
968a255269 Correctly handle errors on remove and reinvite of organization users (#979) 2021-05-17 15:13:26 +02:00
Oscar Hinton
a27be135da Change all remaining modals to be scrollable (#976)
* Change all remaining modals to be scrollable

* Fix password-generator-history and two-factor-options not using modal-body

* Remove modal-dialog-scrollable on two-factor-setup components
2021-05-14 21:03:45 +02:00
Oscar Hinton
bb95eb84ea Bump node to 14 (#955)
* Bump node to 14

* Update Readme

* Change engine to ~14

* Bump jslib

* Remove @angular/localize since it's not used
2021-05-14 20:08:03 +02:00
Matt Gibson
54cd5a68b3 Add event export (#967)
* Include human readable export message on events

* Add export currently visible events.

* PR feedback
2021-05-13 18:39:53 -05:00
Trey Greer
9abdefa947 Added additional languages (#975) 2021-05-13 17:08:27 -04:00
Kyle Spearrin
d9322c1307 use swal titletext to avoid XSS (#966) 2021-05-13 10:08:16 -04:00
Leon-Hikari
a8d614628a Adds folder word-wrap (#880)
Uses spaces and dashes as preferred separator in folder names
(instead of just breaking whereever the max width is encountered)
2021-05-13 14:02:39 +10:00
Thomas Rittson
7f9f6d3d0e Check encKey when importing encrypted JSON (#968)
* Check encKey when importing encrypted JSON

* bump jslib
2021-05-13 11:22:26 +10:00
Vince Grassia
4c1e36462c Fix docker tag version in workflow (#973) 2021-05-12 17:24:18 -04:00
Chad Scharf
32d04106a1 Version bump, 2.20.1 (#971) 2021-05-12 12:58:21 -04:00
Oscar Hinton
a3506e833a Change WebAuthn connectors from using inline onclick to external (#969) 2021-05-12 17:19:20 +02:00
Oscar Hinton
51f3fee75d Bulk re-invite of org users (#961)
* Add support for bulk re-invite of org users

* Add selectAll, resolve review comments
2021-05-12 16:38:17 +02:00
Thomas Rittson
3ac2ce079a Refactor Send 'copy link' functionality (#960)
* Refactor Send 'copy link' functionality

* bump jslib

* Print debug message if copyToClipboard fails

* fix linting
2021-05-12 10:51:12 +10:00
Daniel James Smith
97e1c7a2ea Fix typo in webAuthnAuthenticate (#964) 2021-05-11 17:19:55 -04:00
Joseph Flinn
29f741316c fixing docker push (#965)
* fixing docker push

* adding in the the missed vars
2021-05-11 12:46:14 -07:00
Joseph Flinn
293ae12e33 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:02:31 -07:00
Kyle Spearrin
49d1c135db 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:17 -04:00
Oscar Hinton
d900d2d3f8 Change modal-dialog for add-edit cipher to be scrollable (#957) 2021-05-07 09:43:41 +02:00
Oscar Hinton
4a61f0ac04 Cleanup tsconfig (#954)
* Cleanup tsconfig

* Removed dummy module
2021-05-05 09:46:14 +02:00
Oscar Hinton
b1635debcc Password reprompt (#929)
* Use passwordRepromptService

* Rename passwordPrompt to reprompt. Protect bulk actions

* Change card to hidden, minor refactor.

* Explicit reprompt value check

* Ensure locales are the same on all platforms

* Move showPasswordDialog to platformutils

* Fix sweet alert validation message margin

* Update locale to be the same as browser
2021-05-03 20:55:42 +02:00
Thomas Rittson
b3a4f833a1 Fix "copy link to clipboard" for large file Sends (#949)
* Throw error if execCommand('copy') is disabled

* Use dialog for file Send creation success

* Show popup modal after long Send file uploads

* fix linting

* bump jslib
2021-04-28 07:40:36 +10:00
Oscar Hinton
dd56c9bc87 Add auto delete warning to trash page (#953)
* Add warning to trash page
2021-04-27 18:49:02 +02:00
Matt Gibson
19f92e74f5 Update jslib (#952) 2021-04-26 16:58:36 -05:00
Oscar Hinton
d71d0d9af6 Improve WebAuthn error detection for invalid data (#946) 2021-04-23 21:07:15 +02:00
Matt Gibson
2392d34ed8 Update jslib (#947) 2021-04-23 14:02:42 -05:00
Matt Gibson
f6eec08b70 Specify organization id as the indexing entity (#945)
* Specify organization id as the indexing entity

* Update jslib
2021-04-23 09:41:10 -05:00
Oscar Hinton
9547b72566 Bump dependencies (#936)
* Bump dependencies
2021-04-22 21:29:29 +02:00
Vincent Salucci
38097c40d8 [Version] Bumped to 2.20.0 (#944)
* [Version] Bumped to 2.20.0

* Updated package-lock version
2021-04-22 11:53:20 -05:00
Vincent Salucci
66b7f4d344 [Reset Password] Feature Flag (#943) 2021-04-22 09:43:51 -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
Chad Scharf
d823e8522c Bump version to 2.16.2 (#668) 2020-10-09 10:49:56 -04:00
paulussujono
6bc5ac46b7 ️ added autofocus on first field of modal forms (#667)
added to modals:
- invite user
- add item
- add collection
- add folder
2020-10-06 09:06:44 -04:00
Kyle Spearrin
1193a93f86 map en-IN 2020-09-28 14:21:11 -04:00
Addison Beck
4cd052e009 Default selection plan upgrade fix (#658)
* fixed a broken default selection for plan upgrades

* added a semicolon
2020-09-18 14:15:24 -04:00
Kyle Spearrin
949f61f1a4 bump version 2020-09-15 17:04:21 -04:00
Kyle Spearrin
2145c3f88c language updates 2020-09-15 13:38:46 -04:00
Kyle Spearrin
bb71d5dc0a New Crowdin updates (#655)
* New translations messages.json (French)

* New translations messages.json (Portuguese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Polish)

* New translations messages.json (Bulgarian)

* New translations messages.json (Dutch)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (Finnish)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Catalan)

* New translations messages.json (Portuguese, Brazilian)
2020-09-15 12:55:23 -04:00
Chad Scharf
41856ff6af 653 - fix user agent detection for Edge (#654)
* 653 - fix user agent detection for Edge

* Update edge detection to only new version

* update jslib

* update jslib

* fix jslib ref constructor
2020-09-15 10:31:12 -04:00
Addison Beck
a1388ddab7 fixed the cvc learn more link in the payment component (#652) 2020-09-14 15:53:24 -04:00
Kyle Spearrin
b2d13f586d New Crowdin updates (#651)
* New translations messages.json (French)

* New translations messages.json (Portuguese, Brazilian)

* 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 (Croatian)

* New translations messages.json (Russian)

* 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 (Slovak)

* New translations messages.json (Portuguese)

* New translations messages.json (Spanish)

* New translations messages.json (German)

* 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 (Polish)

* 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 (Norwegian Bokmal)
2020-09-14 10:49:58 -04:00
Kyle Spearrin
9f0cd586ee only use memory storage for vault data keys (#650)
* only use memory storage for vault data keys

* add lastSync_ to memory storage
2020-09-14 08:35:53 -04:00
Addison Beck
ce67497d3a added localization variable for link sso (#648) 2020-09-11 14:22:56 -04:00
Kyle Spearrin
0dc26e589a New Crowdin updates (#646)
* New translations messages.json (Spanish)

* New translations messages.json (Catalan)

* New translations messages.json (Danish)

* New translations messages.json (German)

* New translations messages.json (Hungarian)

* New translations messages.json (Italian)

* New translations messages.json (Dutch)

* New translations messages.json (Polish)

* New translations messages.json (Portuguese)

* New translations messages.json (Russian)

* New translations messages.json (Swedish)

* New translations messages.json (Ukrainian)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Estonian)

* New translations messages.json (English, United Kingdom)
2020-09-09 16:10:35 -04:00
Kyle Spearrin
e14a676eea switch from session storage to memory storage (#644) 2020-09-08 13:47:20 -04:00
Chad Scharf
11cf89493d form promise added for sso prevalidation (#643) 2020-09-08 12:18:13 -04:00
Kyle Spearrin
5be121ec71 New Crowdin updates (#642)
* New translations messages.json (French)

* New translations messages.json (Portuguese, Brazilian)

* 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 (Croatian)

* New translations messages.json (Russian)

* 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 (Slovak)

* New translations messages.json (Portuguese)

* New translations messages.json (Spanish)

* New translations messages.json (German)

* 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 (Polish)

* 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 (Norwegian Bokmal)
2020-09-08 11:29:01 -04:00
Kyle Spearrin
95e58b5e69 update jslib 2020-09-08 11:25:04 -04:00
Chad Scharf
506fd22280 Update jslib - sso pre-validation (#641) 2020-09-08 10:41:24 -04:00
Addison Beck
d79b12dedc updated jslib (#640) 2020-09-08 09:26:50 -04:00
Chad Scharf
599cd7299c bump version (#637) 2020-09-05 21:24:45 -04:00
Addison Beck
18d26b79af updated jslib (#636) 2020-09-04 16:05:42 -04:00
Addison Beck
1f81b81a58 updated jslib (#635) 2020-09-04 14:18:09 -04:00
Kyle Spearrin
cc5e420484 adjust margins 2020-09-04 12:07:14 -04:00
Chad Scharf
b4eaa48765 Updated favicon to new standard (#634) 2020-09-03 17:02:53 -04:00
Addison Beck
76354741be Filter out custom plans from consideration on org create (#631) 2020-09-02 15:53:45 -04:00
Chad Scharf
1b466609f0 SSO pre-validation messages (#628) 2020-08-31 16:48:09 -04:00
Kyle Spearrin
7e11b8bb5a disable certain org settings fields when selfhost (#627) 2020-08-28 11:22:30 -04:00
Vincent Salucci
b251e1f73c [SSO] Add set-password loading placeholder (#626)
* Preparing for new jslib // removed resetMasterPassword variable // Added sync service

* initial commit of loading set password

* Update jslib (e55528e -> 700e945)

* center justify text

* Reverted testing data
2020-08-28 08:56:51 -05:00
Kyle Spearrin
fa11382c08 adjust paths to portal 2020-08-27 16:12:20 -04:00
Addison Beck
e17a49acd5 Sso link existing user (#616)
* created and applied link-sso component

* implemented linking existing user to sso

* removed an unused import

* created and applied link-sso component

* implemented linking existing user to sso

* removed an unused import

* merge

* added a token to the sso linking flow

* [jslib] Update (5d874d0 -> 6ab444a) (#618)

* Update jslib (5d874d0 -> 6ab444a)

* Update dependency flows

* created and applied link-sso component

* implemented linking existing user to sso

* removed an unused import

* merge

* added a token to the sso linking flow

* implemented linking existing user to sso

* removed an unused import

* account for some variable shakeup in jslib for link sso

* updated jslib

* updated jslib

* still trying to fix jslib

* finally, really, truly updated jslib

Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
2020-08-27 11:44:04 -04:00
Addison Beck
bc71ffa6f2 Product description updates (#625)
* updated product messages on org create

* formatted messages.json

* formatted messages.json
2020-08-26 14:44:15 -04:00
Kyle Spearrin
95dc3c92c5 few fixes to plan changes (#624) 2020-08-25 14:21:03 -04:00
Kyle Spearrin
2135accaf4 yoti web support (#623) 2020-08-25 09:25:22 -04:00
Vincent Salucci
429c38fc66 [jslib] Update (5d874d0 -> 6ab444a) (#618)
* Update jslib (5d874d0 -> 6ab444a)

* Update dependency flows
2020-08-21 13:40:48 -05:00
Kyle Spearrin
56e92b1695 cleanup various sso tasks (#617) 2020-08-20 16:39:05 -04:00
Matt Smith
b2685d455b Modifications made to support Browser Extension SSO (#605)
* Update feature/sso jslib 261a200 -> 2e823ea (#589)

* [SSO] Reset master password  (#580)

* Initial commit reset master password (sso)

* Reverted order of two factor/reset password conditional

* Added necessary resetMasterPassword flag for potential entry into RMP flow

* Complete Revamp: Reverted Register // Deleted reset-master-password // updated sso/(settings)change password to use use super class // Adjust routing/messages // Created (accounts) change-password

* Updated button -> Set Master Password

* Refactored change password sub classes to use new submit pattern

* Cleaned import statements

* Update jslib (7fa5178 -> fe167be)

* Update jslib fe167be - >34632e5

* Fixed sso base class import

* merge master

* Fixed missing semicolon // updated jslib to whats in feature/sso

* Fixed two factor formatting

* Added new change password component to app module

* Updated component selector

* updating jslib 34632e5 -> 2e823ea

* Fixed lint warning in two-factor component

Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com>

* Update jslib to 101c568 (#594)

* Support for dynamic clientid (#595)

* support third party sso clients

* jslib update

* update jslib

* Modifications made for Browser Extension SSO

* Brought web specific ssocomponent into module

* Removed sso complete transition

* Fixed remaining merge issues

* Removed un-needed block of code.

* Moved processing to sso-connector.

* Removed unused import

* Fixed curly braces..

* Linter fixes

* Aligned verbiage for process message handler

* Lintr fixes

* Firefox can't handle closing the window this way.

* Update sso.ts

Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com>
Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
2020-08-20 14:30:22 -05:00
Kyle Spearrin
abfd1fa254 abstract set password to jslib (#614) 2020-08-19 11:15:04 -04:00
Oscar Hinton
24a5717e27 Fix @ngtools/webpack version (#613) 2020-08-18 16:19:20 -04:00
Kyle Spearrin
9d9503b00e lock duo sdk to specific commit 2020-08-18 11:28:21 -04:00
Kyle Spearrin
7b0579ccf3 update ngtools for webpack 2020-08-18 11:09:37 -04:00
Kyle Spearrin
df84dff54f jquery types updates 2020-08-18 10:57:42 -04:00
Kyle Spearrin
367c09f7e6 update tsnode 2020-08-18 10:52:45 -04:00
Kyle Spearrin
46967dc126 node types to resolve iterator error 2020-08-18 10:05:19 -04:00
Kyle Spearrin
e0ede7ba74 call api to set password with key parameters (#609)
* call api to set password with key parameters

* update ssoCompleteRegistration string
2020-08-17 15:04:59 -04:00
Oscar Hinton
1fe7554818 Upgrade Angular CDK (#610) 2020-08-17 12:14:55 -04:00
Oscar Hinton
eff3332fef Upgrade Angular to 9 (#606)
* Upgrade Angular to 8

* Upgrade Angular to 9

* Fix format

* Fix import sorting
2020-08-17 10:04:38 -04:00
Kyle Spearrin
caea4775b3 SSO feature (#604)
* Update feature/sso jslib 261a200 -> 2e823ea (#589)

* [SSO] Reset master password  (#580)

* Initial commit reset master password (sso)

* Reverted order of two factor/reset password conditional

* Added necessary resetMasterPassword flag for potential entry into RMP flow

* Complete Revamp: Reverted Register // Deleted reset-master-password // updated sso/(settings)change password to use use super class // Adjust routing/messages // Created (accounts) change-password

* Updated button -> Set Master Password

* Refactored change password sub classes to use new submit pattern

* Cleaned import statements

* Update jslib (7fa5178 -> fe167be)

* Update jslib fe167be - >34632e5

* Fixed sso base class import

* merge master

* Fixed missing semicolon // updated jslib to whats in feature/sso

* Fixed two factor formatting

* Added new change password component to app module

* Updated component selector

* updating jslib 34632e5 -> 2e823ea

* Fixed lint warning in two-factor component

Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com>

* Update jslib to 101c568 (#594)

* Support for dynamic clientid (#595)

* support third party sso clients

* jslib update

* update jslib

* Update change-password.component.ts

* Update sso.component.ts

* Update app.module.ts

Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
2020-08-13 14:32:07 -04:00
Addison Beck
5f04950358 Price and Plan Updates (#598)
* added the multi select checkbox to org ciphers

* wired up select all/none

* allowed for bulk delete of ciphers from the org vault

* refactored bulk actions into a dedicated component

* tweaked formatting settings and reformatted files

* moved some shared code to jslib

* some more formatting fixes

* undid jslib connection changes

* removed a function that was moved to jslib

* reset jslib again?

* set up delete many w/admin cipher methods

* removed extra href tags

* added organization id to bulk delete request model when coming from an org vault

* fixed up some compiler warnings for formatting

* updated organization create component to pull list of plans from static store

* wired up the organization create page to new data struct

* continued work on plan updates

* accounted for the subscription screen in plan updates

* adjusted for code review changes from server PR for plan updates

* cleaned up linter errors

* changed a few variable names

* moved price information, added sales tax and subtotal labels

* code review fixups for bulk delete from org vault

* added back a removed parameter from the vault component

* seperated some imports with newlines

* updated jslib

* resolved some build errors

* updated names to reflect server name changes for plan updates

* adjusted logic for using annual total for annual prices in server model

* rearranged an import for the linter

* broke up an async call

* updated organization create component to pull list of plans from static store

* wired up the organization create page to new data struct

* continued work on plan updates

* accounted for the subscription screen in plan updates

* adjusted for code review changes from server PR for plan updates

* cleaned up linter errors

* changed a few variable names

* moved price information, added sales tax and subtotal labels

* updated names to reflect server name changes for plan updates

* adjusted logic for using annual total for annual prices in server model

* rearranged an import for the linter

* broke up an async call

* resolved merge fun

* updated jslib

* made plans a public variable

* removed sales tax hooks

* added a getter for selected plan interval

* went a little too crazy with the interval getter

* formatting

* added a semicolon

* updated jslib

Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>
2020-08-12 17:16:38 -04:00
Kyle Spearrin
c46af91240 implement identifier update in org settings (#601) 2020-08-12 16:46:18 -04:00
Oscar Hinton
f5034effd2 Upgrade TypeScript (#600)
* Upgrade typescript to 3.6.5.

* Resolve compile error and warnings
2020-08-12 15:43:26 -04:00
Addison Beck
20408347fb Allow Bulk Delete In Org Vault (#577)
* added the multi select checkbox to org ciphers

* wired up select all/none

* allowed for bulk delete of ciphers from the org vault

* refactored bulk actions into a dedicated component

* tweaked formatting settings and reformatted files

* moved some shared code to jslib

* some more formatting fixes

* undid jslib connection changes

* removed a function that was moved to jslib

* reset jslib again?

* set up delete many w/admin cipher methods

* removed extra href tags

* added organization id to bulk delete request model when coming from an org vault

* fixed up some compiler warnings for formatting

* code review fixups for bulk delete from org vault

* added back a removed parameter from the vault component

* seperated some imports with newlines

* updated jslib

* resolved some build errors

* code review cleanup for bulk delete from an org vault

* code review cleanup for bulk delete from an org vault

* code review cleanup for bulk delete from an org vault

* code review cleanup for bulk delete from an org vault

* updated jslib to latest

Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>
2020-08-11 11:30:30 -04:00
Kyle Spearrin
49d5bfd3e7 update jslib 2020-08-03 15:25:24 -04:00
Vincent Salucci
e99d1a74fd update jslib (f301b92 -> 101c568) (#593) 2020-08-03 07:56:47 -05:00
Vincent Salucci
43d1cede98 update jslib (261a200 -> f301b92) (#590) 2020-08-01 17:18:18 -05:00
Vincent Salucci
091fc93645 update jslib (fe167be -> 261a200) (#588) 2020-07-31 13:54:13 -05:00
Kyle Spearrin
dfe2771ba7 Taiwan 2020-07-31 06:38:25 -04:00
Kyle Spearrin
d3664321fd fix download link 2020-07-28 22:52:13 -04:00
Chad Scharf
2e01ff7826 Fix modal-body div missing #583 (#585)
* Fix modal-body div missing #583

* Revert "Fix modal-body div missing #583"

This reverts commit 38f0cae82d.

* Fixing modal-body div missing #583
2020-07-28 09:55:59 -04:00
Vincent Salucci
59d5a7439d Update jslib 7fa5178 -> fe167be (#584) 2020-07-27 13:20:13 -05:00
K. Sasa
6e3edd75eb Consistent: Replaced the clipboard icon with a clone icon to improve UX (#582)
* Replace copy value button fa-clipboard to fa-clone

* Replace clone item button fa-clone to fa-files-o
2020-07-27 13:21:11 -04:00
Oscar Hinton
78992444bf Support biometric changes in jslib (#571) 2020-07-24 14:39:39 -04:00
Chad Scharf
f1dea8fb1a Transition reference id to data (#578)
* Transition reference id to data

* reference event request model change
2020-07-21 10:43:38 -04:00
Kyle Spearrin
04e5ab0d01 update jslib 2020-07-16 10:56:54 -04:00
Kyle Spearrin
22a1cef498 SSO support (#575)
* support for sso

* resetMasterPassword

* update jslib

* [Enterprise] Added button to launch portal (#570)

* initial commit

* Added Enterprise button and used new business portal bool

* Reverting services module local changes

* Formatted some new lines

* Closed alerts on lock (#572)

Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>

* Updated enterprise URL dev (port) (#574)

Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>
2020-07-16 09:18:25 -04:00
Kyle Spearrin
98eaeddbfd update jslib 2020-07-16 09:16:20 -04:00
Vincent Salucci
00e4df2dd3 Updated enterprise URL dev (port) (#574) 2020-07-14 09:12:49 -05:00
Addison Beck
cfb4133152 Closed alerts on lock (#572)
Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>
2020-07-09 15:11:28 -05:00
Vincent Salucci
42361d17b5 [Enterprise] Added button to launch portal (#570)
* initial commit

* Added Enterprise button and used new business portal bool

* Reverting services module local changes

* Formatted some new lines
2020-07-07 13:32:22 -05:00
Vincent Salucci
02ee95506c Update jslib (57ace40 -> d308245) (#569) 2020-07-07 09:46:44 -05:00
Kyle Spearrin
a749946457 bump version 2020-06-29 11:31:52 -04:00
Kyle Spearrin
18fb86c243 New Crowdin updates (#565)
* New translations messages.json (French)

* New translations messages.json (Portuguese)

* 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 (Russian)

* New translations messages.json (Polish)

* New translations messages.json (Spanish)

* New translations messages.json (Dutch)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (German)

* New translations messages.json (Danish)

* New translations messages.json (Catalan)

* New translations messages.json (Bulgarian)

* New translations messages.json (English, United Kingdom)
2020-06-29 11:30:21 -04:00
Kyle Spearrin
7597e4006c New Crowdin updates (#564)
* New translations messages.json (French)

* New translations messages.json (Portuguese)

* New translations messages.json (Sinhala)

* New translations messages.json (Esperanto)

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

* New translations messages.json (Estonian)

* New translations messages.json (Croatian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Vietnamese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Polish)

* New translations messages.json (Spanish)

* New translations messages.json (German)

* 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 (Dutch)

* 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 (Norwegian Bokmal)
2020-06-26 21:24:31 -04:00
Chad Scharf
50be5f4895 Merge pull request #563 from bitwarden/v2.15.0
version bump
2020-06-25 17:20:47 -04:00
Chad Scharf
326fb47593 version bump 2020-06-25 17:02:17 -04:00
Chad Scharf
240c576bad Merge branch 'feature/reference-id' 2020-06-25 16:36:28 -04:00
Chad Scharf
88c8c8ae55 referenceId PR feedback + lint fix 2020-06-25 16:30:45 -04:00
Kyle Spearrin
394a7e42fb a few tweaks for hidden passwords (#561)
* a few tweaks for hidden passwords

* revert org layout changes

* revert column size change
2020-06-25 15:55:50 -04:00
Chad Scharf
869ee217eb Updated jslib 2020-06-25 15:51:26 -04:00
Chad Scharf
03dbe272fc Added referenceId to register component 2020-06-25 15:18:21 -04:00
Chad Scharf
87973e9775 Merge pull request #558 from bitwarden/feature/tax-info-collection
Feature/tax info collection
2020-06-18 11:33:08 -04:00
Chad Scharf
4450b1aa81 update jslib 2020-06-18 11:30:36 -04:00
Chad Scharf
57575ea322 Merge pull request #487 from clayadams5226/patch-1
Update ISSUE_TEMPLATE.md
2020-06-18 09:29:35 -04:00
Chad Scharf
68d3d7abfd Combine tax info with other updates 2020-06-17 20:11:30 -04:00
Chad Scharf
4502a966a1 PR feedback, loading spinner 2020-06-17 13:35:39 -04:00
Chad Scharf
e523733b2c Merge branch 'feature/tax-info-collection' of https://github.com/bitwarden/web into feature/tax-info-collection
Merge conflicts and jslib update
2020-06-17 13:21:43 -04:00
Chad Scharf
3864f1d950 Revert services.module.ts 2020-06-17 13:20:06 -04:00
Chad Scharf
4bdb9c8632 Collect tax info for payments 2020-06-17 13:20:06 -04:00
Chad Scharf
b1c098614c tax info collection zip + VAT 2020-06-17 13:20:06 -04:00
Vincent Salucci
4309064804 [Enterprise] Added environment checks (#559)
* Update jslib (2b6657a -> 28d21ca)

* Environment variable checks
2020-06-16 09:35:25 -05:00
Chad Scharf
f91e67ad6b Revert services.module.ts 2020-06-12 19:51:00 -04:00
Chad Scharf
d63ec210c7 Collect tax info for payments 2020-06-12 19:33:29 -04:00
Chad Scharf
3d160ee1df Merge pull request #538 from Hinton/feature/hide-passwords
Add support for collections with hide passwords
2020-06-11 14:51:05 -04:00
Hinton
51b482f57d Merge branch 'master' of https://github.com/bitwarden/web into feature/hide-passwords 2020-06-11 20:33:43 +02:00
Hinton
b367c4b4ce Update jslib 2020-06-11 20:28:22 +02:00
Chad Scharf
7432ad310c Merge pull request #557 from bitwarden/feature/enterprise-landing-page
Layout images and styling for register page
2020-06-11 12:04:18 -04:00
Chad Scharf
5b02202efb Llayout images and styling for register page 2020-06-11 11:27:46 -04:00
Chad Scharf
23056bcd63 tax info collection zip + VAT 2020-06-08 17:24:05 -04:00
Kyle Spearrin
2b0c92a4ea stub alternate layout support for register page (#550) 2020-06-04 14:12:37 -04:00
Kyle Spearrin
d669d43fe4 update jslib 2020-06-04 12:48:11 -04:00
hinton
426e0edfb5 Allow editing of newly added fields 2020-06-03 20:46:32 +02:00
Kyle Spearrin
2cc0aa6f3d a few cleanup items for full width setting change (#547) 2020-06-02 09:56:16 -04:00
Chad Scharf
f895916fbb Merge pull request #543 from syntax-error752/feature/UIscaling
Update CSS to allow for larger screens.
2020-06-01 19:27:28 -04:00
syntaxerror752
fea3bba0df Changed method of keeping the logon box the same size 2020-06-02 08:29:58 +10:00
hinton
7ed7321219 Mark "hidden" fields and totp as disabled. 2020-06-01 21:59:58 +02:00
hinton
b2bf192677 Merge branch 'master' of https://github.com/bitwarden/web into feature/hide-passwords 2020-06-01 21:38:17 +02:00
syntaxerror752
d323e775ca Removed the need for the messageing service to be in app.component.ts 2020-05-31 22:02:41 +10:00
syntaxerror752
22a00b2341 Added toggle full width function
Added toggle full width function.
Added messaging service to trigger function.
Added CSS to keep login box the same size.
2020-05-30 18:30:41 +10:00
syntaxerror752
f36bba6406 Revert last commit due to requested changes
Revert last commit due to requested changes.
Renamed variable.
2020-05-30 11:12:15 +10:00
syntaxerror752
674c583881 Update HTML and TS scripts for UI scaling 2020-05-29 23:08:03 +10:00
syntaxerror752
eb5ad7c6dc Added UI scaling tickbox to options menu 2020-05-29 21:28:26 +10:00
Kyle Spearrin
ca771eb04c New Crowdin translations (#546)
* New translations messages.json (Czech)

* New translations messages.json (Danish)

* New translations messages.json (Portuguese)

* New translations messages.json (Chinese Simplified)
2020-05-28 20:06:27 -04:00
Vincent Salucci
d705b8ab33 Update jslib 2858724 -> 212a2e3 (#545) 2020-05-28 13:58:49 -05:00
Kyle Spearrin
9454eda082 New Crowdin translations (#544)
* New translations messages.json (Afrikaans)

* New translations messages.json (German)

* New translations messages.json (Finnish)

* New translations messages.json (Italian)

* New translations messages.json (Polish)

* New translations messages.json (English, United Kingdom)
2020-05-26 10:31:40 -04:00
hinton
7d5329e186 Add hide password checkboxes to add/edit collection. 2020-05-23 11:15:23 +02:00
Kyle Spearrin
18979a7f1a Add support for greek language (#541) 2020-05-22 23:14:26 -04:00
Vincent Salucci
7301158e54 [Paging] Added for Organization Users, Pages, and Collections (#539)
* Updating jslib

* Added paging for Organizational Users, Groups, and Collections

* Updated jslib fb7335b -> 2858724
2020-05-22 11:26:43 -05:00
hinton
5b9c41f29a Use correct variable for view password 2020-05-22 09:26:57 +02:00
Kyle Spearrin
179884cf93 New translations messages.json (German) (#540) 2020-05-21 15:08:42 -04:00
hinton
5bc01ea13e Add support for collections with hide passwords 2020-05-21 15:58:55 +02:00
Kyle Spearrin
ca43db8d93 New Crowdin translations (#537)
* New translations messages.json (French)

* New translations messages.json (Dutch)

* 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 (Russian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Korean)

* New translations messages.json (Spanish)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (Hebrew)

* New translations messages.json (Finnish)

* New translations messages.json (Greek)

* New translations messages.json (German)

* New translations messages.json (Czech)

* New translations messages.json (Catalan)

* New translations messages.json (Bulgarian)

* New translations messages.json (Norwegian Bokmal)
2020-05-21 09:48:59 -04:00
Kyle Spearrin
f4cb5e6632 update jslib 2020-05-20 15:42:51 -04:00
Kyle Spearrin
da2e740e65 bump version 2020-05-18 22:02:44 -04:00
Kyle Spearrin
2f0d2bdf32 New Crowdin translations (#533)
* New translations messages.json (French)

* New translations messages.json (Polish)

* New translations messages.json (Esperanto)

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

* New translations messages.json (Estonian)

* New translations messages.json (Croatian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Vietnamese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese)

* New translations messages.json (Dutch)

* New translations messages.json (Spanish)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (Hebrew)

* 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 (Bulgarian)

* New translations messages.json (Belarusian)

* New translations messages.json (Afrikaans)

* New translations messages.json (Norwegian Bokmal)
2020-05-18 15:56:43 -04:00
Kyle Spearrin
97eedb2034 fixing a few bug, asset updates, tweaks (#532)
* fixing a few bug, asset updates, tweaks

* dont save until save button clicked
2020-05-18 09:51:20 -04:00
Kyle Spearrin
3ac46e62cb updated formatting 2020-05-08 11:54:49 -04:00
Srdjan Milic
97db3635af fix: webpack.config.js plugin (#525) 2020-05-08 11:42:28 -04:00
Chad Scharf
e3464da19a Merge pull request #527 from bitwarden/soft-delete
Soft delete feature
2020-05-08 11:17:02 -04:00
Chad Scharf
ec3ee8fbb3 Merge branch 'master' into soft-delete 2020-05-08 09:32:59 -04:00
Kyle Spearrin
96208d3760 brand color updates 2020-05-05 16:59:33 -04:00
Kyle Spearrin
5bb61c0730 color updates + jslib 2020-05-05 16:32:45 -04:00
Srdjan Milic
858f86d9df fix: package.json info (#523) 2020-05-01 12:21:23 -04:00
Vincent Salucci
aa1e5a11ad [Auto Logout] Added warning dialog for log out action (#518)
* Added warning dialog for log out timeout action

* Reverting testing service module endpoints
2020-04-25 08:13:30 -05:00
Kyle Spearrin
ded8865914 Null check allUsers (#515) 2020-04-20 00:17:06 -04:00
Kyle Spearrin
da1437a268 update lunr types (#514) 2020-04-14 15:55:22 -04:00
Chad Scharf
599f831a09 Merge pull request #513 from bitwarden/soft-delete-toast
[Soft Delete] - Deleted message (sent to trash)
2020-04-14 15:19:12 -04:00
Chad Scharf
23b532e2bf [Soft Delete] - Deleted message (sent to trash) 2020-04-14 15:06:54 -04:00
Chad Scharf
9f1b8ae58f Merge pull request #511 from bitwarden/soft-delete-chad
[Soft Delete] - Added trash and related functionality to web vault
2020-04-10 13:56:33 -04:00
Chad Scharf
d62850f82d [Soft Delete] enable copy/view operations in trash 2020-04-10 13:42:37 -04:00
Chad Scharf
41a0cfd0a2 [Soft Delete] - Added trash and related functionality to web vault 2020-04-08 16:48:30 -04:00
Vincent Salucci
fb6e85c56b Update jslib (28e3fff -> 72e3893) (#510)
* Update jslib (28e3fff -> 72e3893)

* Updated lock description, updated vaultTimeoutService init

Co-authored-by: Vincent Salucci <vsalucci@bitwarden.com>
2020-04-06 13:07:09 -05:00
Vincent Salucci
d58550c2b8 [Auto-Logout] Implement upstream changes (#506)
* Initial commit of auto logout functionality

* Update jslib 31a2574 -> 28e3fff

* Reverting prod URLs

* Set log out expired param to false

Co-authored-by: Vincent Salucci <vsalucci@bitwarden.com>
2020-03-30 09:59:47 -05:00
Kyle Spearrin
5bf3ca2708 New Crowdin translations (#505)
* New translations messages.json (Hebrew)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (Swedish)

* New translations messages.json (Spanish)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Korean)

* New translations messages.json (Japanese)

* New translations messages.json (Italian)

* New translations messages.json (Hungarian)

* New translations messages.json (Greek)

* New translations messages.json (German)

* New translations messages.json (French)

* New translations messages.json (Finnish)

* New translations messages.json (Estonian)

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

* New translations messages.json (Dutch)

* New translations messages.json (Danish)

* New translations messages.json (Czech)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Catalan)

* New translations messages.json (Bulgarian)
2020-03-24 14:50:53 -04:00
Kyle Spearrin
3e4a7e7a56 version bump 2020-03-21 00:55:44 -04:00
Kyle Spearrin
5d17de227b update jslib 2020-03-21 00:20:06 -04:00
Vincent Salucci
0d985c0221 Update jslib (0a30c7e -> 3ad546c) (#500)
Co-authored-by: Vincent Salucci <vsalucci@bitwarden.com>
2020-03-18 13:06:06 -05:00
Clayton
fba2102518 Update ISSUE_TEMPLATE.md
Added a uniform template to be used for all issues that are reported.
2020-03-06 08:27:39 -05:00
307 changed files with 82010 additions and 16062 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

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

@@ -0,0 +1,201 @@
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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- 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@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
- 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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- 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@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
with:
nuget-version: 'latest'
- name: Set up MSBuild
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
- name: Print environment
run: |
nuget help | grep Version
msbuild -version
dotnet --info
node --version
npm --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- 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

@@ -3,3 +3,50 @@ Please do not submit feature requests. The [Community Forums][1] has a
section for submitting, voting for, and discussing product feature requests.
[1]: https://community.bitwarden.com
-->
## Describe the Bug
<!-- Comment:
A clear and concise description of what the bug is.
-->
## Steps To Reproduce
<!-- Comment:
How can we reproduce the behavior:
-->
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. Click on '...'
## Expected Result
<!-- Comment:
A clear and concise description of what you expected to happen.
-->
## Actual Result
<!-- Comment:
A clear and concise description of what is happening.
-->
## Screenshots or Videos
<!-- Comment:
If applicable, add screenshots and/or a short video to help explain your problem.
-->
## Environment
- Operating system: [e.g. Windows 10, Mac OS Catalina]
- Browser: [e.g. Firefox 73.0.1]
- Build Version (Bottom of the page): [2.13.0]
## Additional Context
<!-- Comment:
Add any other context about the problem here.
-->

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" />
@@ -23,38 +23,47 @@
### Requirements
- [Node.js](https://nodejs.org) v8.11 or greater
- [Node.js](https://nodejs.org) v14 or greater
### 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"
}

View File

@@ -9,3 +9,4 @@ files:
zh-CN: zh_CN
zh-TW: zh_TW
en-GB: en_GB
en-IN: en_IN

2
jslib

Submodule jslib updated: 0a30c7eb1e...6b9246c272

13897
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
{
"name": "bitwarden-web",
"version": "2.13.1",
"version": "2.20.3",
"license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web",
"scripts": {
"sub:init": "git submodule update --init --recursive",
"sub:update": "git submodule update --remote",
@@ -10,90 +12,100 @@
"symlink:mac": "npm run symlink:lin",
"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: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:watch": "gulp prebuild && webpack serve",
"build:dev": "gulp prebuild && cross-env ENV=development webpack",
"build:dev:watch": "gulp prebuild && cross-env ENV=development webpack serve",
"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 serve",
"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 serve",
"build:selfhost": "gulp prebuild && cross-env SELF_HOST=true webpack serve",
"build:selfhost:watch": "gulp prebuild && cross-env SELF_HOST=true webpack serve",
"build:selfhost:prod": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack",
"build:selfhost:prod:watch": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack-dev-server",
"build:selfhost:prod:watch": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack serve",
"clean:l10n": "git push origin --delete l10n_master",
"dist": "npm run build:prod && gulp postdist",
"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": "^7.2.11",
"@ngtools/webpack": "^7.2.2",
"@types/jquery": "^3.3.6",
"@types/lunr": "^2.1.6",
"@types/node-forge": "^0.7.5",
"@types/papaparse": "^4.5.3",
"@angular/compiler-cli": "^11.2.11",
"@ngtools/webpack": "^11.2.10",
"@types/duo_web_sdk": "^2.7.0",
"@types/jquery": "^3.5.5",
"@types/lunr": "^2.3.3",
"@types/node": "^14.14.43",
"@types/node-forge": "^0.9.7",
"@types/papaparse": "^5.2.5",
"@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",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"del": "^3.0.0",
"extract-text-webpack-plugin": "next",
"file-loader": "^2.0.0",
"gh-pages": "^1.2.0",
"gulp": "^4.0.0",
"gulp-google-webfonts": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"node-sass": "^4.13.1",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.0",
"terser-webpack-plugin": "^1.2.3",
"ts-loader": "^5.3.3",
"tslint": "^5.12.1",
"@types/webpack": "^4.4.27",
"@types/zxcvbn": "^4.4.1",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.4.0",
"cross-env": "^7.0.3",
"css-loader": "^5.2.3",
"del": "^6.0.0",
"file-loader": "^6.2.0",
"gh-pages": "^3.1.0",
"gulp": "^4.0.2",
"gulp-google-webfonts": "^4.0.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.1",
"mini-css-extract-plugin": "^1.5.0",
"sass": "^1.32.10",
"sass-loader": "^10.1.1",
"style-loader": "^2.0.0",
"tapable": "^1.1.3",
"terser-webpack-plugin": "^4.2.3",
"ts-loader": "^8.1.0",
"tslint": "^6.1.3",
"tslint-loader": "^3.5.4",
"typescript": "3.2.4",
"webpack": "^4.29.0",
"webpack-cli": "^3.2.1",
"webpack-dev-server": "^3.1.14"
"typescript": "4.1.5",
"webpack": "^4.46.0",
"webpack-cli": "^4.6.0",
"webpack-dev-server": "^3.11.2"
},
"dependencies": {
"@angular/animations": "7.2.1",
"@angular/cdk": "7.2.1",
"@angular/common": "7.2.1",
"@angular/compiler": "7.2.1",
"@angular/core": "7.2.1",
"@angular/forms": "7.2.1",
"@angular/platform-browser": "7.2.1",
"@angular/platform-browser-dynamic": "7.2.1",
"@angular/router": "7.2.1",
"@angular/upgrade": "7.2.1",
"@microsoft/signalr": "3.1.0",
"@microsoft/signalr-protocol-msgpack": "3.1.0",
"angular2-toaster": "6.1.0",
"angulartics2": "6.3.0",
"big-integer": "1.6.36",
"bootstrap": "4.3.1",
"braintree-web-drop-in": "1.13.0",
"core-js": "2.6.2",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"@angular/animations": "^11.2.11",
"@angular/cdk": "^11.2.10",
"@angular/common": "^11.2.11",
"@angular/compiler": "^11.2.11",
"@angular/core": "^11.2.11",
"@angular/forms": "^11.2.11",
"@angular/platform-browser": "^11.2.11",
"@angular/platform-browser-dynamic": "^11.2.11",
"@angular/router": "^11.2.11",
"@microsoft/signalr": "3.1.13",
"@microsoft/signalr-protocol-msgpack": "3.1.13",
"angular2-toaster": "11.0.1",
"big-integer": "1.6.48",
"bootstrap": "4.6.0",
"braintree-web-drop-in": "1.28.0",
"browser-hrtime": "^1.1.8",
"core-js": "^3.11.0",
"date-input-polyfill": "^2.14.0",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git#5f9a1a8598d2cda494c4f5ee0e38b31474abfee9",
"font-awesome": "4.7.0",
"jquery": "3.4.1",
"lunr": "2.3.3",
"ngx-infinite-scroll": "7.0.1",
"node-forge": "0.7.6",
"papaparse": "4.6.0",
"popper.js": "1.14.4",
"jquery": "3.6.0",
"lunr": "2.3.9",
"ngx-infinite-scroll": "10.0.1",
"node-forge": "0.10.0",
"papaparse": "5.3.0",
"popper.js": "1.16.1",
"qrious": "4.0.2",
"rxjs": "6.3.3",
"sweetalert2": "9.8.1",
"web-animations-js": "2.3.1",
"webcrypto-shim": "0.1.4",
"whatwg-fetch": "3.0.0",
"zone.js": "0.8.28",
"rxjs": "^6.6.7",
"sweetalert2": "^10.16.6",
"tslib": "^2.2.0",
"webcrypto-shim": "0.1.7",
"whatwg-fetch": "3.6.2",
"zone.js": "0.11.4",
"zxcvbn": "4.4.2"
},
"engines": {
"node": "~14",
"npm": "~6"
}
}

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

@@ -1,4 +1,4 @@
<form (ngSubmit)="submit()" class="container" ngNativeValidate>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="text-center mb-4">
@@ -25,9 +25,11 @@
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block">
<i class="fa fa-unlock-alt" aria-hidden="true"></i>
{{'unlock' | i18n}}
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span>
<i class="fa fa-unlock-alt" aria-hidden="true"></i> {{'unlock' | i18n}}
</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-block ml-2 mt-0" (click)="logOut()">
{{'logOut' | i18n}}

View File

@@ -1,15 +1,16 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LockService } from 'jslib/abstractions/lock.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { UserService } from 'jslib/abstractions/user.service';
import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
import { RouterService } from '../services/router.service';
@@ -23,22 +24,15 @@ export class LockComponent extends BaseLockComponent {
constructor(router: Router, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
userService: UserService, cryptoService: CryptoService,
storageService: StorageService, lockService: LockService,
storageService: StorageService, vaultTimeoutService: VaultTimeoutService,
environmentService: EnvironmentService, private routerService: RouterService,
stateService: StateService) {
stateService: StateService, apiService: ApiService) {
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService,
storageService, lockService, environmentService, stateService);
storageService, vaultTimeoutService, environmentService, stateService, apiService);
}
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

@@ -44,6 +44,11 @@
<i class="fa fa-pencil-square-o" aria-hidden="true"></i> {{'createAccount' | i18n}}
</a>
</div>
<div class="d-flex">
<a routerLink="/sso" class="btn btn-outline-secondary btn-block mt-2">
<i class="fa fa-bank" aria-hidden="true"></i> {{'enterpriseSingleSignOn' | i18n}}
</a>
</div>
</div>
</div>
</div>

View File

@@ -5,7 +5,10 @@ import {
} from '@angular/router';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
@@ -20,13 +23,18 @@ export class LoginComponent extends BaseLoginComponent {
constructor(authService: AuthService, router: Router,
i18nService: I18nService, private route: ActivatedRoute,
storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService) {
super(authService, router, platformUtilsService, i18nService, storageService, stateService);
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) {
super(authService, router,
platformUtilsService, i18nService,
stateService, environmentService,
passwordGenerationService, cryptoFunctionService,
storageService);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}
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;
}
@@ -44,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

@@ -1,103 +1,156 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'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">
{{'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">
<small class="form-text text-muted">{{'emailAddressDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="name">{{'yourName' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name"
[appAutofocus]="email !== ''">
<small class="form-text text-muted">{{'yourNameDesc' | i18n}}</small>
</div>
<div class="form-group">
<app-callout type="info" *ngIf="enforcedPolicyOptions">
{{'masterPasswordPolicyInEffect' | i18n}}
<ul class="mb-0">
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
</li>
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
</li>
<li *ngIf="enforcedPolicyOptions?.requireUpper">
{{'policyInEffectUppercase' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireLower">
{{'policyInEffectLowercase' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
{{'policyInEffectNumbers' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
</ul>
</app-callout>
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex">
<div class="w-100">
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPassword" class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required
appInputVerbatim>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
<div>
<button type="button" class="ml-1 btn btn-link"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
<div class="progress-bar invisible"></div>
</div>
</div>
<small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
<div class="d-flex">
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPasswordRetype" class="text-monospace form-control"
[(ngModel)]="confirmMasterPassword" required appInputVerbatim>
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
(click)="togglePassword(true)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{'masterPassHint' | i18n}}</label>
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
</div>
<hr>
<div class="d-flex mb-2">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span>{{'submit' | i18n}}</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'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 class="layout" [ngClass]="['layout', layout]">
<header class="header" *ngIf="layout === 'enterprise2'">
<div class="container">
<div class="row">
<div class="col-7">
<img alt="Bitwarden" class="logo mb-2" src="../../images/register-layout/logo-horizontal-white.png">
</div>
</div>
</div>
</div>
</form>
</header>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row">
<div class="col-7" *ngIf="layout">
<div class="mt-5">
<div *ngIf="layout === 'enterprise2'">
<h2>Companies globally trust Bitwarden for password management.</h2>
<p>Start your 7-day free trial!</p>
<p class="highlight">Quickly deploy your <b>organization</b></p>
<p>Use Bitwarden across all platforms</p>
<p>Collaborate and share securely</p>
<figure>
<figcaption>
<cite>
<img src="../../images/register-layout/wired-logo.png" alt="Wired">
</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
</blockquote>
</figure>
</div>
<div *ngIf="layout === 'enterprise3'">
<p>Enterprise 3 layout</p>
</div>
<div *ngIf="layout === 'enterprise4'">
<p>Enterprise 4 layout</p>
</div>
</div>
</div>
<div [ngClass]="{'col-5': layout, 'col-12': !layout}">
<div class="row justify-content-md-center mt-5">
<div [ngClass]="{'col-5': !layout, 'col-12': layout}">
<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">
{{'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">
<small class="form-text text-muted">{{'emailAddressDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="name">{{'yourName' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name"
[appAutofocus]="email !== ''">
<small class="form-text text-muted">{{'yourNameDesc' | i18n}}</small>
</div>
<div class="form-group">
<app-callout type="info" *ngIf="enforcedPolicyOptions">
{{'masterPasswordPolicyInEffect' | i18n}}
<ul class="mb-0">
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
</li>
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
</li>
<li *ngIf="enforcedPolicyOptions?.requireUpper">
{{'policyInEffectUppercase' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireLower">
{{'policyInEffectLowercase' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
{{'policyInEffectNumbers' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
</ul>
</app-callout>
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex">
<div class="w-100">
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPassword" class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required
appInputVerbatim>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
<div>
<button type="button" class="ml-1 btn btn-link"
appA11yTitle="{{'toggleVisibility' | i18n}}"
(click)="togglePassword(false)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
<div class="progress-bar invisible"></div>
</div>
</div>
<small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
<div class="d-flex">
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPasswordRetype" class="text-monospace form-control"
[(ngModel)]="confirmMasterPassword" required appInputVerbatim>
<button type="button" class="ml-1 btn btn-link"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(true)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{'masterPassHint' | i18n}}</label>
<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"
[disabled]="form.loading">
<span>{{'submit' | i18n}}</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>

View File

@@ -19,6 +19,7 @@ import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordP
import { Policy } from 'jslib/models/domain/policy';
import { PolicyData } from 'jslib/models/data/policyData';
import { ReferenceEventRequest } from 'jslib/models/request/referenceEventRequest';
@Component({
selector: 'app-register',
@@ -26,7 +27,7 @@ import { PolicyData } from 'jslib/models/data/policyData';
})
export class RegisterComponent extends BaseRegisterComponent {
showCreateOrgMessage = false;
showTerms = true;
layout = '';
enforcedPolicyOptions: MasterPasswordPolicyOptions;
private policies: Policy[];
@@ -38,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() {
@@ -62,7 +62,8 @@ 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;
}
@@ -70,9 +71,21 @@ export class RegisterComponent extends BaseRegisterComponent {
this.stateService.save('loginRedirect', { route: '/settings/premium' });
} else if (qParams.org != null) {
this.showCreateOrgMessage = true;
this.referenceData.flow = qParams.org;
this.stateService.save('loginRedirect',
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
}
if (qParams.layout != null) {
this.layout = this.referenceData.layout = qParams.layout;
}
if (qParams.reference != null) {
this.referenceData.id = qParams.reference;
} else {
this.referenceData.id = ('; ' + document.cookie).split('; reference=').pop().split(';').shift();
}
if (this.referenceData.id === '') {
this.referenceData.id = null;
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
@@ -83,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

@@ -0,0 +1,85 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'setMasterPassword' | i18n}}</p>
<div class="card d-block">
<div class="card-body text-center" *ngIf="syncLoading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
{{'loading' | i18n}}
</div>
<div class="card-body" *ngIf="!syncLoading">
<app-callout type="info">{{'ssoCompleteRegistration' | i18n}}</app-callout>
<div class="form-group">
<app-callout type="info" *ngIf="enforcedPolicyOptions">
{{'masterPasswordPolicyInEffect' | i18n}}
<ul class="mb-0">
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
</li>
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
</li>
<li *ngIf="enforcedPolicyOptions?.requireUpper">
{{'policyInEffectUppercase' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireLower">
{{'policyInEffectLowercase' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
{{'policyInEffectNumbers' | i18n}}</li>
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
</ul>
</app-callout>
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex">
<div class="w-100">
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPasswordHash" class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required
appInputVerbatim>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
<div>
<button type="button" class="ml-1 btn btn-link"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
<div class="progress-bar invisible"></div>
</div>
</div>
<small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
<div class="d-flex">
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPasswordRetype" class="text-monospace form-control"
[(ngModel)]="masterPasswordRetype" required appInputVerbatim>
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
(click)="togglePassword(true)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{'masterPassHint' | i18n}}</label>
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary btn-block ml-2 mt-0" (click)="logOut()">
{{'logOut' | i18n}}
</button>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,34 @@
import { Component } from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
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 { SyncService } from 'jslib/abstractions/sync.service';
import { UserService } from 'jslib/abstractions/user.service';
import {
SetPasswordComponent as BaseSetPasswordComponent,
} from 'jslib/angular/components/set-password.component';
@Component({
selector: 'app-set-password',
templateUrl: 'set-password.component.html',
})
export class SetPasswordComponent extends BaseSetPasswordComponent {
constructor(apiService: ApiService, i18nService: I18nService,
cryptoService: CryptoService, messagingService: MessagingService,
userService: UserService, passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService, policyService: PolicyService, router: Router,
syncService: SyncService, route: ActivatedRoute) {
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
platformUtilsService, policyService, router, apiService, syncService, route);
}
}

View File

@@ -0,0 +1,33 @@
<form #form (ngSubmit)="submit()" class="container" [appApiAction]="initiateSsoFormPromise" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<img src="../../images/logo-dark@2x.png" class="logo mb-2" alt="Bitwarden">
<div class="card d-block mt-4">
<div class="card-body" *ngIf="loggingIn">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
{{'loading' | i18n}}
</div>
<div class="card-body" *ngIf="!loggingIn">
<p>{{'ssoLogInWithOrgIdentifier' | i18n}}</p>
<div class="form-group">
<label for="identifier">{{'organizationIdentifier' | i18n}}</label>
<input id="identifier" class="form-control" type="text" name="Identifier"
[(ngModel)]="identifier" required appAutofocus>
</div>
<hr>
<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> {{'logIn' | i18n}}
</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}
</a>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,61 @@
import { Component } from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { SsoComponent as BaseSsoComponent } from 'jslib/angular/components/sso.component';
const IdentifierStorageKey = 'ssoOrgIdentifier';
@Component({
selector: 'app-sso',
templateUrl: 'sso.component.html',
})
export class SsoComponent extends BaseSsoComponent {
constructor(authService: AuthService, router: Router,
i18nService: I18nService, route: ActivatedRoute,
storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService, apiService: ApiService,
cryptoFunctionService: CryptoFunctionService,
passwordGenerationService: PasswordGenerationService) {
super(authService, router, i18nService, route, storageService, stateService, platformUtilsService,
apiService, cryptoFunctionService, passwordGenerationService);
this.redirectUri = window.location.origin + '/sso-connector.html';
this.clientId = 'web';
}
async ngOnInit() {
super.ngOnInit();
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
if (qParams.identifier != null) {
this.identifier = qParams.identifier;
} else {
const storedIdentifier = await this.storageService.get<string>(IdentifierStorageKey);
if (storedIdentifier != null) {
this.identifier = storedIdentifier;
}
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
}
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

@@ -1,5 +1,5 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="twoStepOptionsTitle">
<div class="modal-dialog" role="document">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="twoStepOptionsTitle">{{'twoStepOptions' | i18n}}</h2>
@@ -7,17 +7,19 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="list-group list-group-flush">
<a href="#" appStopClick *ngFor="let p of providers" (click)="choose(p)"
class="list-group-item list-group-item-action">
<img [src]="'images/two-factor/' + p.type + '.png'" alt="" class="pull-right">
<h3>{{p.name}}</h3>
{{p.description}}
</a>
<a href="#" appStopClick class="list-group-item list-group-item-action" (click)="recover()">
<h3>{{'recoveryCodeTitle' | i18n}}</h3>
{{'recoveryCodeDesc' | i18n}}
</a>
<div class="modal-body">
<div class="list-group list-group-flush">
<a href="#" appStopClick *ngFor="let p of providers" (click)="choose(p)"
class="list-group-item list-group-item-action">
<img [src]="'images/two-factor/' + p.type + '.png'" alt="" class="pull-right">
<h3>{{p.name}}</h3>
{{p.description}}
</a>
<a href="#" appStopClick class="list-group-item list-group-item-action" (click)="recover()">
<h3>{{'recoveryCodeTitle' | i18n}}</h3>
{{'recoveryCodeDesc' | i18n}}
</a>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>

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';
@@ -28,15 +31,15 @@ import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/comp
templateUrl: 'two-factor.component.html',
})
export class TwoFactorComponent extends BaseTwoFactorComponent {
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
@ViewChild('twoFactorOptions', { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef;
constructor(authService: AuthService, router: Router,
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';
@@ -15,6 +16,8 @@ import { LoginComponent } from './accounts/login.component';
import { RecoverDeleteComponent } from './accounts/recover-delete.component';
import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component';
import { RegisterComponent } from './accounts/register.component';
import { SetPasswordComponent } from './accounts/set-password.component';
import { SsoComponent } from './accounts/sso.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
import { VerifyRecoverDeleteComponent } from './accounts/verify-recover-delete.component';
@@ -55,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';
@@ -81,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 = [
{
@@ -99,18 +109,36 @@ const routes: Routes = [
canActivate: [UnauthGuardService],
data: { titleId: 'createAccount' },
},
{
path: 'sso', component: SsoComponent,
canActivate: [UnauthGuardService],
data: { titleId: 'enterpriseSingleSignOn' },
},
{
path: 'set-password', component: SetPasswordComponent,
data: { titleId: 'setMasterPassword' },
},
{
path: 'hint', component: HintComponent,
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',
@@ -130,6 +158,11 @@ const routes: Routes = [
canActivate: [UnauthGuardService],
data: { titleId: 'deleteAccount' },
},
{
path: 'send/:sendId/:key',
component: AccessComponent,
data: { title: 'Bitwarden Send' },
},
],
},
{
@@ -138,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,
@@ -160,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'},
},
],
},
],
},
{
@@ -216,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],
},
},
],
},
@@ -253,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' } },
@@ -297,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';
import {
BodyOutputType,
Toast,
ToasterConfig,
ToasterContainerComponent,
ToasterService,
} from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import {
Component,
@@ -35,7 +32,6 @@ import { CryptoService } from 'jslib/abstractions/crypto.service';
import { EventService } from 'jslib/abstractions/event.service';
import { FolderService } from 'jslib/abstractions/folder.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LockService } from 'jslib/abstractions/lock.service';
import { NotificationsService } from 'jslib/abstractions/notifications.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
@@ -46,6 +42,7 @@ import { StateService } from 'jslib/abstractions/state.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { TokenService } from 'jslib/abstractions/token.service';
import { UserService } from 'jslib/abstractions/user.service';
import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
import { ConstantsService } from 'jslib/services/constants.service';
@@ -70,15 +67,15 @@ 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 lockService: LockService, private storageService: StorageService,
private vaultTimeoutService: VaultTimeoutService, private storageService: StorageService,
private cryptoService: CryptoService, private collectionService: CollectionService,
private sanitizer: DomSanitizer, private searchService: SearchService,
private notificationsService: NotificationsService, private routerService: RouterService,
@@ -110,7 +107,7 @@ export class AppComponent implements OnDestroy, OnInit {
this.logOut(!!message.expired);
break;
case 'lockVault':
await this.lockService.lock();
await this.vaultTimeoutService.lock();
break;
case 'locked':
this.notificationsService.updateConnection(false);
@@ -139,14 +136,20 @@ 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 },
});
case 'setFullWidth':
this.setFullWidth();
break;
default:
break;
@@ -154,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) {
@@ -166,6 +169,8 @@ export class AppComponent implements OnDestroy, OnInit {
}
}
});
this.setFullWidth();
}
ngOnDestroy() {
@@ -193,11 +198,12 @@ 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'));
}
Swal.close();
this.router.navigate(['/']);
});
}
@@ -262,4 +268,13 @@ export class AppComponent implements OnDestroy, OnInit {
this.notificationsService.reconnectFromActivity();
}
}
private async setFullWidth() {
const enableFullWidth = await this.storageService.get<boolean>('enableFullWidth');
if (enableFullWidth) {
document.body.classList.add('full-width');
} else {
document.body.classList.remove('full-width');
}
}
}

View File

@@ -1,8 +1,4 @@
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 +23,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';
@@ -34,6 +31,8 @@ import { LoginComponent } from './accounts/login.component';
import { RecoverDeleteComponent } from './accounts/recover-delete.component';
import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component';
import { RegisterComponent } from './accounts/register.component';
import { SetPasswordComponent } from './accounts/set-password.component';
import { SsoComponent } from './accounts/sso.component';
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
@@ -58,13 +57,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,
@@ -96,10 +93,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';
@@ -107,6 +109,14 @@ 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';
import { OrganizationsComponent } from './settings/organizations.component';
@@ -115,13 +125,14 @@ import { PremiumComponent } from './settings/premium.component';
import { ProfileComponent } from './settings/profile.component';
import { PurgeVaultComponent } from './settings/purge-vault.component';
import { SettingsComponent } from './settings/settings.component';
import { TaxInfoComponent } from './settings/tax-info.component';
import { TwoFactorAuthenticatorComponent } from './settings/two-factor-authenticator.component';
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';
@@ -143,13 +154,16 @@ import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.comp
import { AddEditComponent } from './vault/add-edit.component';
import { AttachmentsComponent } from './vault/attachments.component';
import { BulkActionsComponent } from './vault/bulk-actions.component';
import { BulkDeleteComponent } from './vault/bulk-delete.component';
import { BulkMoveComponent } from './vault/bulk-move.component';
import { BulkRestoreComponent } from './vault/bulk-restore.component';
import { BulkShareComponent } from './vault/bulk-share.component';
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';
@@ -173,51 +187,80 @@ 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 localeBg from '@angular/common/locales/bg';
import localeCa from '@angular/common/locales/ca';
import localeCs from '@angular/common/locales/cs';
import localeDa from '@angular/common/locales/da';
import localeDe from '@angular/common/locales/de';
import localeEl from '@angular/common/locales/el';
import localeEnGb from '@angular/common/locales/en-GB';
import localeEnIn from '@angular/common/locales/en-IN';
import localeEo from '@angular/common/locales/eo';
import localeEs from '@angular/common/locales/es';
import localeEt from '@angular/common/locales/et';
import localeFi from '@angular/common/locales/fi';
import localeFr from '@angular/common/locales/fr';
import localeHe from '@angular/common/locales/he';
import localeHr from '@angular/common/locales/hr';
import localeHu from '@angular/common/locales/hu';
import localeId from '@angular/common/locales/id';
import localeIt from '@angular/common/locales/it';
import localeJa from '@angular/common/locales/ja';
import localeKo from '@angular/common/locales/ko';
import localeLv from '@angular/common/locales/lv';
import localeMl from '@angular/common/locales/ml';
import localeNb from '@angular/common/locales/nb';
import localeNl from '@angular/common/locales/nl';
import localePl from '@angular/common/locales/pl';
import localePtBr from '@angular/common/locales/pt';
import localePtPt from '@angular/common/locales/pt-PT';
import localeRo from '@angular/common/locales/ro';
import localeRu from '@angular/common/locales/ru';
import localeSk from '@angular/common/locales/sk';
import localeSr from '@angular/common/locales/sr';
import localeSv from '@angular/common/locales/sv';
import localeTr from '@angular/common/locales/tr';
import localeUk from '@angular/common/locales/uk';
import localeZhCn from '@angular/common/locales/zh-Hans';
import localeZhTw from '@angular/common/locales/zh-Hant';
registerLocaleData(localeCa, 'ca');
registerLocaleData(localeCs, 'cs');
registerLocaleData(localeBg, 'bg');
registerLocaleData(localeDa, 'da');
registerLocaleData(localeDe, 'de');
registerLocaleData(localeEl, 'el');
registerLocaleData(localeEnGb, 'en-GB');
registerLocaleData(localeEnIn, 'en-IN');
registerLocaleData(localeEs, 'es');
registerLocaleData(localeEt, 'et');
registerLocaleData(localeEo, 'eo');
registerLocaleData(localeFi, 'fi');
registerLocaleData(localeFr, 'fr');
registerLocaleData(localeHe, 'he');
registerLocaleData(localeHr, 'hr');
registerLocaleData(localeHu, 'hu');
registerLocaleData(localeId, 'id');
registerLocaleData(localeIt, 'it');
registerLocaleData(localeJa, 'ja');
registerLocaleData(localeKo, 'ko');
registerLocaleData(localeLv, 'lv');
registerLocaleData(localeMl, 'ml');
registerLocaleData(localeNb, 'nb');
registerLocaleData(localeNl, 'nl');
registerLocaleData(localePl, 'pl');
registerLocaleData(localePtBr, 'pt-BR');
registerLocaleData(localePtPt, 'pt-PT');
registerLocaleData(localeRo, 'ro');
registerLocaleData(localeRu, 'ru');
registerLocaleData(localeSk, 'sk');
registerLocaleData(localeSr, 'sr');
registerLocaleData(localeSv, 'sv');
registerLocaleData(localeTr, 'tr');
registerLocaleData(localeUk, 'uk');
registerLocaleData(localeZhCn, 'zh-CN');
registerLocaleData(localeZhTw, 'zh-TW');
@@ -229,25 +272,24 @@ registerLocaleData(localeZhTw, 'zh-TW');
FormsModule,
AppRoutingModule,
ServicesModule,
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
pageTracking: {
clearQueryParams: true,
},
}),
ToasterModule.forRoot(),
InfiniteScrollModule,
DragDropModule,
],
declarations: [
A11yTitleDirective,
AcceptEmergencyComponent,
AccessComponent,
AcceptOrganizationComponent,
AccountComponent,
SetPasswordComponent,
AddCreditComponent,
AddEditComponent,
AdjustPaymentComponent,
AdjustSeatsComponent,
AdjustStorageComponent,
ApiActionDirective,
ApiKeyComponent,
AppComponent,
AttachmentsComponent,
AutofocusDirective,
@@ -255,8 +297,10 @@ registerLocaleData(localeZhTw, 'zh-TW');
BlurClickDirective,
BoxRowDirective,
BreachReportComponent,
BulkActionsComponent,
BulkDeleteComponent,
BulkMoveComponent,
BulkRestoreComponent,
BulkShareComponent,
CalloutComponent,
ChangeEmailComponent,
@@ -272,6 +316,13 @@ registerLocaleData(localeZhTw, 'zh-TW');
DeleteOrganizationComponent,
DomainRulesComponent,
DownloadLicenseComponent,
EmergencyAccessAddEditComponent,
EmergencyAccessAttachmentsComponent,
EmergencyAccessComponent,
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
EmergencyAccessViewComponent,
EmergencyAddEditComponent,
ExportComponent,
ExposedPasswordsReportComponent,
FallbackSrcDirective,
@@ -285,6 +336,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
ImportComponent,
InactiveTwoFactorReportComponent,
InputVerbatimDirective,
LinkSsoComponent,
LockComponent,
LoginComponent,
ModalComponent,
@@ -292,7 +344,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
OptionsComponent,
OrgAccountComponent,
OrgAddEditComponent,
OrgApiKeyComponent,
OrganizationBillingComponent,
OrganizationPlansComponent,
OrganizationSubscriptionComponent,
@@ -316,7 +367,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgPolicyEditComponent,
OrgPoliciesComponent,
OrgReusedPasswordsReportComponent,
OrgRotateApiKeyComponent,
OrgSettingComponent,
OrgToolsComponent,
OrgTwoFactorSetupComponent,
@@ -342,10 +392,15 @@ registerLocaleData(localeZhTw, 'zh-TW');
SearchCiphersPipe,
SearchPipe,
SelectCopyDirective,
SendAddEditComponent,
SendComponent,
SendInfoComponent,
SettingsComponent,
ShareComponent,
SsoComponent,
StopClickDirective,
StopPropDirective,
TaxInfoComponent,
ToolsComponent,
TrueFalseValueDirective,
TwoFactorAuthenticatorComponent,
@@ -355,8 +410,8 @@ registerLocaleData(localeZhTw, 'zh-TW');
TwoFactorOptionsComponent,
TwoFactorRecoveryComponent,
TwoFactorSetupComponent,
TwoFactorU2fComponent,
TwoFactorVerifyComponent,
TwoFactorWebAuthnComponent,
TwoFactorYubiKeyComponent,
UnsecuredWebsitesReportComponent,
UpdateKeyComponent,
@@ -372,18 +427,25 @@ registerLocaleData(localeZhTw, 'zh-TW');
],
entryComponents: [
AddEditComponent,
ApiKeyComponent,
AttachmentsComponent,
BulkActionsComponent,
BulkDeleteComponent,
BulkMoveComponent,
BulkRestoreComponent,
BulkShareComponent,
CollectionsComponent,
DeauthorizeSessionsComponent,
DeleteAccountComponent,
DeleteOrganizationComponent,
EmergencyAccessAddEditComponent,
EmergencyAccessAttachmentsComponent,
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
EmergencyAddEditComponent,
FolderAddEditComponent,
ModalComponent,
OrgAddEditComponent,
OrgApiKeyComponent,
OrgAttachmentsComponent,
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
@@ -391,23 +453,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

@@ -1,12 +0,0 @@
import { NgModule } from '@angular/core';
import { ModalComponent } from 'jslib/angular/components/modal.component';
@NgModule({
imports: [],
declarations: [
ModalComponent,
],
})
export class DummyModule {
}

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>
@@ -39,7 +42,7 @@
<i class="fa fa-fw fa-question-circle" aria-hidden="true"></i>
{{'getHelp' | i18n}}
</a>
<a class="dropdown-item" href="https://bitwarden.com#download" target="_blank" rel="noopener">
<a class="dropdown-item" href="https://bitwarden.com/download/" target="_blank" rel="noopener">
<i class="fa fa-fw fa-download" aria-hidden="true"></i>
{{'getApps' | i18n}}
</a>

View File

@@ -1,45 +1,56 @@
<app-navbar></app-navbar>
<div class="org-nav" *ngIf="organization">
<div class="container d-flex flex-column">
<div class="my-auto d-flex align-items-center pl-1">
<app-avatar [data]="organization.name" size="45" [circle]="true"></app-avatar>
<div class="org-name ml-3">
<span>{{organization.name}}</span>
<small class="text-muted">{{'organization' | i18n}}</small>
</div>
<div class="ml-auto card border-danger text-danger bg-transparent" *ngIf="!organization.enabled">
<div class="card-body py-2">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
{{'organizationIsDisabled' | i18n}}
<div class="container d-flex">
<div class="d-flex flex-column">
<div class="my-auto d-flex align-items-center pl-1">
<app-avatar [data]="organization.name" size="45" [circle]="true"></app-avatar>
<div class="org-name ml-3">
<span>{{organization.name}}</span>
<small class="text-muted">{{'organization' | i18n}}</small>
</div>
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!organization.enabled">
<div class="card-body py-2">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
{{'organizationIsDisabled' | i18n}}
</div>
</div>
</div>
<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" *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="showToolsTab">
<a class="nav-link" [routerLink]="toolsRoute" routerLinkActive="active">
<i class="fa fa-wrench" aria-hidden="true"></i>
{{'tools' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="organization.isOwner">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="fa fa-cogs" aria-hidden="true"></i>
{{'settings' | i18n}}
</a>
</li>
</ul>
</div>
<div class="ml-auto d-flex align-items-center">
<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>
</div>
<ul class="nav nav-tabs" *ngIf="organization.isManager">
<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">
<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">
<i class="fa fa-wrench" aria-hidden="true"></i>
{{'tools' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="organization.isOwner">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="fa fa-cogs" aria-hidden="true"></i>
{{'settings' | i18n}}
</a>
</li>
</ul>
</div>
</div>
<router-outlet></router-outlet>

View File

@@ -4,10 +4,14 @@ import {
OnDestroy,
OnInit,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
import { ApiService } from 'jslib/abstractions/api.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
import { Organization } from 'jslib/models/domain/organization';
@@ -20,19 +24,28 @@ const BroadcasterSubscriptionId = 'OrganizationLayoutComponent';
})
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
organization: Organization;
businessTokenPromise: Promise<any>;
private organizationId: string;
private businessUrl: string;
constructor(private route: ActivatedRoute, private userService: UserService,
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
private broadcasterService: BroadcasterService, private ngZone: NgZone,
private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
private environmentService: EnvironmentService) { }
ngOnInit() {
this.businessUrl = 'https://portal.bitwarden.com';
if (this.environmentService.enterpriseUrl != null) {
this.businessUrl = this.environmentService.enterpriseUrl;
} else if (this.environmentService.baseUrl != null) {
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();
});
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
@@ -51,4 +64,69 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
async load() {
this.organization = await this.userService.getOrganization(this.organizationId);
}
async goToBusinessPortal() {
if (this.businessTokenPromise != null) {
return;
}
try {
this.businessTokenPromise = this.apiService.getEnterprisePortalSignInToken();
const token = await this.businessTokenPromise;
if (token != null) {
const userId = await this.userService.getUserId();
this.platformUtilsService.launchUri(this.businessUrl + '/login?userId=' + userId +
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organization.id);
}
} catch { }
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

@@ -1,5 +1,5 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="collectionAddEditTitle">
<div class="modal-dialog" role="document">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="collectionAddEditTitle">{{title}}</h2>
@@ -14,7 +14,8 @@
<div class="modal-body" *ngIf="!loading">
<div class="form-group">
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required
appAutofocus>
</div>
<div class="form-group">
<label for="externalId">{{'externalId' | i18n}}</label>
@@ -41,6 +42,7 @@
<tr>
<th>&nbsp;</th>
<th>{{'name' | i18n}}</th>
<th width="100" class="text-center">{{'hidePasswords' | i18n}}</th>
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
</tr>
</thead>
@@ -58,6 +60,10 @@
<span class="sr-only">{{'groupAccessAllItems' | i18n}}</span>
</ng-container>
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="g.hidePasswords"
name="Groups[{{i}}].HidePasswords" [disabled]="!g.checked || g.accessAll">
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly"
[disabled]="!g.checked || g.accessAll">

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,14 +63,15 @@ 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;
(group[0] as any).hidePasswords = s.hidePasswords;
}
});
}
@@ -81,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;
}
@@ -97,11 +96,12 @@ export class CollectionAddEditComponent implements OnInit {
(g as any).checked = select == null ? !(g as any).checked : select;
if (!(g as any).checked) {
(g as any).readOnly = false;
(g as any).hidePasswords = false;
}
}
selectAll(select: boolean) {
this.groups.forEach((g) => this.check(g, select));
this.groups.forEach(g => this.check(g, select));
}
async submit() {
@@ -112,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));
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) {
@@ -122,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();
@@ -144,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

@@ -16,9 +16,11 @@
<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 && (collections | search:searchText:'name':'id') as searchedCollections">
<ng-container
*ngIf="!loading && (isPaging() ? pagedCollections : collections | search:searchText:'name':'id') as searchedCollections">
<p *ngIf="!searchedCollections.length">{{'noCollectionsInList' | i18n}}</p>
<table class="table table-hover table-list" *ngIf="searchedCollections.length">
<table class="table table-hover table-list" *ngIf="searchedCollections.length" infiniteScroll
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<tbody>
<tr *ngFor="let c of searchedCollections">
<td>

View File

@@ -8,12 +8,12 @@ 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';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { UserService } from 'jslib/abstractions/user.service';
import { CollectionData } from 'jslib/models/data/collectionData';
@@ -34,27 +34,32 @@ import { EntityUsersComponent } from './entity-users.component';
templateUrl: 'collections.component.html',
})
export class CollectionsComponent implements OnInit {
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
@ViewChild('usersTemplate', { read: ViewContainerRef }) usersModalRef: ViewContainerRef;
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef;
loading = true;
organizationId: string;
collections: CollectionView[];
pagedCollections: CollectionView[];
searchText: string;
protected didScroll = false;
protected pageSize = 100;
private pagedCollectionsCount = 0;
private modal: ModalComponent = null;
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 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();
@@ -66,17 +71,35 @@ 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();
this.loading = false;
}
loadMore() {
if (!this.collections || this.collections.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedCollections.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedCollectionsCount > this.pageSize) {
pagedSize = this.pagedCollectionsCount;
}
if (this.collections.length > pagedLength) {
this.pagedCollections =
this.pagedCollections.concat(this.collections.slice(pagedLength, pagedLength + pagedSize));
}
this.pagedCollectionsCount = this.pagedCollections.length;
this.didScroll = this.pagedCollections.length > this.pageSize;
}
edit(collection: CollectionView) {
if (this.modal != null) {
this.modal.close();
@@ -117,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 { }
@@ -147,10 +169,28 @@ export class CollectionsComponent implements OnInit {
});
}
async resetPaging() {
this.pagedCollections = [];
this.loadMore();
}
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.collections && this.collections.length > this.pageSize;
}
private removeCollection(collection: CollectionView) {
const index = this.collections.indexOf(collection);
if (index > -1) {
this.collections.splice(index, 1);
this.resetPaging();
}
}
}

View File

@@ -1,5 +1,5 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="eventLogsTitle">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="eventLogsTitle">

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,9 +94,9 @@ export class EntityEventsComponent implements OnInit {
} catch { }
this.continuationToken = response.continuationToken;
const events = response.data.map((r) => {
const events = await Promise.all(response.data.map(async r => {
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = this.eventService.getEventInfo(r);
const eventInfo = await this.eventService.getEventInfo(r);
const user = this.showUser && userId != null && this.orgUsersUserIdMap.has(userId) ?
this.orgUsersUserIdMap.get(userId) : null;
return {
@@ -110,7 +110,7 @@ export class EntityEventsComponent implements OnInit {
ip: r.ipAddress,
type: r.type,
};
});
}));
if (!clearExisting && this.events != null && this.events.length > 0) {
this.events = this.events.concat(events);

View File

@@ -1,5 +1,5 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAccessTitle">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="userAccessTitle">
@@ -47,6 +47,8 @@
<th>{{'name' | i18n}}</th>
<th *ngIf="entity === 'collection'">&nbsp;</th>
<th>{{'userType' | i18n}}</th>
<th width="100" class="text-center" *ngIf="entity === 'collection'">{{'hidePasswords' |
i18n}}</th>
<th width="100" class="text-center" *ngIf="entity === 'collection'">{{'readOnly' |
i18n}}</th>
</tr>
@@ -84,6 +86,12 @@
<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"
name="{{u.id.substr(0,8)}}_HidePasswords"
[disabled]="u.accessAll || !u.checked">
</td>
<td class="text-center" *ngIf="entity === 'collection'">
<input type="checkbox" [(ngModel)]="u.readOnly" name="{{u.id.substr(0,8)}}_ReadOnly"

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,17 +72,18 @@ 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;
(user[0] as any).hidePasswords = s.hidePasswords;
}
});
}
}
this.allUsers.forEach((u) => {
this.allUsers.forEach(u => {
if (this.entity === 'collection' && u.accessAll) {
(u as any).checked = true;
}
@@ -107,6 +107,7 @@ export class EntityUsersComponent implements OnInit {
} else {
if (this.entity === 'collection') {
(u as any).readOnly = false;
(u as any).hidePasswords = false;
}
this.selectedCount--;
}
@@ -119,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));
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

@@ -15,6 +15,10 @@
<i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"></i>
{{'refresh' | i18n}}
</button>
<button #exportBtn [appApiAction]="exportPromise" type="button" class="btn btn-sm btn-outline-primary ml-3"
(click)="exportEvents()" [disabled]="loaded && refreshBtn.loading">
{{'export' | i18n}}
</button>
</div>
</div>
<ng-container *ngIf="!loaded">

View File

@@ -7,13 +7,16 @@ import { ActivatedRoute, Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { ApiService } from 'jslib/abstractions/api.service';
import { ExportService } from 'jslib/abstractions/export.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
import { EventService } from '../../services/event.service';
import { EventResponse } from 'jslib/models/response/eventResponse';
import { ListResponse } from 'jslib/models/response/listResponse';
import { EventView } from 'jslib/models/view/eventView';
@Component({
selector: 'app-org-events',
@@ -23,23 +26,24 @@ export class EventsComponent implements OnInit {
loading = true;
loaded = false;
organizationId: string;
events: any[];
events: EventView[];
start: string;
end: string;
continuationToken: string;
refreshPromise: Promise<any>;
exportPromise: Promise<any>;
morePromise: Promise<any>;
private orgUsersUserIdMap = new Map<string, any>();
private orgUsersIdMap = new Map<string, any>();
constructor(private apiService: ApiService, private route: ActivatedRoute,
private eventService: EventService, private i18nService: I18nService,
private toasterService: ToasterService, private userService: UserService,
constructor(private apiService: ApiService, private route: ActivatedRoute, private eventService: EventService,
private i18nService: I18nService, private toasterService: ToasterService, private userService: UserService,
private exportService: ExportService, private platformUtilsService: PlatformUtilsService,
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 +59,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 });
@@ -64,8 +68,26 @@ export class EventsComponent implements OnInit {
this.loaded = true;
}
async exportEvents() {
if (this.appApiPromiseUnfulfilled()) {
return;
}
this.loading = true;
this.exportPromise = this.exportService.getEventExport(this.events).then(data => {
const fileName = this.exportService.getFileName('org-events', 'csv');
this.platformUtilsService.saveFile(window, data, { type: 'text/plain' }, fileName);
});
try {
await this.exportPromise;
} catch { }
this.exportPromise = null;
this.loading = false;
}
async loadEvents(clearExisting: boolean) {
if (this.refreshPromise != null || this.morePromise != null) {
if (this.appApiPromiseUnfulfilled()) {
return;
}
@@ -92,13 +114,14 @@ export class EventsComponent implements OnInit {
} catch { }
this.continuationToken = response.continuationToken;
const events = response.data.map((r) => {
const events = await Promise.all(response.data.map(async r => {
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = this.eventService.getEventInfo(r);
const eventInfo = await this.eventService.getEventInfo(r);
const user = userId != null && this.orgUsersUserIdMap.has(userId) ?
this.orgUsersUserIdMap.get(userId) : null;
return {
return new EventView({
message: eventInfo.message,
humanReadableMessage: eventInfo.humanReadableMessage,
appIcon: eventInfo.appIcon,
appName: eventInfo.appName,
userId: userId,
@@ -107,8 +130,8 @@ export class EventsComponent implements OnInit {
date: r.date,
ip: r.ipAddress,
type: r.type,
};
});
});
}));
if (!clearExisting && this.events != null && this.events.length > 0) {
this.events = this.events.concat(events);
@@ -120,4 +143,8 @@ export class EventsComponent implements OnInit {
this.morePromise = null;
this.refreshPromise = null;
}
private appApiPromiseUnfulfilled() {
return this.refreshPromise != null || this.morePromise != null || this.exportPromise != null;
}
}

View File

@@ -1,5 +1,5 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="groupAddEditTitle">
<div class="modal-dialog" role="document">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="groupAddEditTitle">{{title}}</h2>
@@ -24,6 +24,10 @@
<h3 class="mt-4 d-flex">
<div class="mb-2">
{{'accessControl' | i18n}}
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
href="https://bitwarden.com/help/article/user-types-access-control/#access-control">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="ml-auto" *ngIf="access === 'selected' && collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
@@ -59,6 +63,7 @@
<tr>
<th>&nbsp;</th>
<th>{{'name' | i18n}}</th>
<th width="100" class="text-center">{{'hidePasswords' | i18n}}</th>
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
</tr>
</thead>
@@ -71,6 +76,10 @@
<td (click)="check(c)">
{{c.name}}
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="c.hidePasswords"
name="Collection[{{i}}].HidePasswords" [disabled]="!c.checked">
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly"
[disabled]="!c.checked">

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,11 +57,12 @@ 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;
collection[0].hidePasswords = s.hidePasswords;
}
});
}
@@ -76,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);
}
@@ -89,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() {
@@ -98,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));
request.collections = this.collections.filter(c => (c as any).checked)
.map(c => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
}
try {
@@ -109,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();
@@ -131,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

@@ -16,9 +16,10 @@
<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 && (groups | search:searchText:'name':'id') as searchedGroups">
<ng-container *ngIf="!loading && (isPaging() ? pagedGroups : groups | search:searchText:'name':'id') as searchedGroups">
<p *ngIf="!searchedGroups.length">{{'noGroupsInList' | i18n}}</p>
<table class="table table-hover table-list" *ngIf="searchedGroups.length">
<table class="table table-hover table-list" *ngIf="searchedGroups.length" infiniteScroll
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<tbody>
<tr *ngFor="let g of searchedGroups">
<td>

View File

@@ -11,11 +11,11 @@ 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';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { UserService } from 'jslib/abstractions/user.service';
import { GroupResponse } from 'jslib/models/response/groupResponse';
@@ -31,24 +31,29 @@ import { GroupAddEditComponent } from './group-add-edit.component';
templateUrl: 'groups.component.html',
})
export class GroupsComponent implements OnInit {
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
@ViewChild('usersTemplate', { read: ViewContainerRef }) usersModalRef: ViewContainerRef;
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef;
loading = true;
organizationId: string;
groups: GroupResponse[];
pagedGroups: GroupResponse[];
searchText: string;
protected didScroll = false;
protected pageSize = 100;
private pagedGroupsCount = 0;
private modal: ModalComponent = null;
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 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) {
@@ -56,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();
@@ -70,9 +75,26 @@ export class GroupsComponent implements OnInit {
const groups = response.data != null && response.data.length > 0 ? response.data : [];
groups.sort(Utils.getSortFunction(this.i18nService, 'name'));
this.groups = groups;
this.resetPaging();
this.loading = false;
}
loadMore() {
if (!this.groups || this.groups.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedGroups.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedGroupsCount > this.pageSize) {
pagedSize = this.pagedGroupsCount;
}
if (this.groups.length > pagedLength) {
this.pagedGroups = this.pagedGroups.concat(this.groups.slice(pagedLength, pagedLength + pagedSize));
}
this.pagedGroupsCount = this.pagedGroups.length;
this.didScroll = this.pagedGroups.length > this.pageSize;
}
edit(group: GroupResponse) {
if (this.modal != null) {
this.modal.close();
@@ -113,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 { }
@@ -142,10 +163,28 @@ export class GroupsComponent implements OnInit {
});
}
async resetPaging() {
this.pagedGroups = [];
this.loadMore();
}
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.groups && this.groups.length > this.pageSize;
}
private removeGroup(group: GroupResponse) {
const index = this.groups.indexOf(group);
if (index > -1) {
this.groups.splice(index, 1);
this.resetPaging();
}
}
}

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

@@ -25,6 +25,31 @@
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
[(ngModel)]="searchText">
</div>
<div class="dropdown ml-3" appListDropdown>
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsButton"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
{{'reinviteSelected' | i18n}}
</button>
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{'remove' | i18n}}
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
{{'selectAll' | i18n}}
</button>
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
{{'unselectAll' | i18n}}
</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'inviteUser' | i18n}}
@@ -35,15 +60,20 @@
<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 && (users | search:searchText:'name':'email':'id') as searchedUsers">
<ng-container
*ngIf="!loading && (isPaging() ? pagedUsers : users | search:searchText:'name':'email':'id') as searchedUsers">
<p *ngIf="!searchedUsers.length">{{'noUsersInList' | i18n}}</p>
<ng-container *ngIf="searchedUsers.length">
<app-callout type="info" title="{{'confirmUsers' | i18n}}" icon="fa-check-circle" *ngIf="showConfirmUsers">
{{'usersNeedConfirmed' | i18n}}
</app-callout>
<table class="table table-hover table-list">
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<tbody>
<tr *ngFor="let u of searchedUsers">
<td (click)="checkUser(u)" class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="u.checked" appStopProp>
</td>
<td width="30">
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true"
[fontSize]="14"></app-avatar>
@@ -67,6 +97,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,19 +11,21 @@ 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';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { UserService } from 'jslib/abstractions/user.service';
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
import { OrganizationUserBulkRequest } from 'jslib/models/request/organizationUserBulkRequest';
import { OrganizationUserUserDetailsResponse } from 'jslib/models/response/organizationUserResponse';
import { OrganizationUserStatusType } from 'jslib/enums/organizationUserStatusType';
@@ -37,19 +39,22 @@ import { UserAddEditComponent } from './user-add-edit.component';
import { UserConfirmComponent } from './user-confirm.component';
import { UserGroupsComponent } from './user-groups.component';
const MaxCheckedCount = 500;
@Component({
selector: 'app-org-people',
templateUrl: 'people.component.html',
})
export class PeopleComponent implements OnInit {
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
@ViewChild('groupsTemplate', { read: ViewContainerRef }) groupsModalRef: ViewContainerRef;
@ViewChild('eventsTemplate', { read: ViewContainerRef }) eventsModalRef: ViewContainerRef;
@ViewChild('confirmTemplate', { read: ViewContainerRef }) confirmModalRef: ViewContainerRef;
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
@ViewChild('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef;
loading = true;
organizationId: string;
users: OrganizationUserUserDetailsResponse[];
pagedUsers: OrganizationUserUserDetailsResponse[];
searchText: string;
status: OrganizationUserStatusType = null;
statusMap = new Map<OrganizationUserStatusType, OrganizationUserUserDetailsResponse[]>();
@@ -59,21 +64,25 @@ export class PeopleComponent implements OnInit {
accessEvents = false;
accessGroups = false;
protected didScroll = false;
protected pageSize = 100;
private pagedUsersCount = 0;
private modal: ModalComponent = null;
private allUsers: OrganizationUserUserDetailsResponse[];
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 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;
}
@@ -81,10 +90,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]);
}
@@ -101,7 +110,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 {
@@ -119,10 +128,29 @@ export class PeopleComponent implements OnInit {
} else {
this.users = this.allUsers;
}
// Reset checkbox selecton
this.selectAll(false);
this.resetPaging();
}
loadMore() {
if (!this.users || this.users.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedUsers.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedUsersCount > this.pageSize) {
pagedSize = this.pagedUsersCount;
}
if (this.users.length > pagedLength) {
this.pagedUsers = this.pagedUsers.concat(this.users.slice(pagedLength, pagedLength + pagedSize));
}
this.pagedUsersCount = this.pagedUsers.length;
this.didScroll = this.pagedUsers.length > this.pageSize;
}
get allCount() {
return this.allUsers.length;
return this.allUsers != null ? this.allUsers.length : 0;
}
get invitedCount() {
@@ -206,22 +234,84 @@ export class PeopleComponent implements OnInit {
return false;
}
this.actionPromise = this.apiService.deleteOrganizationUser(this.organizationId, user.id);
try {
await this.apiService.deleteOrganizationUser(this.organizationId, user.id);
this.analytics.eventTrack.next({ action: 'Deleted User' });
await this.actionPromise;
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', user.name || user.email));
this.removeUser(user);
} catch { }
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async reinvite(user: OrganizationUserUserDetailsResponse) {
if (this.actionPromise != null) {
return;
}
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));
try {
await this.actionPromise;
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenReinvited', user.name || user.email));
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async bulkRemove() {
if (this.actionPromise != null) {
return;
}
const users = this.getCheckedUsers();
if (users.length <= 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('noSelectedUsersApplicable'));
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('removeSelectedUsersConfirmation'), this.i18nService.t('remove'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
const request = new OrganizationUserBulkRequest(users.map(user => user.id));
this.actionPromise = this.apiService.deleteManyOrganizationUsers(this.organizationId, request);
try {
await this.actionPromise;
this.toasterService.popAsync('success', null, this.i18nService.t('usersHasBeenRemoved'));
await this.load();
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async bulkReinvite() {
if (this.actionPromise != null) {
return;
}
const users = this.getCheckedUsers().filter(u => u.status === OrganizationUserStatusType.Invited);
if (users.length <= 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('noSelectedUsersApplicable'));
return;
}
const request = new OrganizationUserBulkRequest(users.map(user => user.id));
this.actionPromise = this.apiService.postManyOrganizationUserReinvite(this.organizationId, request);
try {
await this.actionPromise;
this.toasterService.popAsync('success', null, this.i18nService.t('usersHasBeenReinvited'));
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
@@ -235,6 +325,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;
}
@@ -254,9 +358,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(() => {
@@ -265,12 +374,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) {
@@ -294,15 +410,41 @@ export class PeopleComponent implements OnInit {
});
}
private async doConfirmation(user: OrganizationUserUserDetailsResponse) {
async resetPaging() {
this.pagedUsers = [];
this.loadMore();
}
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.users && this.users.length > this.pageSize;
}
checkUser(user: OrganizationUserUserDetailsResponse, select?: boolean) {
(user as any).checked = select == null ? !(user as any).checked : select;
}
selectAll(select: boolean) {
if (select) {
this.selectAll(false);
}
const selectCount = select && this.users.length > MaxCheckedCount
? MaxCheckedCount
: this.users.length;
for (let i = 0; i < selectCount; i++) {
this.checkUser(this.users[i], select);
}
}
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;
@@ -313,6 +455,7 @@ export class PeopleComponent implements OnInit {
let index = this.users.indexOf(user);
if (index > -1) {
this.users.splice(index, 1);
this.resetPaging();
}
if (this.statusMap.has(OrganizationUserStatusType.Accepted)) {
index = this.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
@@ -333,4 +476,8 @@ export class PeopleComponent implements OnInit {
}
}
}
private getCheckedUsers() {
return this.users.filter(u => (u as any).checked);
}
}

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';
@@ -28,12 +29,18 @@ import { PolicyEditComponent } from './policy-edit.component';
templateUrl: 'policies.component.html',
})
export class PoliciesComponent implements OnInit {
@ViewChild('editTemplate', { read: ViewContainerRef }) editModalRef: ViewContainerRef;
@ViewChild('editTemplate', { read: ViewContainerRef, static: true }) editModalRef: ViewContainerRef;
loading = true;
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

@@ -1,5 +1,5 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="policiesEditTitle">
<div class="modal-dialog" role="document">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="policiesEditTitle">{{'editPolicy' | i18n}} - {{name}}</h2>
@@ -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

@@ -1,5 +1,5 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog" [ngClass]="{'modal-lg': !editMode}" role="document">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title" id="userAddEditTitle">
@@ -19,11 +19,18 @@
<p>{{'inviteUserDesc' | i18n}}</p>
<div class="form-group mb-4">
<label for="emails">{{'email' | i18n}}</label>
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required>
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required
appAutoFocus>
<small class="text-muted">{{'inviteMultipleEmailDesc' | i18n : '20'}}</small>
</div>
</ng-container>
<h3>{{'userType' | i18n}}</h3>
<h3>
{{'userType' | i18n}}
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
href="https://bitwarden.com/help/article/user-types-access-control/#user-types">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</h3>
<div class="form-check mt-2 form-check-block">
<input class="form-check-input" type="radio" name="userType" id="userTypeUser"
[value]="organizationUserType.User" [(ngModel)]="type">
@@ -56,9 +63,141 @@
<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">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</div>
<div class="ml-auto" *ngIf="access === 'selected' && collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
@@ -94,6 +233,7 @@
<tr>
<th>&nbsp;</th>
<th>{{'name' | i18n}}</th>
<th width="100" class="text-center">{{'hidePasswords' | i18n}}</th>
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
</tr>
</thead>
@@ -106,6 +246,10 @@
<td (click)="check(c)">
{{c.name}}
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="c.hidePasswords"
name="Collection[{{i}}].HidePasswords" [disabled]="!c.checked">
</td>
<td class="text-center">
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly"
[disabled]="!c.checked">
@@ -120,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,12 +67,16 @@ 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;
collection[0].hidePasswords = s.hidePasswords;
}
});
}
@@ -80,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);
}
@@ -93,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));
collections = this.collections.filter(c => (c as any).checked)
.map(c => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
}
try {
@@ -109,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 {
@@ -116,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();
@@ -142,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">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<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

@@ -1,5 +1,5 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="groupAccessTitle">
<div class="modal-dialog" role="document">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h2 class="modal-title" id="groupAccessTitle">

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

@@ -10,17 +10,23 @@
<div class="col-6">
<div class="form-group">
<label for="name">{{'organizationName' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="org.name">
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="org.name"
[disabled]="selfHosted">
</div>
<div class="form-group">
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
<input id="billingEmail" class="form-control" type="text" name="BillingEmail"
[(ngModel)]="org.billingEmail">
[(ngModel)]="org.billingEmail" [disabled]="selfHosted">
</div>
<div class="form-group">
<label for="businessName">{{'businessName' | i18n}}</label>
<input id="businessName" class="form-control" type="text" name="BusinessName"
[(ngModel)]="org.businessName">
[(ngModel)]="org.businessName" [disabled]="selfHosted">
</div>
<div class="form-group">
<label for="identifier">{{'identifier' | i18n}}</label>
<input id="identifier" class="form-control" type="text" name="Identifier"
[(ngModel)]="org.identifier">
</div>
</div>
<div class="col-6">
@@ -49,9 +55,17 @@
<h1>{{'taxInformation' | i18n}}</h1>
</div>
<p>{{'taxInformationDesc' | i18n}}</p>
<a href="https://bitwarden.com/contact/" target="_blank" rel="noopener" class="btn btn-outline-secondary">
{{'contactSupport' | i18n}}
</a>
<div *ngIf="!org || loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</div>
<form *ngIf="org && !loading" #formTax (ngSubmit)="submitTaxInfo()" [appApiAction]="taxFormPromise" ngNativeValidate>
<app-tax-info></app-tax-info>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="formTax.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
</form>
<div class="secondary-header text-danger border-0 mb-0">
<h1>{{'dangerZone' | i18n}}</h1>
</div>

View File

@@ -7,46 +7,50 @@ 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';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpdateRequest';
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 { ApiKeyComponent } from './api-key.component';
import { TaxInfoComponent } from '../../settings/tax-info.component';
import { DeleteOrganizationComponent } from './delete-organization.component';
import { RotateApiKeyComponent } from './rotate-api-key.component';
@Component({
selector: 'app-org-account',
templateUrl: 'account.component.html',
})
export class AccountComponent {
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
@ViewChild('apiKeyTemplate', { read: ViewContainerRef }) apiKeyModalRef: ViewContainerRef;
@ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef }) rotateApiKeyModalRef: ViewContainerRef;
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef, static: true }) deleteModalRef: ViewContainerRef;
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef, static: true }) purgeModalRef: ViewContainerRef;
@ViewChild('apiKeyTemplate', { read: ViewContainerRef, static: true }) apiKeyModalRef: ViewContainerRef;
@ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef, static: true }) rotateApiKeyModalRef: ViewContainerRef;
@ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent;
selfHosted = false;
loading = true;
canUseApi = false;
org: OrganizationResponse;
formPromise: Promise<any>;
taxFormPromise: Promise<any>;
private organizationId: string;
private modal: ModalComponent = null;
constructor(private componentFactoryResolver: ComponentFactoryResolver,
private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private route: ActivatedRoute, private syncService: SyncService) { }
private toasterService: ToasterService, private route: ActivatedRoute,
private syncService: SyncService, private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.selfHosted = this.platformUtilsService.isSelfHost();
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
try {
this.org = await this.apiService.getOrganization(this.organizationId);
@@ -62,15 +66,21 @@ export class AccountComponent {
request.name = this.org.name;
request.businessName = this.org.businessName;
request.billingEmail = this.org.billingEmail;
request.identifier = this.org.identifier;
this.formPromise = this.apiService.putOrganization(this.organizationId, request).then(() => {
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 { }
}
async submitTaxInfo() {
this.taxFormPromise = this.taxInfo.submitTaxInfo();
await this.taxFormPromise;
this.toasterService.popAsync('success', null, this.i18nService.t('taxInfoUpdated'));
}
deleteOrganization() {
if (this.modal != null) {
this.modal.close();
@@ -110,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;
@@ -124,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';
@@ -33,14 +32,14 @@ export class AdjustSeatsComponent {
@Output() onAdjusted = new EventEmitter<number>();
@Output() onCanceled = new EventEmitter();
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
seatAdjustment = 0;
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

@@ -4,8 +4,8 @@
aria-hidden="true">&times;</span></button>
<h2 class="card-body-header">{{'changeBillingPlan' | i18n}}</h2>
<p class="mb-0">{{'changeBillingPlanUpgrade' | i18n}}</p>
<app-organization-plans [showFree]="false" [showCancel]="true" plan="families" [organizationId]="organizationId"
(onCanceled)="cancel()">
<app-organization-plans [showFree]="false" [showCancel]="true" [plan]="defaultUpgradePlan"
[product]="defaultUpgradeProduct" [organizationId]="organizationId" (onCanceled)="cancel()">
</app-organization-plans>
</div>
</div>

View File

@@ -8,6 +8,9 @@ import {
import { ApiService } from 'jslib/abstractions/api.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PlanType } from 'jslib/enums/planType';
import { ProductType } from 'jslib/enums/productType';
@Component({
selector: 'app-change-plan',
templateUrl: 'change-plan.component.html',
@@ -18,12 +21,13 @@ export class ChangePlanComponent {
@Output() onCanceled = new EventEmitter();
formPromise: Promise<any>;
defaultUpgradePlan: PlanType = PlanType.FamiliesAnnually;
defaultUpgradeProduct: ProductType = ProductType.Families;
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService) { }
async submit() {
try {
this.platformUtilsService.eventTrack('Changed Plan');
this.onChanged.emit();
} catch { }
}

View File

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

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

@@ -24,7 +24,7 @@
</app-callout>
<dl *ngIf="selfHosted">
<dt>{{'billingPlan' | i18n}}</dt>
<dd>{{sub.plan}}</dd>
<dd>{{sub.plan.name}}</dd>
<dt>{{'expiration' | i18n}}</dt>
<dd *ngIf="sub.expiration">
{{sub.expiration | date:'mediumDate'}}
@@ -39,7 +39,7 @@
<div class="col-4">
<dl>
<dt>{{'billingPlan' | i18n}}</dt>
<dd>{{sub.plan}}</dd>
<dd>{{sub.plan.name}}</dd>
<ng-container *ngIf="subscription">
<dt>{{'status' | i18n}}</dt>
<dd>

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';
@@ -13,7 +12,6 @@ import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { TokenService } from 'jslib/abstractions/token.service';
import { PlanType } from 'jslib/enums/planType';
@@ -38,15 +36,14 @@ export class OrganizationSubscriptionComponent implements OnInit {
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
constructor(private tokenService: TokenService, private apiService: ApiService,
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
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;
@@ -76,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 { }
@@ -96,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 { }
@@ -192,34 +187,20 @@ export class OrganizationSubscriptionComponent implements OnInit {
}
get billingInterval() {
const monthly = this.sub.planType === PlanType.EnterpriseMonthly ||
this.sub.planType === PlanType.TeamsMonthly;
const monthly = !this.sub.plan.isAnnual;
return monthly ? 'month' : 'year';
}
get storageGbPrice() {
return this.billingInterval === 'month' ? 0.5 : 4;
return this.sub.plan.additionalStoragePricePerGb;
}
get seatPrice() {
switch (this.sub.planType) {
case PlanType.EnterpriseMonthly:
return 4;
case PlanType.EnterpriseAnnually:
return 36;
case PlanType.TeamsMonthly:
return 2.5;
case PlanType.TeamsAnnually:
return 24;
default:
return 0;
}
return this.sub.plan.seatPrice;
}
get canAdjustSeats() {
return this.sub.planType === PlanType.EnterpriseMonthly ||
this.sub.planType === PlanType.EnterpriseAnnually ||
this.sub.planType === PlanType.TeamsMonthly || this.sub.planType === PlanType.TeamsAnnually;
return this.sub.plan.hasAdditionalSeatsOption;
}
get canDownloadLicense() {

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();
});

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