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

Compare commits

..

100 Commits

Author SHA1 Message Date
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
139 changed files with 30503 additions and 3804 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

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

@@ -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: 57ace40845...abb54f0073

2621
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "bitwarden-web",
"version": "2.15.1",
"version": "2.17.1",
"license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web",
"scripts": {
@@ -28,10 +28,11 @@
"lint:fix": "tslint src/**/*.ts --fix"
},
"devDependencies": {
"@angular/compiler-cli": "^7.2.11",
"@ngtools/webpack": "^7.2.2",
"@types/jquery": "^3.3.6",
"@angular/compiler-cli": "^9.1.12",
"@ngtools/webpack": "^9.1.12",
"@types/jquery": "^3.5.1",
"@types/lunr": "^2.3.3",
"@types/node": "^10.17.28",
"@types/node-forge": "^0.7.5",
"@types/papaparse": "^4.5.3",
"@types/webcrypto": "^0.0.28",
@@ -43,45 +44,44 @@
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"del": "^3.0.0",
"mini-css-extract-plugin": "^0.9.0",
"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",
"mini-css-extract-plugin": "^0.9.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",
"ts-loader": "^7.0.5",
"tslint": "^6.1.3",
"tslint-loader": "^3.5.4",
"typescript": "3.2.4",
"typescript": "3.8.3",
"webpack": "^4.29.0",
"webpack-cli": "^3.2.1",
"webpack-dev-server": "^3.1.14"
},
"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",
"@angular/animations": "9.1.12",
"@angular/cdk": "9.2.4",
"@angular/common": "9.1.12",
"@angular/compiler": "9.1.12",
"@angular/core": "9.1.12",
"@angular/forms": "9.1.12",
"@angular/platform-browser": "9.1.12",
"@angular/platform-browser-dynamic": "9.1.12",
"@angular/router": "9.1.12",
"@microsoft/signalr": "3.1.0",
"@microsoft/signalr-protocol-msgpack": "3.1.0",
"angular2-toaster": "6.1.0",
"angulartics2": "6.3.0",
"angular2-toaster": "8.0.0",
"angulartics2": "9.1.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",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git#410a9186cc34663c4913b17d6528067cd3331f1d",
"font-awesome": "4.7.0",
"jquery": "3.4.1",
"lunr": "2.3.3",
@@ -90,12 +90,13 @@
"papaparse": "4.6.0",
"popper.js": "1.14.4",
"qrious": "4.0.2",
"rxjs": "6.3.3",
"rxjs": "6.6.2",
"sweetalert2": "9.8.1",
"tslib": "^2.0.1",
"web-animations-js": "2.3.1",
"webcrypto-shim": "0.1.4",
"whatwg-fetch": "3.0.0",
"zone.js": "0.8.28",
"zone.js": "0.10.3",
"zxcvbn": "4.4.2"
}
}

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,6 +1,7 @@
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';
@@ -25,9 +26,9 @@ export class LockComponent extends BaseLockComponent {
userService: UserService, cryptoService: CryptoService,
storageService: StorageService, vaultTimeoutService: VaultTimeoutService,
environmentService: EnvironmentService, private routerService: RouterService,
stateService: StateService) {
stateService: StateService, apiService: ApiService) {
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService,
storageService, vaultTimeoutService, environmentService, stateService);
storageService, vaultTimeoutService, environmentService, stateService, apiService);
}
async ngOnInit() {

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,8 +23,13 @@ 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;
}

View File

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

View File

@@ -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,6 @@ import { PolicyData } from 'jslib/models/data/policyData';
})
export class RegisterComponent extends BaseRegisterComponent {
showCreateOrgMessage = false;
showTerms = true;
layout = '';
enforcedPolicyOptions: MasterPasswordPolicyOptions;
@@ -39,7 +39,6 @@ export class RegisterComponent extends BaseRegisterComponent {
passwordGenerationService: PasswordGenerationService, private policyService: PolicyService) {
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService,
passwordGenerationService);
this.showTerms = !platformUtilsService.isSelfHost();
}
getPasswordScoreAlertDisplay() {
@@ -64,6 +63,7 @@ export class RegisterComponent extends BaseRegisterComponent {
async ngOnInit() {
const queryParamsSub = this.route.queryParams.subscribe((qParams) => {
this.referenceData = new ReferenceEventRequest();
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
this.email = qParams.email;
}
@@ -71,19 +71,20 @@ 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 = qParams.layout;
this.layout = this.referenceData.layout = qParams.layout;
}
if (qParams.reference != null) {
this.referenceId = qParams.reference;
this.referenceData.id = qParams.reference;
} else {
this.referenceId = ('; ' + document.cookie).split('; reference=').pop().split(';').shift();
this.referenceData.id = ('; ' + document.cookie).split('; reference=').pop().split(';').shift();
}
if (this.referenceId === '') {
this.referenceId = null;
if (this.referenceData.id === '') {
this.referenceData.id = null;
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();

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

@@ -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;
}
@@ -66,7 +69,11 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
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

@@ -15,6 +15,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 +57,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';
@@ -99,6 +104,15 @@ 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],
@@ -130,6 +144,11 @@ const routes: Routes = [
canActivate: [UnauthGuardService],
data: { titleId: 'deleteAccount' },
},
{
path: 'send/:sendId/:key',
component: AccessComponent,
data: { title: 'Bitwarden Send' },
},
],
},
{
@@ -138,6 +157,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,

View File

@@ -1,5 +1,5 @@
import * as jq from 'jquery';
import Swal from 'sweetalert2/src/sweetalert2.js';
import Swal from 'sweetalert2/dist/sweetalert2.js';
import {
BodyOutputType,
@@ -203,6 +203,8 @@ export class AppComponent implements OnDestroy, OnInit {
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
this.i18nService.t('loginExpired'));
}
Swal.close();
this.router.navigate(['/']);
});
}

View File

@@ -34,6 +34,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 +60,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 +96,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 +112,7 @@ 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 { 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';
@@ -144,6 +150,7 @@ 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';
@@ -175,7 +182,10 @@ import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
import { registerLocaleData } from '@angular/common';
import {
registerLocaleData,
DatePipe,
} from '@angular/common';
import localeCa from '@angular/common/locales/ca';
import localeCs from '@angular/common/locales/cs';
import localeDa from '@angular/common/locales/da';
@@ -189,6 +199,8 @@ import localeHe from '@angular/common/locales/he';
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';
@@ -214,6 +226,8 @@ registerLocaleData(localeHe, 'he');
registerLocaleData(localeIt, 'it');
registerLocaleData(localeJa, 'ja');
registerLocaleData(localeKo, 'ko');
registerLocaleData(localeLv, 'lv');
registerLocaleData(localeMl, 'ml');
registerLocaleData(localeNb, 'nb');
registerLocaleData(localeNl, 'nl');
registerLocaleData(localePl, 'pl');
@@ -233,7 +247,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
FormsModule,
AppRoutingModule,
ServicesModule,
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
Angulartics2Module.forRoot({
pageTracking: {
clearQueryParams: true,
},
@@ -244,14 +258,17 @@ registerLocaleData(localeZhTw, 'zh-TW');
],
declarations: [
A11yTitleDirective,
AccessComponent,
AcceptOrganizationComponent,
AccountComponent,
SetPasswordComponent,
AddCreditComponent,
AddEditComponent,
AdjustPaymentComponent,
AdjustSeatsComponent,
AdjustStorageComponent,
ApiActionDirective,
ApiKeyComponent,
AppComponent,
AttachmentsComponent,
AutofocusDirective,
@@ -259,6 +276,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
BlurClickDirective,
BoxRowDirective,
BreachReportComponent,
BulkActionsComponent,
BulkDeleteComponent,
BulkMoveComponent,
BulkRestoreComponent,
@@ -290,6 +308,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
ImportComponent,
InactiveTwoFactorReportComponent,
InputVerbatimDirective,
LinkSsoComponent,
LockComponent,
LoginComponent,
ModalComponent,
@@ -297,7 +316,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
OptionsComponent,
OrgAccountComponent,
OrgAddEditComponent,
OrgApiKeyComponent,
OrganizationBillingComponent,
OrganizationPlansComponent,
OrganizationSubscriptionComponent,
@@ -321,7 +339,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgPolicyEditComponent,
OrgPoliciesComponent,
OrgReusedPasswordsReportComponent,
OrgRotateApiKeyComponent,
OrgSettingComponent,
OrgToolsComponent,
OrgTwoFactorSetupComponent,
@@ -347,8 +364,11 @@ registerLocaleData(localeZhTw, 'zh-TW');
SearchCiphersPipe,
SearchPipe,
SelectCopyDirective,
SendAddEditComponent,
SendComponent,
SettingsComponent,
ShareComponent,
SsoComponent,
StopClickDirective,
StopPropDirective,
TaxInfoComponent,
@@ -378,7 +398,9 @@ registerLocaleData(localeZhTw, 'zh-TW');
],
entryComponents: [
AddEditComponent,
ApiKeyComponent,
AttachmentsComponent,
BulkActionsComponent,
BulkDeleteComponent,
BulkMoveComponent,
BulkRestoreComponent,
@@ -390,7 +412,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
FolderAddEditComponent,
ModalComponent,
OrgAddEditComponent,
OrgApiKeyComponent,
OrgAttachmentsComponent,
OrgCollectionAddEditComponent,
OrgCollectionsComponent,
@@ -398,12 +419,12 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgEntityUsersComponent,
OrgGroupAddEditComponent,
OrgPolicyEditComponent,
OrgRotateApiKeyComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
OrgUserGroupsComponent,
PasswordGeneratorHistoryComponent,
PurgeVaultComponent,
SendAddEditComponent,
ShareComponent,
TwoFactorAuthenticatorComponent,
TwoFactorDuoComponent,
@@ -414,7 +435,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
TwoFactorYubiKeyComponent,
UpdateKeyComponent,
],
providers: [],
providers: [DatePipe],
bootstrap: [AppComponent],
})
export class AppModule { }

View File

@@ -8,6 +8,11 @@
<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</a>
</li>
-->
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/tools">{{'tools' | i18n}}</a>
</li>
@@ -39,7 +44,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="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 class="ml-auto d-flex align-items-center">
<button class="btn btn-primary" (click)="goToEnterprisePortal()" #enterpriseBtn
[appApiAction]="enterpriseTokenPromise" *ngIf="organization.useBusinessPortal">
<i class="fa fa-bank fa-fw" [hidden]="enterpriseBtn.loading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin fa-fw" [hidden]="!enterpriseBtn.loading" title="{{'loading' | i18n}}"
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,11 +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';
@@ -21,20 +24,21 @@ const BroadcasterSubscriptionId = 'OrganizationLayoutComponent';
})
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
organization: Organization;
enterpriseTokenPromise: Promise<any>;
private organizationId: string;
private enterpriseUrl: string;
constructor(private route: ActivatedRoute, private userService: UserService,
private broadcasterService: BroadcasterService, private environmentService: EnvironmentService,
private ngZone: NgZone) { }
private broadcasterService: BroadcasterService, private ngZone: NgZone,
private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
private environmentService: EnvironmentService) { }
ngOnInit() {
this.enterpriseUrl = 'https://enterprise.bitwarden.com';
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 + '/enterprise';
this.enterpriseUrl = this.environmentService.baseUrl + '/portal';
}
document.body.classList.remove('layout_frontend');
@@ -42,7 +46,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
this.organizationId = params.organizationId;
await this.load();
});
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
@@ -61,4 +64,20 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
async load() {
this.organization = await this.userService.getOrganization(this.organizationId);
}
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.organization.id);
}
} catch { }
this.enterpriseTokenPromise = null;
}
}

View File

@@ -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>

View File

@@ -35,8 +35,8 @@ 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;
@@ -85,7 +85,7 @@ export class CollectionsComponent implements OnInit {
}
loadMore() {
if (this.collections.length <= this.pageSize) {
if (!this.collections || this.collections.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedCollections.length;
@@ -185,7 +185,7 @@ export class CollectionsComponent implements OnInit {
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.collections.length > this.pageSize;
return !searching && this.collections && this.collections.length > this.pageSize;
}
private removeCollection(collection: CollectionView) {

View File

@@ -32,8 +32,8 @@ 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;
@@ -81,7 +81,7 @@ export class GroupsComponent implements OnInit {
}
loadMore() {
if (this.groups.length <= this.pageSize) {
if (!this.groups || this.groups.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedGroups.length;
@@ -179,7 +179,7 @@ export class GroupsComponent implements OnInit {
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.groups.length > this.pageSize;
return !searching && this.groups && this.groups.length > this.pageSize;
}
private removeGroup(group: GroupResponse) {

View File

@@ -43,10 +43,10 @@ import { UserGroupsComponent } from './user-groups.component';
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;
@@ -129,7 +129,7 @@ export class PeopleComponent implements OnInit {
}
loadMore() {
if (this.users.length <= this.pageSize) {
if (!this.users || this.users.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedUsers.length;
@@ -331,7 +331,7 @@ export class PeopleComponent implements OnInit {
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.users.length > this.pageSize;
return !searching && this.users && this.users.length > this.pageSize;
}
private async doConfirmation(user: OrganizationUserUserDetailsResponse) {

View File

@@ -1,3 +1,8 @@
<app-callout [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

@@ -13,6 +13,7 @@ import {
import { PolicyType } from 'jslib/enums/policyType';
import { ApiService } from 'jslib/abstractions/api.service';
import { EnvironmentService } from 'jslib/abstractions';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { UserService } from 'jslib/abstractions/user.service';
@@ -28,41 +29,25 @@ 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>;
private enterpriseUrl: string;
private modal: ModalComponent = null;
private orgPolicies: PolicyResponse[];
private policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
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) => {
@@ -72,8 +57,53 @@ export class PoliciesComponent implements OnInit {
this.router.navigate(['/organizations', this.organizationId]);
return;
}
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,
},
];
await this.load();
});
// 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() {
@@ -102,6 +132,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 +142,22 @@ export class PoliciesComponent implements OnInit {
this.modal = null;
});
}
// Remove when removing deprecation warning
async goToEnterprisePortal() {
if (this.enterpriseTokenPromise != null) {
return;
}
try {
this.enterpriseTokenPromise = this.apiService.getEnterprisePortalSignInToken();
const token = await this.enterpriseTokenPromise;
if (token != null) {
const userId = await this.userService.getUserId();
this.platformUtilsService.launchUri(this.enterpriseUrl + '/login?userId=' + userId +
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organizationId);
}
} catch { }
this.enterpriseTokenPromise = null;
}
}

View File

@@ -17,6 +17,18 @@
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>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"

View File

@@ -27,6 +27,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;
@@ -127,45 +128,66 @@ 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;
default:
break;
}
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 { }
}
}
private preValidate(): boolean {
switch (this.type) {
case PolicyType.RequireSso:
if (!this.enabled) { // Don't need prevalidation checks if submitting to disable
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;
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

@@ -19,7 +19,8 @@
<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>

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">

View File

@@ -11,29 +11,30 @@ 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 { TaxInfoComponent } from '../../settings/tax-info.component';
import { ApiKeyComponent } from './api-key.component';
import { DeleteOrganizationComponent } from './delete-organization.component';
import { RotateApiKeyComponent } from './rotate-api-key.component';
@Component({
selector: 'app-org-account',
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;
@@ -46,9 +47,11 @@ export class AccountComponent {
constructor(private componentFactoryResolver: ComponentFactoryResolver,
private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private route: ActivatedRoute, private syncService: SyncService) { }
private route: ActivatedRoute, private syncService: SyncService,
private platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost();
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
try {
@@ -65,6 +68,7 @@ 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);
});
@@ -120,7 +124,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;
@@ -134,8 +145,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

@@ -33,7 +33,7 @@ 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>;

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,6 +21,8 @@ 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) { }

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

@@ -13,7 +13,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,10 +37,10 @@ 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,
private messagingService: MessagingService, private route: ActivatedRoute) {
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private analytics: Angulartics2,
private toasterService: ToasterService, private messagingService: MessagingService,
private route: ActivatedRoute) {
this.selfHosted = platformUtilsService.isSelfHost();
}
@@ -192,34 +191,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

@@ -82,10 +82,6 @@ export class CiphersComponent extends BaseCiphersComponent {
await this.resetPaging();
}
checkCipher(c: CipherView) {
// do nothing
}
events(c: CipherView) {
this.onEventsClicked.emit(c);
}

View File

@@ -19,10 +19,15 @@
</ng-container>
</small>
</h1>
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()"
*ngIf="!deleted">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
</button>
<div class="ml-auto d-flex">
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [modal]="modal" [deleted]="deleted"
[organization]="organization">
</app-vault-bulk-actions>
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()"
*ngIf="!deleted">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
</button>
</div>
</div>
<app-org-vault-ciphers (onCipherClicked)="editCipher($event)"
(onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"

View File

@@ -41,19 +41,19 @@ const BroadcasterSubscriptionId = 'OrgVaultComponent';
templateUrl: 'vault.component.html',
})
export class VaultComponent implements OnInit, OnDestroy {
@ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent;
@ViewChild(CiphersComponent) ciphersComponent: CiphersComponent;
@ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef;
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
@ViewChild('collections', { read: ViewContainerRef }) collectionsModalRef: ViewContainerRef;
@ViewChild('eventsTemplate', { read: ViewContainerRef }) eventsModalRef: ViewContainerRef;
@ViewChild(GroupingsComponent, { static: true }) groupingsComponent: GroupingsComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@ViewChild('attachments', { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef;
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
@ViewChild('collections', { read: ViewContainerRef, static: true }) collectionsModalRef: ViewContainerRef;
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
organization: Organization;
collectionId: string = null;
type: CipherType = null;
deleted: boolean = false;
private modal: ModalComponent = null;
modal: ModalComponent = null;
constructor(private route: ActivatedRoute, private userService: UserService,
private router: Router, private changeDetectorRef: ChangeDetectorRef,

View File

@@ -0,0 +1,59 @@
<form #form (ngSubmit)="load()" [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">Bitwarden Send</p>
<div class="card d-block">
<div class="card-body" *ngIf="loading" class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</div>
<div class="card-body" *ngIf="!loading && passwordRequired">
<p>{{'sendProtectedPassword' | i18n}}</p>
<p>{{'sendProtectedPasswordDontKnow' | i18n}}</p>
<div class="form-group">
<label for="password">{{'password' | i18n}}</label>
<input id="password" type="password" name="Password" class="text-monospace form-control"
[(ngModel)]="password" required appInputVerbatim appAutofocus>
</div>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span>
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'continue' | i18n}}
</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="card-body" *ngIf="!loading && !passwordRequired && send">
<p class="text-center"><b>{{send.name}}</b></p>
<hr>
<!-- Text -->
<ng-container *ngIf="send.type === sendType.Text">
<app-callout *ngIf="send.text.hidden" type="tip">{{'sendHiddenByDefault' | i18n}}</app-callout>
<div class="form-group">
<textarea id="text" rows="8" name="Text" [(ngModel)]="sendText" class="form-control"
readonly (click)="selectText()"></textarea>
</div>
<button class="btn btn-block btn-link" type="button" (click)="toggleText()"
*ngIf="send.text.hidden">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showText, 'fa-eye-slash': showText}"></i>
{{'toggleVisibility' | i18n}}
</button>
<button class="btn btn-block btn-link" type="button" (click)="copyText()">
<i class="fa fa-copy" aria-hidden="true"></i> {{'copyValue' | i18n}}
</button>
</ng-container>
<!-- File -->
<ng-container *ngIf="send.type === sendType.File">
<p>{{send.file.fileName}}</p>
<button class="btn btn-primary btn-block" type="button" (click)="download()">
<i class="fa fa-download" aria-hidden="true"></i>
{{'downloadFile' | i18n}} ({{send.file.sizeName}})</button>
</ng-container>
</div>
</div>
</div>
</div>
</form>

View File

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

View File

@@ -0,0 +1,129 @@
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="sendAddEditTitle">
<div class="modal-dialog modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate
autocomplete="off">
<div class="modal-header">
<h2 class="modal-title" id="sendAddEditTitle">{{title}}</h2>
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="send">
<div class="row" *ngIf="!editMode">
<div class="col-6 form-group">
<label for="type">{{'whatTypeOfSend' | i18n}}</label>
<select id="type" name="Type" [(ngModel)]="send.type" class="form-control" appAutofocus>
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="send.name" required>
</div>
</div>
<!-- Text -->
<ng-container *ngIf="send.type === sendType.Text">
<div class="form-group">
<label for="text">{{'sendTypeText' | i18n}}</label>
<textarea id="text" name="Text.Text" rows="6" [(ngModel)]="send.text.text"
class="form-control"></textarea>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="send.text.hidden"
id="text-hidden" name="Text.Hidden">
<label class="form-check-label" for="text-hidden">{{'cfTypeHidden' | i18n}}</label>
</div>
</div>
</ng-container>
<!-- File -->
<ng-container *ngIf="send.type === sendType.File">
<div class="form-group">
<div *ngIf="editMode">
<strong class="d-block">{{'file' | i18n}}</strong>
{{send.file.fileName}} ({{send.file.sizeName}})
</div>
<div *ngIf="!editMode">
<label for="file">{{'file' | i18n}}</label>
<input type="file" id="file" class="form-control-file" name="file" required>
<small class="form-text text-muted">{{'maxFileSize' | i18n}}</small>
</div>
</div>
</ng-container>
<h3 class="mt-4">{{'options' | i18n}}</h3>
<div class="row">
<div class="col-6 form-group">
<label for="deletionDate">{{'deletionDate' | i18n}}</label>
<input id="deletionDate" class="form-control" type="datetime-local" name="DeletionDate"
[(ngModel)]="deletionDate" required>
</div>
<div class="col-6 form-group">
<div class="d-flex">
<label for="expirationDate">{{'expirationDate' | i18n}}</label>
<a href="#" appStopClick (click)="clearExpiration()" class="ml-auto">
{{'clear' | i18n}}
</a>
</div>
<input id="expirationDate" class="form-control" type="datetime-local" name="ExpirationDate"
[(ngModel)]="expirationDate">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="maxAccessCount">{{'maxAccessCount' | i18n}}</label>
<input id="maxAccessCount" class="form-control" type="number" name="MaxAccessCount"
[(ngModel)]="send.maxAccessCount">
</div>
<div class="col-6 form-group" *ngIf="editMode">
<label for="accessCount">{{'currentAccessCount' | i18n}}</label>
<input id="accessCount" class="form-control" type="number" name="AccessCount" readonly
[(ngModel)]="send.accessCount">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="password" *ngIf="!hasPassword">{{'password' | i18n}}</label>
<label for="password" *ngIf="hasPassword">{{'newPassword' | i18n}}</label>
<input id="password" class="form-control" type="password" name="Password"
[(ngModel)]="password">
</div>
</div>
<div class="form-group">
<label for="notes">{{'notes' | i18n}}</label>
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="send.notes" class="form-control"></textarea>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" [(ngModel)]="send.disabled" id="disabled"
name="Disabled">
<label class="form-check-label" for="disabled">{{'disabled' | i18n}}</label>
</div>
</div>
<div class="form-group" *ngIf="link">
<label for="link">{{'sendLink' | i18n}}</label>
<input type="text" readonly id="link" name="Link" [(ngModel)]="link" class="form-control">
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{'cancel' | i18n}}
</button>
<div class="ml-auto" *ngIf="send">
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
[appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,192 @@
import { DatePipe } from '@angular/common';
import {
EventEmitter,
Input,
Output,
} from '@angular/core';
import { Component } from '@angular/core';
import { SendType } from 'jslib/enums/sendType';
import { ApiService } from 'jslib/abstractions/api.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SendService } from 'jslib/abstractions/send.service';
import { SendView } from 'jslib/models/view/sendView';
import { SendFileView } from 'jslib/models/view/sendFileView';
import { SendTextView } from 'jslib/models/view/sendTextView';
import { Send } from 'jslib/models/domain/send';
import { SendData } from 'jslib/models/data/sendData';
@Component({
selector: 'app-send-add-edit',
templateUrl: 'add-edit.component.html',
})
export class AddEditComponent {
@Input() sendId: string;
@Input() type: SendType;
@Output() onSavedSend = new EventEmitter<SendView>();
@Output() onDeletedSend = new EventEmitter<SendView>();
@Output() onCancelled = new EventEmitter<SendView>();
editMode: boolean = false;
send: SendView;
link: string;
title: string;
deletionDate: string;
expirationDate: string;
hasPassword: boolean;
password: string;
formPromise: Promise<any>;
deletePromise: Promise<any>;
sendType = SendType;
typeOptions: any[];
constructor(private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
private apiService: ApiService, private environmentService: EnvironmentService,
private datePipe: DatePipe, private sendService: SendService) {
this.typeOptions = [
{ name: i18nService.t('sendTypeFile'), value: SendType.File },
{ name: i18nService.t('sendTypeText'), value: SendType.Text },
];
}
async ngOnInit() {
await this.load();
}
async load() {
this.editMode = this.sendId != null;
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t('editSend');
} else {
this.title = this.i18nService.t('createSend');
}
if (this.send == null) {
if (this.editMode) {
const send = await this.loadSend();
this.send = await send.decrypt();
} else {
this.send = new SendView();
this.send.type = this.type == null ? SendType.File : this.type;
this.send.file = new SendFileView();
this.send.text = new SendTextView();
this.send.deletionDate = new Date();
this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7);
}
}
this.hasPassword = this.send.password != null && this.send.password.trim() !== '';
// Parse dates
this.deletionDate = this.send.deletionDate == null ? null :
this.datePipe.transform(this.send.deletionDate, 'yyyy-MM-ddTHH:mm');
this.expirationDate = this.send.expirationDate == null ? null :
this.datePipe.transform(this.send.expirationDate, 'yyyy-MM-ddTHH:mm');
if (this.editMode) {
let webVaultUrl = this.environmentService.getWebVaultUrl();
if (webVaultUrl == null) {
webVaultUrl = 'https://vault.bitwarden.com';
}
this.link = webVaultUrl + '/#/send/' + this.send.accessId + '/' + this.send.urlB64Key;
}
}
async submit(): Promise<boolean> {
if (this.send.name == null || this.send.name === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nameRequired'));
return false;
}
let file: File = null;
if (this.send.type === SendType.File && !this.editMode) {
const fileEl = document.getElementById('file') as HTMLInputElement;
const files = fileEl.files;
if (files == null || files.length === 0) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('selectFile'));
return;
}
file = files[0];
if (file.size > 104857600) { // 100 MB
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('maxFileSize'));
return;
}
}
const encSend = await this.encryptSend(file);
try {
this.formPromise = this.sendService.saveWithServer(encSend);
await this.formPromise;
this.send.id = encSend[0].id;
this.platformUtilsService.showToast('success', null,
this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend'));
this.onSavedSend.emit(this.send);
return true;
} catch { }
return false;
}
clearExpiration() {
this.expirationDate = null;
}
async delete(): Promise<void> {
if (this.deletePromise != null) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('deleteSendConfirmation'),
this.i18nService.t('deleteSend'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
try {
this.deletePromise = this.apiService.deleteSend(this.send.id);
await this.deletePromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
await this.load();
this.onDeletedSend.emit(this.send);
} catch { }
}
protected async loadSend(): Promise<Send> {
const response = await this.apiService.getSend(this.sendId);
const data = new SendData(response);
return new Send(data);
}
protected async encryptSend(file: File): Promise<[Send, ArrayBuffer]> {
const sendData = await this.sendService.encrypt(this.send, file, this.password, null);
// Parse dates
try {
sendData[0].deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate);
} catch {
sendData[0].deletionDate = null;
}
try {
sendData[0].expirationDate = this.expirationDate == null ? null : new Date(this.expirationDate);
} catch {
sendData[0].expirationDate = null;
}
return sendData;
}
}

View File

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

View File

@@ -0,0 +1,207 @@
import {
Component,
ComponentFactoryResolver,
OnInit,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { SendType } from 'jslib/enums/sendType';
import { SendView } from 'jslib/models/view/sendView';
import { AddEditComponent } from './add-edit.component';
import { ModalComponent } from '../modal.component';
import { ApiService } from 'jslib/abstractions/api.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SendService } from 'jslib/abstractions/send.service';
@Component({
selector: 'app-send',
templateUrl: 'send.component.html',
})
export class SendComponent implements OnInit {
@ViewChild('sendAddEdit', { read: ViewContainerRef, static: true }) sendAddEditModalRef: ViewContainerRef;
sendType = SendType;
loaded = false;
loading = true;
refreshing = false;
expired: boolean = false;
type: SendType = null;
sends: SendView[] = [];
filteredSends: SendView[] = [];
searchText: string;
selectedType: SendType;
selectedAll: boolean;
searchPlaceholder: string;
filter: (cipher: SendView) => boolean;
searchPending = false;
modal: ModalComponent = null;
actionPromise: any;
private searchTimeout: any;
constructor(private apiService: ApiService, private sendService: SendService,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private platformUtilsService: PlatformUtilsService, private environmentService: EnvironmentService) { }
async ngOnInit() {
await this.load();
}
async load(filter: (send: SendView) => boolean = null) {
this.loading = true;
const sends = await this.sendService.getAllDecrypted();
this.sends = sends;
this.selectAll();
this.loading = false;
this.loaded = true;
}
async reload(filter: (send: SendView) => boolean = null) {
this.loaded = false;
this.sends = [];
await this.load(filter);
}
async refresh() {
try {
this.refreshing = true;
await this.reload(this.filter);
} finally {
this.refreshing = false;
}
}
async applyFilter(filter: (send: SendView) => boolean = null) {
this.filter = filter;
await this.search(null);
}
async search(timeout: number = null) {
this.searchPending = false;
if (this.searchTimeout != null) {
clearTimeout(this.searchTimeout);
}
if (timeout == null) {
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
return;
}
this.searchPending = true;
this.searchTimeout = setTimeout(async () => {
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
this.searchPending = false;
}, timeout);
}
addSend() {
const component = this.editSend(null);
component.type = this.type;
}
editSend(send: SendView) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.sendAddEditModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<AddEditComponent>(
AddEditComponent, this.sendAddEditModalRef);
childComponent.sendId = send == null ? null : send.id;
childComponent.onSavedSend.subscribe(async (s: SendView) => {
this.modal.close();
await this.load();
});
childComponent.onDeletedSend.subscribe(async (s: SendView) => {
this.modal.close();
await this.load();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
return childComponent;
}
async removePassword(s: SendView): Promise<boolean> {
if (this.actionPromise != null || s.password == null) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('removePasswordConfirmation'),
this.i18nService.t('removePassword'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.apiService.putSendRemovePassword(s.id);
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('removedPassword'));
await this.load();
} catch { }
this.actionPromise = null;
}
async delete(s: SendView): Promise<boolean> {
if (this.actionPromise != null) {
return false;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('deleteSendConfirmation'),
this.i18nService.t('deleteSend'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.apiService.deleteSend(s.id);
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
await this.load();
} catch { }
this.actionPromise = null;
return true;
}
copy(s: SendView) {
let webVaultUrl = this.environmentService.getWebVaultUrl();
if (webVaultUrl == null) {
webVaultUrl = 'https://vault.bitwarden.com';
}
const link = webVaultUrl + '/#/send/' + s.accessId + '/' + s.urlB64Key;
this.platformUtilsService.copyToClipboard(link);
this.platformUtilsService.showToast('success', null,
this.i18nService.t('valueCopied', this.i18nService.t('sendLink')));
}
searchTextChanged() {
this.search(200);
}
selectAll() {
this.clearSelections();
this.selectedAll = true;
this.applyFilter(null);
}
selectType(type: SendType) {
this.clearSelections();
this.selectedType = type;
this.applyFilter((s) => s.type === type);
}
clearSelections() {
this.selectedAll = false;
this.selectedType = null;
}
}

View File

@@ -42,6 +42,7 @@ import { NotificationsService } from 'jslib/services/notifications.service';
import { PasswordGenerationService } from 'jslib/services/passwordGeneration.service';
import { PolicyService } from 'jslib/services/policy.service';
import { SearchService } from 'jslib/services/search.service';
import { SendService } from 'jslib/services/send.service';
import { SettingsService } from 'jslib/services/settings.service';
import { StateService } from 'jslib/services/state.service';
import { SyncService } from 'jslib/services/sync.service';
@@ -74,6 +75,7 @@ import {
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib/abstractions/platformUtils.service';
import { PolicyService as PolicyServiceAbstraction } from 'jslib/abstractions/policy.service';
import { SearchService as SearchServiceAbstraction } from 'jslib/abstractions/search.service';
import { SendService as SendServiceAbstraction } from 'jslib/abstractions/send.service';
import { SettingsService as SettingsServiceAbstraction } from 'jslib/abstractions/settings.service';
import { StateService as StateServiceAbstraction } from 'jslib/abstractions/state.service';
import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/storage.service';
@@ -93,7 +95,7 @@ const secureStorageService: StorageServiceAbstraction = new MemoryStorageService
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new WebCryptoFunctionService(window,
platformUtilsService);
const cryptoService = new CryptoService(storageService,
platformUtilsService.isDev() ? storageService : secureStorageService, cryptoFunctionService);
platformUtilsService.isDev() ? storageService : secureStorageService, cryptoFunctionService, platformUtilsService);
const tokenService = new TokenService(storageService);
const appIdService = new AppIdService(storageService);
const apiService = new ApiService(tokenService, platformUtilsService,
@@ -106,19 +108,21 @@ const cipherService = new CipherService(cryptoService, userService, settingsServ
const folderService = new FolderService(cryptoService, userService, apiService, storageService,
i18nService, cipherService);
const collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
searchService = new SearchService(cipherService, platformUtilsService);
searchService = new SearchService(cipherService);
const policyService = new PolicyService(userService, storageService);
const sendService = new SendService(cryptoService, userService, apiService, storageService,
i18nService, cryptoFunctionService);
const vaultTimeoutService = new VaultTimeoutService(cipherService, folderService, collectionService,
cryptoService, platformUtilsService, storageService, messagingService, searchService, userService, tokenService,
null, async () => messagingService.send('logout', { expired: false }));
const syncService = new SyncService(userService, apiService, settingsService,
folderService, cipherService, cryptoService, collectionService, storageService, messagingService, policyService,
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
sendService, async (expired: boolean) => messagingService.send('logout', { expired: expired }));
const passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, policyService);
const totpService = new TotpService(storageService, cryptoFunctionService);
const containerService = new ContainerService(cryptoService);
const authService = new AuthService(cryptoService, apiService,
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService);
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService);
const exportService = new ExportService(folderService, cipherService, apiService);
const importService = new ImportService(cipherService, folderService, apiService, i18nService, collectionService);
const notificationsService = new NotificationsService(userService, syncService, appIdService,
@@ -140,8 +144,8 @@ export function initFactory(): Function {
} else {
environmentService.notificationsUrl = isDev ? 'http://localhost:61840' :
'https://notifications.bitwarden.com'; // window.location.origin + '/notifications';
environmentService.enterpriseUrl = isDev ? 'http://localhost:61840' :
'https://enterprise.bitwarden.com'; // window.location.origin + '/enterprise';
environmentService.enterpriseUrl = isDev ? 'http://localhost:52313' :
'https://portal.bitwarden.com'; // window.location.origin + '/portal';
}
apiService.setUrls({
base: isDev ? null : window.location.origin,
@@ -218,6 +222,7 @@ export function initFactory(): Function {
{ provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService },
{ provide: EventLoggingServiceAbstraction, useValue: eventLoggingService },
{ provide: PolicyServiceAbstraction, useValue: policyService },
{ provide: SendServiceAbstraction, useValue: sendService },
{
provide: APP_INITIALIZER,
useFactory: initFactory,

View File

@@ -14,6 +14,14 @@
<h1>{{'encKeySettings' | i18n}}</h1>
</div>
<app-change-kdf></app-change-kdf>
<div class="secondary-header border-0 mb-0">
<h1>{{'apiKey' | i18n}}</h1>
</div>
<p>
{{'userApiKeyDesc' | i18n}}
</p>
<button type="button" class="btn btn-outline-secondary" (click)="viewUserApiKey()">{{'viewApiKey' | i18n}}</button>
<button type="button" class="btn btn-outline-secondary" (click)="rotateUserApiKey()">{{'rotateApiKey' | i18n}}</button>
<div class="secondary-header text-danger border-0 mb-0">
<h1>{{'dangerZone' | i18n}}</h1>
</div>
@@ -30,3 +38,5 @@
<ng-template #deauthorizeSessionsTemplate></ng-template>
<ng-template #purgeVaultTemplate></ng-template>
<ng-template #deleteAccountTemplate></ng-template>
<ng-template #viewUserApiKeyTemplate></ng-template>
<ng-template #rotateUserApiKeyTemplate></ng-template>

View File

@@ -6,22 +6,29 @@ import {
} from '@angular/core';
import { ModalComponent } from '../modal.component';
import { ApiKeyComponent } from './api-key.component';
import { DeauthorizeSessionsComponent } from './deauthorize-sessions.component';
import { DeleteAccountComponent } from './delete-account.component';
import { PurgeVaultComponent } from './purge-vault.component';
import { ApiService } from 'jslib/abstractions/api.service';
import { UserService } from 'jslib/abstractions/user.service';
@Component({
selector: 'app-account',
templateUrl: 'account.component.html',
})
export class AccountComponent {
@ViewChild('deauthorizeSessionsTemplate', { read: ViewContainerRef }) deauthModalRef: ViewContainerRef;
@ViewChild('purgeVaultTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
@ViewChild('deleteAccountTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
@ViewChild('deauthorizeSessionsTemplate', { read: ViewContainerRef, static: true }) deauthModalRef: ViewContainerRef;
@ViewChild('purgeVaultTemplate', { read: ViewContainerRef, static: true }) purgeModalRef: ViewContainerRef;
@ViewChild('deleteAccountTemplate', { read: ViewContainerRef, static: true }) deleteModalRef: ViewContainerRef;
@ViewChild('viewUserApiKeyTemplate', { read: ViewContainerRef, static: true }) viewUserApiKeyModalRef: ViewContainerRef;
@ViewChild('rotateUserApiKeyTemplate', { read: ViewContainerRef, static: true }) rotateUserApiKeyModalRef: ViewContainerRef;
private modal: ModalComponent = null;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
constructor(private componentFactoryResolver: ComponentFactoryResolver, private apiService: ApiService,
private userService: UserService) { }
deauthorizeSessions() {
if (this.modal != null) {
@@ -64,4 +71,49 @@ export class AccountComponent {
this.modal = null;
});
}
async viewUserApiKey() {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.viewUserApiKeyModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.viewUserApiKeyModalRef);
childComponent.keyType = 'user';
childComponent.entityId = await this.userService.getUserId();
childComponent.postKey = this.apiService.postUserApiKey.bind(this.apiService);
childComponent.scope = 'api';
childComponent.grantType = 'client_credentials';
childComponent.apiKeyTitle = 'apiKey';
childComponent.apiKeyWarning = 'userApiKeyWarning';
childComponent.apiKeyDescription = 'userApiKeyDesc';
this.modal.onClosed.subscribe(async () => {
this.modal = null;
});
}
async rotateUserApiKey() {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.rotateUserApiKeyModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.rotateUserApiKeyModalRef);
childComponent.keyType = 'user';
childComponent.isRotation = true;
childComponent.entityId = await this.userService.getUserId();
childComponent.postKey = this.apiService.postUserRotateApiKey.bind(this.apiService);
childComponent.scope = 'api';
childComponent.grantType = 'client_credentials';
childComponent.apiKeyTitle = 'apiKey';
childComponent.apiKeyWarning = 'userApiKeyWarning';
childComponent.apiKeyDescription = 'apiKeyRotateDesc';
this.modal.onClosed.subscribe(async () => {
this.modal = null;
});
}
}

View File

@@ -33,7 +33,7 @@ export class AddCreditComponent implements OnInit {
@Output() onAdded = new EventEmitter();
@Output() onCanceled = new EventEmitter();
@ViewChild('ppButtonForm', { read: ElementRef }) ppButtonFormRef: ElementRef;
@ViewChild('ppButtonForm', { read: ElementRef, static: true }) ppButtonFormRef: ElementRef;
paymentMethodType = PaymentMethodType;
ppButtonFormAction = WebConstants.paypal.buttonActionProduction;

View File

@@ -24,8 +24,8 @@ import { TaxInfoComponent } from './tax-info.component';
templateUrl: 'adjust-payment.component.html',
})
export class AdjustPaymentComponent {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
@Input() currentType?: PaymentMethodType;
@Input() organizationId: string;

View File

@@ -35,7 +35,7 @@ export class AdjustStorageComponent {
@Output() onAdjusted = new EventEmitter<number>();
@Output() onCanceled = new EventEmitter();
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
storageAdjustment = 0;
formPromise: Promise<any>;

View File

@@ -2,19 +2,19 @@
<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="apiKeyTitle">{{'apiKey' | i18n}}</h2>
<h2 class="modal-title" id="apiKeyTitle">{{apiKeyTitle | 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>{{'apiKeyDesc' | i18n}}</p>
<p>{{apiKeyDescription | 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="warning" *ngIf="clientSecret">{{apiKeyWarning | i18n}}</app-callout>
<app-callout type="info" title="{{'oauth2ClientCredentials' | i18n}}" icon="fa-key"
*ngIf="clientSecret">
<p class="mb-1">
@@ -31,7 +31,7 @@
</p>
<p class="mb-0">
<strong>grant_type:</strong><br>
<code>client_credentials</code>
<code>{{grantType}}</code>
</p>
</app-callout>
</div>
@@ -39,7 +39,7 @@
<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>{{'viewApiKey' | i18n}}</span>
<span>{{(isRotation ? 'rotateApiKey' : 'viewApiKey') | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>

View File

@@ -1,10 +1,8 @@
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';
@@ -17,17 +15,23 @@ import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse';
templateUrl: 'api-key.component.html',
})
export class ApiKeyComponent {
organizationId: string;
keyType: string;
isRotation: boolean;
postKey: (entityId: string, request: PasswordVerificationRequest) => Promise<ApiKeyResponse>;
entityId: string;
scope: string;
grantType: string;
apiKeyTitle: string;
apiKeyWarning: string;
apiKeyDescription: 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) { }
constructor(private i18nService: I18nService, private analytics: Angulartics2,
private toasterService: ToasterService, private cryptoService: CryptoService) { }
async submit() {
if (this.masterPassword == null || this.masterPassword === '') {
@@ -39,12 +43,11 @@ export class ApiKeyComponent {
const request = new PasswordVerificationRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
try {
this.formPromise = this.apiService.postOrganizationApiKey(this.organizationId, request);
this.formPromise = this.postKey(this.entityId, request);
const response = await this.formPromise;
this.clientSecret = response.apiKey;
this.clientId = 'organization.' + this.organizationId;
this.scope = 'api.organization';
this.analytics.eventTrack.next({ action: 'Viewed Organization API Key' });
this.clientId = `${this.keyType}.${this.entityId}`;
this.analytics.eventTrack.next({ action: `Viewed ${this.keyType} API Key` });
} catch { }
}
}

View File

@@ -28,18 +28,18 @@
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="newMasterPassword">{{'newMasterPass' | i18n}}</label>
<input id="newMasterPassword" type="password" name="NewMasterPasswordHash" class="form-control mb-1"
[(ngModel)]="newMasterPassword" (input)="updatePasswordStrength()" required appInputVerbatim
<label for="masterPassword">{{'newMasterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="NewMasterPasswordHash" class="form-control mb-1"
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required appInputVerbatim
autocomplete="new-password">
<app-password-strength [score]="masterPasswordScore" [showText]="true"></app-password-strength>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="confirmNewMasterPassword">{{'confirmNewMasterPass' | i18n}}</label>
<input id="confirmNewMasterPassword" type="password" name="ConfirmNewMasterPasswordHash"
class="form-control" [(ngModel)]="confirmNewMasterPassword" required appInputVerbatim
<label for="masterPasswordRetype">{{'confirmNewMasterPass' | i18n}}</label>
<input id="masterPasswordRetype" type="password" name="MasterPasswordRetype"
class="form-control" [(ngModel)]="masterPasswordRetype" required appInputVerbatim
autocomplete="new-password">
</div>
</div>

View File

@@ -1,10 +1,4 @@
import {
Component,
OnInit,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { Component } from '@angular/core';
import { ApiService } from 'jslib/abstractions/api.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
@@ -18,8 +12,11 @@ import { PolicyService } from 'jslib/abstractions/policy.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { UserService } from 'jslib/abstractions/user.service';
import {
ChangePasswordComponent as BaseChangePasswordComponent,
} from 'jslib/angular/components/change-password.component';
import { CipherString } from 'jslib/models/domain/cipherString';
import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordPolicyOptions';
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
import { CipherWithIdRequest } from 'jslib/models/request/cipherWithIdRequest';
@@ -31,136 +28,18 @@ import { UpdateKeyRequest } from 'jslib/models/request/updateKeyRequest';
selector: 'app-change-password',
templateUrl: 'change-password.component.html',
})
export class ChangePasswordComponent implements OnInit {
currentMasterPassword: string;
newMasterPassword: string;
confirmNewMasterPassword: string;
formPromise: Promise<any>;
masterPasswordScore: number;
export class ChangePasswordComponent extends BaseChangePasswordComponent {
rotateEncKey = false;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
currentMasterPassword: string;
private masterPasswordStrengthTimeout: any;
private email: string;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private messagingService: MessagingService,
private userService: UserService, private passwordGenerationService: PasswordGenerationService,
private platformUtilsService: PlatformUtilsService, private folderService: FolderService,
private cipherService: CipherService, private syncService: SyncService,
private policyService: PolicyService) { }
async ngOnInit() {
this.email = await this.userService.getEmail();
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions();
}
getPasswordScoreAlertDisplay() {
if (this.enforcedPolicyOptions == null) {
return '';
}
let str: string;
switch (this.enforcedPolicyOptions.minComplexity) {
case 4:
str = this.i18nService.t('strong');
break;
case 3:
str = this.i18nService.t('good');
break;
default:
str = this.i18nService.t('weak');
break;
}
return str + ' (' + this.enforcedPolicyOptions.minComplexity + ')';
}
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (!hasEncKey) {
this.toasterService.popAsync('error', null, this.i18nService.t('updateKey'));
return;
}
if (this.currentMasterPassword == null || this.currentMasterPassword === '' ||
this.newMasterPassword == null || this.newMasterPassword === '') {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return;
}
if (this.newMasterPassword.length < 8) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassLength'));
return;
}
if (this.newMasterPassword !== this.confirmNewMasterPassword) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassDoesntMatch'));
return;
}
const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword,
this.getPasswordStrengthUserInput());
if (this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
strengthResult.score,
this.newMasterPassword,
this.enforcedPolicyOptions)) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPasswordPolicyRequirementsNotMet'));
return;
}
if (strengthResult != null && strengthResult.score < 3) {
const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'),
this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'),
'warning');
if (!result) {
return;
}
}
if (this.rotateEncKey) {
await this.syncService.fullSync(true);
}
const request = new PasswordRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.currentMasterPassword, null);
const email = await this.userService.getEmail();
const kdf = await this.userService.getKdf();
const kdfIterations = await this.userService.getKdfIterations();
const newKey = await this.cryptoService.makeKey(this.newMasterPassword, email.trim().toLowerCase(),
kdf, kdfIterations);
request.newMasterPasswordHash = await this.cryptoService.hashPassword(this.newMasterPassword, newKey);
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
request.key = newEncKey[1].encryptedString;
try {
if (this.rotateEncKey) {
this.formPromise = this.apiService.postPassword(request).then(() => {
return this.updateKey(newKey, request.newMasterPasswordHash);
});
} else {
this.formPromise = this.apiService.postPassword(request);
}
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Changed Password' });
this.toasterService.popAsync('success', this.i18nService.t('masterPasswordChanged'),
this.i18nService.t('logBackIn'));
this.messagingService.send('logout');
} catch { }
}
updatePasswordStrength() {
if (this.masterPasswordStrengthTimeout != null) {
clearTimeout(this.masterPasswordStrengthTimeout);
}
this.masterPasswordStrengthTimeout = setTimeout(() => {
const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword,
this.getPasswordStrengthUserInput());
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
}, 300);
constructor(i18nService: I18nService,
cryptoService: CryptoService, messagingService: MessagingService,
userService: UserService, passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService, policyService: PolicyService,
private folderService: FolderService, private cipherService: CipherService,
private syncService: SyncService, private apiService: ApiService, ) {
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
platformUtilsService, policyService);
}
async rotateEncKeyClicked() {
@@ -198,13 +77,54 @@ export class ChangePasswordComponent implements OnInit {
}
}
private getPasswordStrengthUserInput() {
let userInput: string[] = [];
const atPosition = this.email.indexOf('@');
if (atPosition > -1) {
userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/));
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (!hasEncKey) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('updateKey'));
return;
}
await super.submit();
}
async setupSubmitActions() {
if (this.currentMasterPassword == null || this.currentMasterPassword === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return false;
}
if (this.rotateEncKey) {
await this.syncService.fullSync(true);
}
return super.setupSubmitActions();
}
async performSubmitActions(newMasterPasswordHash: string, newKey: SymmetricCryptoKey,
newEncKey: [SymmetricCryptoKey, CipherString]) {
const request = new PasswordRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.currentMasterPassword, null);
request.newMasterPasswordHash = newMasterPasswordHash;
request.key = newEncKey[1].encryptedString;
try {
if (this.rotateEncKey) {
this.formPromise = this.apiService.postPassword(request).then(() => {
return this.updateKey(newKey, request.newMasterPasswordHash);
});
} else {
this.formPromise = this.apiService.postPassword(request);
}
await this.formPromise;
this.platformUtilsService.showToast('success', this.i18nService.t('masterPasswordChanged'),
this.i18nService.t('logBackIn'));
this.messagingService.send('logout');
} catch {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
}
return userInput;
}
private async updateKey(key: SymmetricCryptoKey, masterPasswordHash: string) {

View File

@@ -12,7 +12,7 @@ import { OrganizationPlansComponent } from './organization-plans.component';
templateUrl: 'create-organization.component.html',
})
export class CreateOrganizationComponent implements OnInit {
@ViewChild(OrganizationPlansComponent) orgPlansComponent: OrganizationPlansComponent;
@ViewChild(OrganizationPlansComponent, { static: true }) orgPlansComponent: OrganizationPlansComponent;
constructor(private route: ActivatedRoute) { }

View File

@@ -0,0 +1,4 @@
<a class="dropdown-item" href="#" appStopClick (click)="submit(returnUri, true)">
<i class="fa fa-fw fa-link" aria-hidden="true"></i>
{{'linkSso' | i18n}}
</a>

View File

@@ -0,0 +1,48 @@
import {
AfterContentInit,
Component,
Input,
} 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 } from 'jslib/angular/components/sso.component';
import { Organization } from 'jslib/models/domain/organization';
@Component({
selector: 'app-link-sso',
templateUrl: 'link-sso.component.html',
})
export class LinkSsoComponent extends SsoComponent implements AfterContentInit {
@Input() organization: Organization;
returnUri: string = '/settings/organizations'
constructor(platformUtilsService: PlatformUtilsService, i18nService: I18nService,
apiService: ApiService, authService: AuthService,
router: Router, route: ActivatedRoute,
cryptoFunctionService: CryptoFunctionService, passwordGenerationService: PasswordGenerationService,
storageService: StorageService, stateService: StateService) {
super(authService, router,
i18nService, route,
storageService, stateService,
platformUtilsService, apiService,
cryptoFunctionService, passwordGenerationService);
this.returnUri = '/settings/organizations';
this.redirectUri = window.location.origin + '/sso-connector.html';
this.clientId = 'web';
}
async ngAfterContentInit() {
this.identifier = this.organization.identifier;
}
}

View File

@@ -1,3 +1,7 @@
<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="createOrganization && selfHosted">
<p>{{'uploadLicenseFileOrg' | i18n}}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
@@ -13,7 +17,8 @@
</button>
</form>
</ng-container>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate
*ngIf="!loading && !selfHosted && this.plans">
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
<div class="row" *ngIf="createOrganization">
<div class="form-group col-6">
@@ -38,69 +43,61 @@
</div>
</div>
<h2 class="mt-5">{{'chooseYourPlan' | i18n}}</h2>
<div class="form-check form-check-block" *ngIf="!ownedBusiness && showFree">
<input class="form-check-input" type="radio" name="PlanType" id="planFree" value="free" [(ngModel)]="plan"
(change)="changedPlan()">
<label class="form-check-label" for="planFree">
{{'planNameFree' | i18n}}
<small class="mb-1">{{'planDescFree' | i18n : '1'}}</small>
<small>• {{'limitedUsers' | i18n : '2'}}</small>
<small>• {{'limitedCollections' | i18n : '2'}}</small>
<span>{{'freeForever' | i18n}}</span>
<div *ngFor="let selectableProduct of selectableProducts" class="form-check form-check-block">
<input class="form-check-input" type="radio" name="product" id="product{{selectableProduct.product}}"
[value]="selectableProduct.product" [(ngModel)]="product" (change)="changedProduct()">
<label class="form-check-label" for="product{{selectableProduct.product}}">
{{ selectableProduct.nameLocalizationKey | i18n}}
<small class="mb-1">{{ selectableProduct.descriptionLocalizationKey | i18n : '1'}}</small>
<ng-container *ngIf="selectableProduct.product === productTypes.Enterprise; else fullFeatureList">
<small>• {{'includeAllTeamsFeatures' | i18n}}</small>
<small *ngIf="selectableProduct.hasSelfHost">{{'onPremHostingOptional' | i18n}}</small>
<small *ngIf="selectableProduct.hasSso">• {{'includeSsoAuthentication' | i18n}}</small>
<small *ngIf="selectableProduct.hasPolicies">• {{'includeEnterprisePolicies' | i18n}}</small>
</ng-container>
<ng-template #fullFeatureList>
<small *ngIf="selectableProduct.product == productTypes.Free">
{{'limitedUsers' | i18n : selectableProduct.maxUsers }}</small>
<small *ngIf="selectableProduct.product != productTypes.Free && selectableProduct.maxUsers">
{{'addShareLimitedUsers' | i18n : selectableProduct.maxUsers}}</small>
<small *ngIf="!selectableProduct.maxUsers">
{{'addShareUnlimitedUsers' | i18n}}</small>
<small *ngIf="selectableProduct.maxCollections">
{{'limitedCollections' | i18n : selectableProduct.maxCollections }}</small>
<small *ngIf="selectableProduct.maxAdditionalSeats">
{{'addShareLimitedUsers' | i18n : selectableProduct.maxAdditionalSeats }}</small>
<small *ngIf="!selectableProduct.maxCollections">• {{'createUnlimitedCollections' | i18n}}</small>
<small *ngIf="selectableProduct.baseStorageGb">
{{'gbEncryptedFileStorage' | i18n : selectableProduct.baseStorageGb + 'GB'}}</small>
<small *ngIf="selectableProduct.hasGroups">• {{'controlAccessWithGroups' | i18n}}</small>
<small *ngIf="selectableProduct.hasApi">• {{'trackAuditLogs' | i18n}}</small>
<small *ngIf="selectableProduct.hasDirectory">• {{'syncUsersFromDirectory' | i18n}}</small>
<small *ngIf="selectableProduct.hasSelfHost">• {{'onPremHostingOptional' | i18n}}</small>
<small *ngIf="selectableProduct.usersGetPremium">• {{'usersGetPremium' | i18n}}</small>
<small *ngIf="selectableProduct.product != productTypes.Free">
{{'priorityCustomerSupport' | i18n}}</small>
<small *ngIf="selectableProduct.trialPeriodDays">
{{'xDayFreeTrial' | i18n : selectableProduct.trialPeriodDays }}
</small>
</ng-template>
<span *ngIf="selectableProduct.product != productTypes.Free">
<ng-container *ngIf="selectableProduct.basePrice">
{{selectableProduct.basePrice / 12 | currency:'$'}} /{{'month' | i18n}},
{{'includesXUsers' | i18n : selectableProduct.baseSeats}}
<ng-container *ngIf="selectableProduct.hasAdditionalSeatsOption">
{{('additionalUsers' | i18n).toLowerCase()}}
{{selectableProduct.seatPrice / 12 | currency:'$'}} /{{'month' | i18n}}
</ng-container>
</ng-container>
</span>
<span *ngIf="!selectableProduct.basePrice && selectableProduct.hasAdditionalSeatsOption">
{{'costPerUser' | i18n : (selectableProduct.seatPrice / 12 | currency:'$')}} /{{'month' | i18n}}
</span>
<span *ngIf="selectableProduct.product == productTypes.Free">{{'freeForever' | i18n}}</span>
</label>
</div>
<div class="form-check form-check-block" *ngIf="!ownedBusiness">
<input class="form-check-input" type="radio" name="PlanType" id="planFamilies" value="families"
[(ngModel)]="plan" (change)="changedPlan()">
<label class="form-check-label" for="planFamilies">
{{'planNameFamilies' | i18n}}
<small class="mb-1">{{'planDescFamilies' | i18n}}</small>
<small>• {{'addShareLimitedUsers' | i18n : '5'}}</small>
<small>• {{'createUnlimitedCollections' | i18n}}</small>
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
<small>• {{'onPremHostingOptional' | i18n}}</small>
<small>• {{'priorityCustomerSupport' | i18n}}</small>
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
<span>{{1 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}}</span>
</label>
</div>
<div class="form-check form-check-block">
<input class="form-check-input" type="radio" name="PlanType" id="planTeams" value="teams" [(ngModel)]="plan"
(change)="changedPlan()">
<label class="form-check-label" for="planTeams">
{{'planNameTeams' | i18n}}
<small class="mb-1">{{'planDescTeams' | i18n}}</small>
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
<small>• {{'createUnlimitedCollections' | i18n}}</small>
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
<small>• {{'priorityCustomerSupport' | i18n}}</small>
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
<span>{{5 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}},
{{('additionalUsers' | i18n).toLowerCase()}}
{{2 | currency:'$'}} /{{'month' | i18n}}</span>
</label>
</div>
<div class="form-check form-check-block">
<input class="form-check-input" type="radio" name="PlanType" id="planEnterprise" value="enterprise"
[(ngModel)]="plan" (change)="changedPlan()">
<label class="form-check-label" for="planEnterprise">
{{'planNameEnterprise' | i18n}}
<small class="mb-1">{{'planDescEnterprise' | i18n}}</small>
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
<small>• {{'createUnlimitedCollections' | i18n}}</small>
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
<small>• {{'controlAccessWithGroups' | i18n}}</small>
<small>• {{'trackAuditLogs' | i18n}}</small>
<small>• {{'syncUsersFromDirectory' | i18n}}</small>
<small>• {{'onPremHostingOptional' | i18n}}</small>
<small>• {{'usersGetPremium' | i18n}}</small>
<small>• {{'priorityCustomerSupport' | i18n}}</small>
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
<span>{{'costPerUser' | i18n : (3 | currency:'$')}} /{{'month' | i18n}}</span>
</label>
</div>
<ng-container *ngIf="!plans[plan].noPayment">
<ng-container *ngIf="!plans[plan].noAdditionalSeats && !plans[plan].baseSeats">
<div *ngIf="product !== productTypes.Free">
<ng-container *ngIf="selectedPlan.hasAdditionalSeatsOption && !selectedPlan.baseSeats">
<h2 class="mt-5">{{'users' | i18n}}</h2>
<div class="row">
<div class="col-6">
@@ -113,13 +110,13 @@
</div>
</ng-container>
<h2 class="mt-5">{{'addons' | i18n}}</h2>
<div class="row" *ngIf="!plans[plan].noAdditionalSeats && plans[plan].baseSeats">
<div class="row" *ngIf="selectedPlan.hasAdditionalSeatsOption && selectedPlan.baseSeats">
<div class="form-group col-6">
<label for="additionalSeats">{{'additionalUserSeats' | i18n}}</label>
<input id="additionalSeats" class="form-control" type="number" name="AdditionalSeats"
[(ngModel)]="additionalSeats" min="0" max="100000" placeholder="{{'userSeatsDesc' | i18n}}">
<small
class="text-muted form-text">{{'userSeatsAdditionalDesc' | i18n : plans[plan].baseSeats : (plans[plan].seatPrice | currency:'$')}}</small>
class="text-muted form-text">{{'userSeatsAdditionalDesc' | i18n : selectedPlan.baseSeats : (seatPriceMonthly(selectedPlan) | currency:'$')}}</small>
</div>
</div>
<div class="row">
@@ -129,11 +126,11 @@
[(ngModel)]="additionalStorage" min="0" max="99" step="1"
placeholder="{{'additionalStorageGbDesc' | i18n}}">
<small
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGb.price | currency:'$') : ('month' | i18n)}}</small>
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (additionalStoragePriceMonthly(selectedPlan) | currency:'$') : ('month' | i18n)}}</small>
</div>
</div>
<div class="row">
<div class="form-group col-6" *ngIf="plans[plan].canBuyPremiumAccessAddon">
<div class="form-group col-6" *ngIf="selectedPlan.hasPremiumAccessOption">
<div class="form-check">
<input id="premiumAccess" class="form-check-input" type="checkbox" name="PremiumAccessAddon"
[(ngModel)]="premiumAccessAddon">
@@ -144,62 +141,77 @@
</div>
</div>
<h2 class="spaced-header">{{'summary' | i18n}}</h2>
<div class="form-check form-check-block">
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalAnnually" value="year"
[(ngModel)]="interval">
<label class="form-check-label" for="intervalAnnually">
{{'annually' | i18n}}
<small *ngIf="plans[plan].annualBasePrice">
{{'basePrice' | i18n}}: {{plans[plan].basePrice | currency:'$'}} &times;12 {{'monthAbbr' | i18n}} =
{{baseTotal(true) | currency:'$'}}
/{{'year' | i18n}}
</small>
<small *ngIf="!plans[plan].noAdditionalSeats">
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{plans[plan].seatPrice | currency:'$'}} &times;12
{{'monthAbbr' | i18n}} = {{seatTotal(true)
<div class="form-check form-check-block" *ngFor="let selectablePlan of selectablePlans">
<input class="form-check-input" type="radio" name="BillingInterval" id="interval{{selectablePlan.type}}"
[value]="selectablePlan.type" [(ngModel)]="plan">
<label class="form-check-label" for="interval{{selectablePlan.type}}">
<ng-container *ngIf="selectablePlan.isAnnual">
{{'annually' | i18n}}
<small *ngIf="selectablePlan.basePrice">
{{'basePrice' | i18n}}: {{ selectablePlan.basePrice / 12 | currency:'$'}} &times; 12
{{'monthAbbr' | i18n}}
=
{{selectablePlan.basePrice | currency:'$'}}
/{{'year' | i18n}}
</small>
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!selectablePlan.baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{selectablePlan.seatPrice / 12 | currency:'$'}} &times; 12
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
| currency:'$'}} /{{'year' | i18n}}
</small>
<small>
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{storageGb.price | currency:'$'}} &times;12 {{'monthAbbr'
| i18n}} = {{additionalStorageTotal(true) | currency:'$'}} /{{'year' | i18n}}
</small>
<small *ngIf="plans[plan].canBuyPremiumAccessAddon && premiumAccessAddon">
{{'premiumAccess' | i18n}}:
{{3.33 | currency:'$'}} &times;12 {{'monthAbbr' | i18n}} = {{40 | currency:'$'}} /{{'year' | i18n}}
</small>
</label>
</div>
<div class="form-check form-check-block" *ngIf="plans[plan].monthlySeatPrice">
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalMonthly" value="month"
[(ngModel)]="interval">
<label class="form-check-label" for="intervalMonthly">
{{'monthly' | i18n}}
<small *ngIf="plans[plan].monthlyBasePrice">
{{'basePrice' | i18n}}: {{baseTotal(false) | currency:'$'}} /{{'month' | i18n}}
</small>
<small *ngIf="!plans[plan].noAdditionalSeats">
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{plans[plan].monthlySeatPrice | currency:'$'}} =
{{seatTotal(false) | currency:'$'}} /{{'month'
| i18n}}
</small>
<small>
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{storageGb.monthlyPrice | currency:'$'}} = {{additionalStorageTotal(false)
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{selectablePlan.additionalStoragePricePerGb / 12 | currency:'$'}} &times; 12 {{'monthAbbr'
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
/{{'year' | i18n}}
</small>
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
{{'premiumAccess' | i18n}}:
{{selectablePlan.premiumAccessOptionCost / 12 | currency:'$'}} &times; 12 {{'monthAbbr' | i18n}}
=
{{40 | currency:'$'}}
/{{'year' | i18n}}
</small>
</ng-container>
<ng-container *ngIf="!selectablePlan.isAnnual">
{{'monthly' | i18n}}
<small *ngIf="selectablePlan.basePrice">
{{'basePrice' | i18n}}: {{selectablePlan.basePrice | currency:'$'}} {{'monthAbbr' | i18n}}
=
{{selectablePlan.basePrice | currency:'$'}}
/{{'month' | i18n}}
</small>
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!selectablePlan.baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{selectablePlan.seatPrice | currency:'$'}}
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
| currency:'$'}} /{{'month' | i18n}}
</small>
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{selectablePlan.additionalStoragePricePerGb | currency:'$'}} {{'monthAbbr'
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
/{{'month' | i18n}}
</small>
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
{{'premiumAccess' | i18n}}:
{{selectablePlan.premiumAccessOptionCost | currency:'$'}} {{'monthAbbr' | i18n}} =
{{40 | currency:'$'}}
/{{'month' | i18n}}
</small>
</ng-container>
</label>
</div>
<hr class="my-3">
<div class="text-lg">
<strong>{{'total' | i18n}}:</strong> {{total | currency:'USD $'}} /{{interval | i18n}}
<strong>{{'total' | i18n}}:</strong> {{subtotal | currency:'USD $'}} /{{selectedPlanInterval | i18n}}
</div>
<ng-container *ngIf="createOrganization">
<small class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (interval | i18n) }}</small>
<small
class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (selectedPlanInterval | i18n) }}</small>
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
<app-payment [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
@@ -209,8 +221,11 @@
</ng-container>
<small class="text-muted font-italic mt-2 d-block" *ngIf="!createOrganization">
{{'paymentCharged' | i18n : (interval | i18n) }}</small>
</ng-container>
<div [ngClass]="{'mt-4': !createOrganization || plans[plan].noPayment}">
</div>
<div *ngIf="singleOrgPolicyBlock" class="mt-4">
<app-callout [type]="'error'">{{'singleOrgBlockCreateMessage' | i18n}}</app-callout>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span>

View File

@@ -2,6 +2,7 @@ import {
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
} from '@angular/core';
@@ -16,91 +17,196 @@ 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 { PolicyService } from 'jslib/abstractions/policy.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { PaymentComponent } from './payment.component';
import { TaxInfoComponent } from './tax-info.component';
import { PlanType } from 'jslib/enums/planType';
import { PolicyType } from 'jslib/enums/policyType';
import { ProductType } from 'jslib/enums/productType';
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
import { OrganizationUpgradeRequest } from 'jslib/models/request/organizationUpgradeRequest';
import { PlanResponse } from 'jslib/models/response/planResponse';
@Component({
selector: 'app-organization-plans',
templateUrl: 'organization-plans.component.html',
})
export class OrganizationPlansComponent {
export class OrganizationPlansComponent implements OnInit {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
@Input() organizationId: string;
@Input() showFree = true;
@Input() showCancel = false;
@Input() plan = 'free';
@Input() product: ProductType = ProductType.Free;
@Input() plan: PlanType = PlanType.Free;
@Output() onSuccess = new EventEmitter();
@Output() onCanceled = new EventEmitter();
selfHosted = false;
ownedBusiness = false;
premiumAccessAddon = false;
storageGbPriceMonthly = 0.33;
additionalStorage = 0;
additionalSeats = 0;
interval = 'year';
loading: boolean = true;
selfHosted: boolean = false;
ownedBusiness: boolean = false;
premiumAccessAddon: boolean = false;
additionalStorage: number = 0;
additionalSeats: number = 0;
name: string;
billingEmail: string;
businessName: string;
storageGb: any = {
price: 0.33,
monthlyPrice: 0.50,
yearlyPrice: 4,
};
plans: any = {
free: {
basePrice: 0,
noAdditionalSeats: true,
noPayment: true,
},
families: {
basePrice: 1,
annualBasePrice: 12,
baseSeats: 5,
noAdditionalSeats: true,
annualPlanType: PlanType.FamiliesAnnually,
canBuyPremiumAccessAddon: true,
},
teams: {
basePrice: 5,
annualBasePrice: 60,
monthlyBasePrice: 8,
baseSeats: 5,
seatPrice: 2,
annualSeatPrice: 24,
monthlySeatPrice: 2.5,
monthPlanType: PlanType.TeamsMonthly,
annualPlanType: PlanType.TeamsAnnually,
},
enterprise: {
seatPrice: 3,
annualSeatPrice: 36,
monthlySeatPrice: 4,
monthPlanType: PlanType.EnterpriseMonthly,
annualPlanType: PlanType.EnterpriseAnnually,
},
};
productTypes = ProductType;
formPromise: Promise<any>;
singleOrgPolicyBlock: boolean = false;
plans: PlanResponse[];
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
private router: Router, private syncService: SyncService) {
private router: Router, private syncService: SyncService,
private policyService: PolicyService) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async ngOnInit() {
if (!this.selfHosted) {
const plans = await this.apiService.getPlans();
this.plans = plans.data;
}
this.loading = false;
}
get createOrganization() {
return this.organizationId == null;
}
get selectedPlan() {
return this.plans.find((plan) => plan.type === this.plan);
}
get selectedPlanInterval() {
return this.selectedPlan.isAnnual
? 'year'
: 'month';
}
get selectableProducts() {
let validPlans = this.plans.filter((plan) => plan.type !== PlanType.Custom);
if (this.ownedBusiness) {
validPlans = validPlans.filter((plan) => plan.canBeUsedByBusiness);
}
if (!this.showFree) {
validPlans = validPlans.filter((plan) => plan.product !== ProductType.Free);
}
validPlans = validPlans
.filter((plan) => !plan.legacyYear
&& !plan.disabled
&& (plan.isAnnual || plan.product === this.productTypes.Free));
return validPlans;
}
get selectablePlans() {
return this.plans.filter((plan) => !plan.legacyYear && !plan.disabled && plan.product === this.product);
}
additionalStoragePriceMonthly(selectedPlan: PlanResponse) {
if (!selectedPlan.isAnnual) {
return selectedPlan.additionalStoragePricePerGb;
}
return selectedPlan.additionalStoragePricePerGb / 12;
}
seatPriceMonthly(selectedPlan: PlanResponse) {
if (!selectedPlan.isAnnual) {
return selectedPlan.seatPrice;
}
return selectedPlan.seatPrice / 12;
}
additionalStorageTotal(plan: PlanResponse): number {
if (!plan.hasAdditionalStorageOption) {
return 0;
}
return plan.additionalStoragePricePerGb * Math.abs(this.additionalStorage || 0);
}
seatTotal(plan: PlanResponse): number {
if (!plan.hasAdditionalSeatsOption) {
return 0;
}
return plan.seatPrice * Math.abs(this.additionalSeats || 0);
}
get subtotal() {
let subTotal = this.selectedPlan.basePrice;
if (this.selectedPlan.hasAdditionalSeatsOption && this.additionalSeats) {
subTotal += this.seatTotal(this.selectedPlan);
}
if (this.selectedPlan.hasAdditionalStorageOption && this.additionalStorage) {
subTotal += this.additionalStorageTotal(this.selectedPlan);
}
if (this.selectedPlan.hasPremiumAccessOption && this.premiumAccessAddon) {
subTotal += this.selectedPlan.premiumAccessOptionPrice;
}
return subTotal;
}
changedProduct() {
this.plan = this.selectablePlans[0].type;
if (!this.selectedPlan.hasPremiumAccessOption) {
this.premiumAccessAddon = false;
}
if (!this.selectedPlan.hasAdditionalStorageOption) {
this.additionalStorage = 0;
}
if (!this.selectedPlan.hasAdditionalSeatsOption) {
this.additionalSeats = 0;
} else if (!this.additionalSeats && !this.selectedPlan.baseSeats &&
this.selectedPlan.hasAdditionalSeatsOption) {
this.additionalSeats = 1;
}
}
changedOwnedBusiness() {
if (!this.ownedBusiness || this.selectedPlan.canBeUsedByBusiness) {
return;
}
this.plan = PlanType.TeamsMonthly;
}
changedCountry() {
this.paymentComponent.hideBank = this.taxComponent.taxInfo.country !== 'US';
// Bank Account payments are only available for US customers
if (this.paymentComponent.hideBank &&
this.paymentComponent.method === PaymentMethodType.BankAccount) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
}
cancel() {
this.onCanceled.emit();
}
async submit() {
if (this.singleOrgPolicyBlock) {
return;
} else {
const policies = await this.policyService.getAll(PolicyType.SingleOrg);
this.singleOrgPolicyBlock = policies.some(policy => policy.enabled);
if (this.singleOrgPolicyBlock) {
return;
}
}
let files: FileList = null;
if (this.createOrganization && this.selfHosted) {
const fileEl = document.getElementById('file') as HTMLInputElement;
@@ -117,7 +223,7 @@ export class OrganizationPlansComponent {
let orgId: string = null;
if (this.createOrganization) {
let tokenResult: [string, PaymentMethodType] = null;
if (!this.selfHosted && this.plan !== 'free') {
if (!this.selfHosted && this.plan !== PlanType.Free) {
tokenResult = await this.paymentComponent.createPaymentToken();
}
const shareKey = await this.cryptoService.makeShareKey();
@@ -140,7 +246,7 @@ export class OrganizationPlansComponent {
request.name = this.name;
request.billingEmail = this.billingEmail;
if (this.plan === 'free') {
if (this.selectedPlan.type === PlanType.Free) {
request.planType = PlanType.Free;
} else {
request.paymentToken = tokenResult[0];
@@ -148,13 +254,9 @@ export class OrganizationPlansComponent {
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
request.premiumAccessAddon = this.selectedPlan.hasPremiumAccessOption &&
this.premiumAccessAddon;
if (this.interval === 'month') {
request.planType = this.plans[this.plan].monthPlanType;
} else {
request.planType = this.plans[this.plan].annualPlanType;
}
request.planType = this.selectedPlan.type;
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
request.billingAddressCountry = this.taxComponent.taxInfo.country;
if (this.taxComponent.taxInfo.includeTaxId) {
@@ -173,13 +275,9 @@ export class OrganizationPlansComponent {
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
request.premiumAccessAddon = this.selectedPlan.hasPremiumAccessOption &&
this.premiumAccessAddon;
if (this.interval === 'month') {
request.planType = this.plans[this.plan].monthPlanType;
} else {
request.planType = this.plans[this.plan].annualPlanType;
}
request.planType = this.selectedPlan.type;
const result = await this.apiService.postOrganizationUpgrade(this.organizationId, request);
if (!result.success && result.paymentIntentClientSecret != null) {
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
@@ -208,88 +306,4 @@ export class OrganizationPlansComponent {
} catch { }
}
cancel() {
this.onCanceled.emit();
}
changedPlan() {
if (!this.plans[this.plan].canBuyPremiumAccessAddon) {
this.premiumAccessAddon = false;
}
if (this.plans[this.plan].monthPlanType == null) {
this.interval = 'year';
}
if (this.plans[this.plan].noAdditionalSeats) {
this.additionalSeats = 0;
} else if (!this.additionalSeats && !this.plans[this.plan].baseSeats &&
!this.plans[this.plan].noAdditionalSeats) {
this.additionalSeats = 1;
}
}
changedOwnedBusiness() {
if (!this.ownedBusiness || this.plan === 'teams' || this.plan === 'enterprise') {
return;
}
this.plan = 'teams';
}
additionalStorageTotal(annual: boolean): number {
if (annual) {
return Math.abs(this.additionalStorage || 0) * this.storageGb.yearlyPrice;
} else {
return Math.abs(this.additionalStorage || 0) * this.storageGb.monthlyPrice;
}
}
seatTotal(annual: boolean): number {
if (this.plans[this.plan].noAdditionalSeats) {
return 0;
}
if (annual) {
return this.plans[this.plan].annualSeatPrice * Math.abs(this.additionalSeats || 0);
} else {
return this.plans[this.plan].monthlySeatPrice * Math.abs(this.additionalSeats || 0);
}
}
baseTotal(annual: boolean): number {
if (annual) {
return Math.abs(this.plans[this.plan].annualBasePrice || 0);
} else {
return Math.abs(this.plans[this.plan].monthlyBasePrice || 0);
}
}
premiumAccessTotal(annual: boolean): number {
if (this.plans[this.plan].canBuyPremiumAccessAddon && this.premiumAccessAddon) {
if (annual) {
return 40;
}
}
return 0;
}
changedCountry() {
this.paymentComponent.hideBank = this.taxComponent.taxInfo.country !== 'US';
// Bank Account payments are only available for US customers
if (this.paymentComponent.hideBank &&
this.paymentComponent.method === PaymentMethodType.BankAccount) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
}
get total(): number {
const annual = this.interval === 'year';
return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual) +
this.premiumAccessTotal(annual);
}
get createOrganization() {
return this.organizationId == null;
}
}

View File

@@ -74,6 +74,17 @@
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<ng-container *ngIf="o.useSso && o.identifier">
<a *ngIf="o.ssoBound; else linkSso" class="dropdown-item" href="#" appStopClick
(click)="unlinkSso(o)">
<i class="fa fa-fw fa-chain-broken" aria-hidden="true"></i>
{{'unlinkSso' | i18n}}
</a>
<ng-template #linkSso>
<app-link-sso [organization]="o">
</app-link-sso>
</ng-template>
</ng-container>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="leave(o)">
<i class="fa fa-fw fa-sign-out" aria-hidden="true"></i>
{{'leave' | i18n}}

View File

@@ -35,6 +35,7 @@ export class OrganizationsComponent implements OnInit {
async ngOnInit() {
if (!this.vault) {
await this.syncService.fullSync(true);
await this.load();
}
}
@@ -46,6 +47,25 @@ export class OrganizationsComponent implements OnInit {
this.loaded = true;
}
async unlinkSso(org: Organization) {
const confirmed = await this.platformUtilsService.showDialog(
'Are you sure you want to unlink SSO for this organization?', org.name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.apiService.deleteSsoUser(org.id).then(() => {
return this.syncService.fullSync(true);
});
await this.actionPromise;
this.analytics.eventTrack.next({ action: 'Unlinked SSO' });
this.toasterService.popAsync('success', null, 'Unlinked SSO');
await this.load();
} catch { }
}
async leave(org: Organization) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('leaveOrganizationConfirmation'), org.name,

View File

@@ -39,13 +39,15 @@
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
</div>
<div class="form-group col-4">
<label for="stripe-card-cvc-element" class="d-flex">
{{'securityCode' | i18n}}
<div class="d-flex">
<label for="stripe-card-cvc-element">
{{'securityCode' | i18n}}
</label>
<a href="https://www.cvvnumber.com/cvv.html" tabindex="-1" target="_blank" rel="noopener noreferrer"
class="ml-auto" appA11yTitle="{{'learnMore' | i18n}}">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</label>
</div>
<div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
</div>
</div>

View File

@@ -225,7 +225,7 @@
<option value="SE">Sweden</option>
<option value="CH">Switzerland</option>
<option value="SY">Syrian Arab Republic</option>
<option value="TW">Taiwan, Province of China</option>
<option value="TW">Taiwan</option>
<option value="TJ">Tajikistan</option>
<option value="TZ">Tanzania, United Republic of</option>
<option value="TH">Thailand</option>

View File

@@ -1,4 +1,5 @@
import {
Directive,
EventEmitter,
Output,
} from '@angular/core';
@@ -13,6 +14,7 @@ import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
import { TwoFactorProviderRequest } from 'jslib/models/request/twoFactorProviderRequest';
@Directive()
export abstract class TwoFactorBaseComponent {
@Output() onUpdated = new EventEmitter<boolean>();

View File

@@ -31,12 +31,12 @@ import { TwoFactorYubiKeyComponent } from './two-factor-yubikey.component';
templateUrl: 'two-factor-setup.component.html',
})
export class TwoFactorSetupComponent implements OnInit {
@ViewChild('recoveryTemplate', { read: ViewContainerRef }) recoveryModalRef: ViewContainerRef;
@ViewChild('authenticatorTemplate', { read: ViewContainerRef }) authenticatorModalRef: ViewContainerRef;
@ViewChild('yubikeyTemplate', { read: ViewContainerRef }) yubikeyModalRef: ViewContainerRef;
@ViewChild('u2fTemplate', { read: ViewContainerRef }) u2fModalRef: ViewContainerRef;
@ViewChild('duoTemplate', { read: ViewContainerRef }) duoModalRef: ViewContainerRef;
@ViewChild('emailTemplate', { read: ViewContainerRef }) emailModalRef: ViewContainerRef;
@ViewChild('recoveryTemplate', { read: ViewContainerRef, static: true }) recoveryModalRef: ViewContainerRef;
@ViewChild('authenticatorTemplate', { read: ViewContainerRef, static: true }) authenticatorModalRef: ViewContainerRef;
@ViewChild('yubikeyTemplate', { read: ViewContainerRef, static: true }) yubikeyModalRef: ViewContainerRef;
@ViewChild('u2fTemplate', { read: ViewContainerRef, static: true }) u2fModalRef: ViewContainerRef;
@ViewChild('duoTemplate', { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
@ViewChild('emailTemplate', { read: ViewContainerRef, static: true }) emailModalRef: ViewContainerRef;
organizationId: string;
providers: any[] = [];

View File

@@ -1,5 +1,6 @@
import {
ComponentFactoryResolver,
Directive,
ViewChild,
ViewContainerRef,
} from '@angular/core';
@@ -15,8 +16,9 @@ import { AddEditComponent } from '../vault/add-edit.component';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
@Directive()
export class CipherReportComponent {
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
loading = false;
hasLoaded = false;

View File

@@ -41,9 +41,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
async setCiphers() {
const allCiphers = await this.getAllCiphers();
const exposedPasswordCiphers: CipherView[] = [];
const promises: Array<Promise<void>> = [];
const promises: Promise<void>[] = [];
allCiphers.forEach((c) => {
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '' || c.isDeleted) {
return;
}
const promise = this.auditService.passwordLeaked(c.login.password).then((exposedCount) => {

View File

@@ -233,6 +233,9 @@
Once syncing of your data is complete, the download icon in the top right corner will turn pink. Click
the download icon and save the CSV file.
</ng-container>
<ng-container *ngIf="format === 'yoticsv'">
From the Yoti browser extension, click on "Settings", then "Export Saved Logins" and save the CSV file.
</ng-container>
</app-callout>
<div class="row">
<div class="col-6">

View File

@@ -42,10 +42,11 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
if (this.services.size > 0) {
const allCiphers = await this.getAllCiphers();
const inactive2faCiphers: CipherView[] = [];
const promises: Array<Promise<void>> = [];
const promises: Promise<void>[] = [];
const docs = new Map<string, string>();
allCiphers.forEach((c) => {
if (c.type !== CipherType.Login || (c.login.totp != null && c.login.totp !== '') || !c.login.hasUris) {
if (c.type !== CipherType.Login || (c.login.totp != null && c.login.totp !== '') || !c.login.hasUris ||
c.isDeleted) {
return;
}
for (let i = 0; i < c.login.uris.length; i++) {

View File

@@ -17,7 +17,7 @@
<div class="ml-auto">
<button class="btn btn-link" appA11yTitle="{{'copyPassword' | i18n}}"
(click)="copy(h.password)">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</li>

View File

@@ -21,7 +21,7 @@ import { PasswordGeneratorHistoryComponent } from './password-generator-history.
templateUrl: 'password-generator.component.html',
})
export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent {
@ViewChild('historyTemplate', { read: ViewContainerRef }) historyModalRef: ViewContainerRef;
@ViewChild('historyTemplate', { read: ViewContainerRef, static: true }) historyModalRef: ViewContainerRef;
private modal: ModalComponent = null;

View File

@@ -37,7 +37,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
const ciphersWithPasswords: CipherView[] = [];
this.passwordUseMap = new Map<string, number>();
allCiphers.forEach((c) => {
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '' || c.isDeleted) {
return;
}
ciphersWithPasswords.push(c);

View File

@@ -33,7 +33,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl
async setCiphers() {
const allCiphers = await this.getAllCiphers();
const unsecuredCiphers = allCiphers.filter((c) => {
if (c.type !== CipherType.Login || !c.login.hasUris) {
if (c.type !== CipherType.Login || !c.login.hasUris || c.isDeleted) {
return false;
}
return c.login.uris.some((u) => u.uri != null && u.uri.indexOf('http://') === 0);

View File

@@ -40,7 +40,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
const allCiphers = await this.getAllCiphers();
const weakPasswordCiphers: CipherView[] = [];
allCiphers.forEach((c) => {
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '' || c.isDeleted) {
return;
}
const hasUsername = c.login.username != null && c.login.username.trim() !== '';

View File

@@ -12,8 +12,8 @@
<div class="row" *ngIf="!editMode">
<div class="col-6 form-group">
<label for="type">{{'whatTypeOfItem' | i18n}}</label>
<select id="type" name="Type" [(ngModel)]="cipher.type" class="form-control"
[disabled]="cipher.isDeleted">
<select id="type" name="Type" [(ngModel)]="cipher.type" class="form-control"
[disabled]="cipher.isDeleted" appAutofocus>
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
@@ -44,7 +44,7 @@
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyUsername' | i18n}}"
(click)="copy(cipher.login.username, 'username', 'Username')" tabindex="-1">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -84,7 +84,7 @@
appA11yTitle="{{'copyPassword' | i18n}}"
(click)="copy(cipher.login.password, 'password', 'Password')" tabindex="-1"
[disabled]="!cipher.viewPassword">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -126,7 +126,7 @@
<button type="button" class="btn btn-link"
appA11yTitle="{{'copyVerificationCode' | i18n}}"
(click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')">
<i class="fa fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -148,7 +148,7 @@
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyUri' | i18n}}" (click)="copy(u.uri, 'uri', 'URI')"
tabindex="-1">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -209,7 +209,7 @@
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyNumber' | i18n}}"
(click)="copy(cipher.card.number, 'number', 'Number')" tabindex="-1">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -246,7 +246,7 @@
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'securityCode' | i18n}}"
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')" tabindex="-1">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -395,7 +395,7 @@
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyValue' | i18n}}"
(click)="copy(f.value, 'value', 'Field')" tabindex="-1">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -416,7 +416,7 @@
appA11yTitle="{{'copyValue' | i18n}}"
(click)="copy(f.value, 'value', f.type === fieldType.Hidden ? 'H_Field' : 'Field')"
tabindex="-1" [disabled]="!cipher.viewPassword && !f.newField">
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>

View File

@@ -0,0 +1,38 @@
<div class="dropdown mr-2" 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)="bulkMove()" *ngIf="!deleted && !organization">
<i class="fa fa-fw fa-share" aria-hidden="true"></i>
{{'moveSelected' | i18n}}
</button>
<button class="dropdown-item" appStopClick (click)="bulkShare()" *ngIf="!deleted && !organization">
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
{{'shareSelected' | i18n}}
</button>
<button class="dropdown-item" (click)="bulkRestore()" *ngIf="deleted && !organization">
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
{{'restoreSelected' | i18n}}
</button>
<button class="dropdown-item text-danger" (click)="bulkDelete()">
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
{{(deleted ? 'permanentlyDeleteSelected' : 'deleteSelected') | 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>
<ng-template #bulkDeleteTemplate></ng-template>
<ng-template #bulkRestoreTemplate></ng-template>
<ng-template #bulkMoveTemplate></ng-template>
<ng-template #bulkShareTemplate></ng-template>

View File

@@ -0,0 +1,154 @@
import {
Component,
ComponentFactoryResolver,
Input,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { Organization } from 'jslib/models/domain/organization';
import { ModalComponent } from '../modal.component';
import { BulkDeleteComponent } from './bulk-delete.component';
import { BulkMoveComponent } from './bulk-move.component';
import { BulkRestoreComponent } from './bulk-restore.component';
import { BulkShareComponent } from './bulk-share.component';
import { CiphersComponent } from './ciphers.component';
@Component({
selector: 'app-vault-bulk-actions',
templateUrl: 'bulk-actions.component.html',
})
export class BulkActionsComponent {
@Input() ciphersComponent: CiphersComponent;
@Input() modal: ModalComponent;
@Input() deleted: boolean;
@Input() organization: Organization;
@ViewChild('bulkDeleteTemplate', { read: ViewContainerRef, static: true }) bulkDeleteModalRef: ViewContainerRef;
@ViewChild('bulkRestoreTemplate', { read: ViewContainerRef, static: true }) bulkRestoreModalRef: ViewContainerRef;
@ViewChild('bulkMoveTemplate', { read: ViewContainerRef, static: true }) bulkMoveModalRef: ViewContainerRef;
@ViewChild('bulkShareTemplate', { read: ViewContainerRef, static: true }) bulkShareModalRef: ViewContainerRef;
constructor(private toasterService: ToasterService,
private i18nService: I18nService,
private componentFactoryResolver: ComponentFactoryResolver) { }
bulkDelete() {
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkDeleteModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkDeleteComponent>(BulkDeleteComponent, this.bulkDeleteModalRef);
childComponent.permanent = this.deleted;
childComponent.cipherIds = selectedIds;
childComponent.organization = this.organization;
childComponent.onDeleted.subscribe(async () => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
bulkRestore() {
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkRestoreModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkRestoreComponent>(BulkRestoreComponent, this.bulkRestoreModalRef);
childComponent.cipherIds = selectedIds;
childComponent.onRestored.subscribe(async () => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
bulkShare() {
const selectedCiphers = this.ciphersComponent.getSelected();
if (selectedCiphers.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkShareModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkShareComponent>(BulkShareComponent, this.bulkShareModalRef);
childComponent.ciphers = selectedCiphers;
childComponent.onShared.subscribe(async () => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(async () => {
this.modal = null;
});
}
bulkMove() {
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkMoveModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkMoveComponent>(BulkMoveComponent, this.bulkMoveModalRef);
childComponent.cipherIds = selectedIds;
childComponent.onMoved.subscribe(async () => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
selectAll(select: boolean) {
this.ciphersComponent.selectAll(select);
}
}

View File

@@ -4,13 +4,16 @@ import {
Input,
Output,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { Organization } from 'jslib/models/domain/organization';
import { CipherBulkDeleteRequest } from 'jslib/models/request/cipherBulkDeleteRequest';
@Component({
selector: 'app-vault-bulk-delete',
templateUrl: 'bulk-delete.component.html',
@@ -18,20 +21,44 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
export class BulkDeleteComponent {
@Input() cipherIds: string[] = [];
@Input() permanent: boolean = false;
@Input() organization: Organization;
@Output() onDeleted = new EventEmitter();
formPromise: Promise<any>;
constructor(private analytics: Angulartics2, private cipherService: CipherService,
private toasterService: ToasterService, private i18nService: I18nService) { }
private toasterService: ToasterService, private i18nService: I18nService,
private apiService: ApiService) { }
async submit() {
this.formPromise = this.permanent ? this.cipherService.deleteManyWithServer(this.cipherIds) :
this.cipherService.softDeleteManyWithServer(this.cipherIds);
if (!this.organization || !this.organization.isAdmin) {
await this.deleteCiphers();
} else {
await this.deleteCiphersAdmin();
}
await this.formPromise;
this.onDeleted.emit();
this.analytics.eventTrack.next({ action: 'Bulk Deleted Items' });
this.toasterService.popAsync('success', null, this.i18nService.t(this.permanent ? 'permanentlyDeletedItems'
: 'deletedItems'));
}
private async deleteCiphers() {
if (this.permanent) {
this.formPromise = await this.cipherService.deleteManyWithServer(this.cipherIds);
} else {
this.formPromise = await this.cipherService.softDeleteManyWithServer(this.cipherIds);
}
}
private async deleteCiphersAdmin() {
const deleteRequest = new CipherBulkDeleteRequest(this.cipherIds, this.organization.id);
if (this.permanent) {
this.formPromise = await this.apiService.deleteManyCiphersAdmin(deleteRequest);
} else {
this.formPromise = await this.apiService.putDeleteManyCiphersAdmin(deleteRequest);
}
}
}

View File

@@ -3,7 +3,7 @@
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<tbody>
<tr *ngFor="let c of filteredCiphers">
<td (click)="checkCipher(c)" class="table-list-checkbox" *ngIf="!organization">
<td (click)="checkCipher(c)" class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="c.checked" appStopProp>
</td>
<td (click)="checkCipher(c)" class="table-list-icon">
@@ -37,9 +37,14 @@
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<ng-container *ngIf="c.type === cipherType.Login && !c.isDeleted">
<a class="dropdown-item" href="#" appStopClick
(click)="copy(c, c.login.username, 'username', 'username')">
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
{{'copyUsername' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick
(click)="copy(c, c.login.password, 'password', 'password')" *ngIf="c.viewPassword">
<i class="fa fa-fw fa-clipboard" aria-hidden="true"></i>
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
{{'copyPassword' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick *ngIf="c.login.canLaunch"
@@ -55,12 +60,11 @@
<a class="dropdown-item" href="#" appStopClick
*ngIf="((!organization && !c.organizationId) || organization) && !c.isDeleted"
(click)="clone(c)">
<i class="fa fa-fw fa-clone" aria-hidden="true"></i>
<i class="fa fa-fw fa-files-o" aria-hidden="true"></i>
{{'clone' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick
*ngIf="!organization && !c.organizationId && !c.isDeleted"
(click)="share(c)">
<a class="dropdown-item" href="#" appStopClick
*ngIf="!organization && !c.organizationId && !c.isDeleted" (click)="share(c)">
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
{{'share' | i18n}}
</a>

View File

@@ -50,36 +50,11 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
this.selectAll(false);
}
checkCipher(c: CipherView, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select;
}
launch(uri: string) {
this.platformUtilsService.eventTrack('Launched Login URI');
this.platformUtilsService.launchUri(uri);
}
selectAll(select: boolean) {
if (select) {
this.selectAll(false);
}
const selectCount = select && this.ciphers.length > MaxCheckedCount ? MaxCheckedCount : this.ciphers.length;
for (let i = 0; i < selectCount; i++) {
this.checkCipher(this.ciphers[i], select);
}
}
getSelected(): CipherView[] {
if (this.ciphers == null) {
return [];
}
return this.ciphers.filter((c) => !!(c as any).checked);
}
getSelectedIds(): string[] {
return this.getSelected().map((c) => c.id);
}
attachments(c: CipherView) {
this.onAttachmentsClicked.emit(c);
}
@@ -159,6 +134,33 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
}
}
selectAll(select: boolean) {
if (select) {
this.selectAll(false);
}
const selectCount = select && this.ciphers.length > MaxCheckedCount
? MaxCheckedCount
: this.ciphers.length;
for (let i = 0; i < selectCount; i++) {
this.checkCipher(this.ciphers[i], select);
}
}
checkCipher(c: CipherView, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select;
}
getSelected(): CipherView[] {
if (this.ciphers == null) {
return [];
}
return this.ciphers.filter((c) => !!(c as any).checked);
}
getSelectedIds(): string[] {
return this.getSelected().map((c) => c.id);
}
protected deleteCipher(id: string, permanent: boolean) {
return permanent ? this.cipherService.deleteWithServer(id) : this.cipherService.softDeleteWithServer(id);
}

View File

@@ -9,7 +9,8 @@
</div>
<div class="modal-body">
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="folder.name" required>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="folder.name" required
appAutofocus>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">

View File

@@ -21,40 +21,8 @@
</small>
</h1>
<div class="ml-auto d-flex">
<div class="dropdown mr-2" 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">
<a class="dropdown-item" href="#" appStopClick (click)="bulkMove()" *ngIf="!deleted">
<i class="fa fa-fw fa-share" aria-hidden="true"></i>
{{'moveSelected' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="bulkShare()" *ngIf="!deleted">
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
{{'shareSelected' | i18n}}
</a>
<a class="dropdown-item" href="#" (click)="bulkRestore()" *ngIf="deleted">
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
{{'restoreSelected' | i18n}}
</a>
<a class="dropdown-item text-danger" href="#" (click)="bulkDelete()">
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
{{(deleted ? 'permanentlyDeleteSelected' : 'deleteSelected') | i18n}}
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" appStopClick (click)="selectAll(true)">
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
{{'selectAll' | i18n}}
</a>
<a class="dropdown-item" href="#" appStopClick (click)="selectAll(false)">
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
{{'unselectAll' | i18n}}
</a>
</div>
</div>
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [modal]="modal" [deleted]="deleted">
</app-vault-bulk-actions>
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addCipher()" *ngIf="!deleted">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
</button>
@@ -122,8 +90,4 @@
<ng-template #cipherAddEdit></ng-template>
<ng-template #share></ng-template>
<ng-template #collections></ng-template>
<ng-template #bulkDeleteTemplate></ng-template>
<ng-template #bulkRestoreTemplate></ng-template>
<ng-template #bulkMoveTemplate></ng-template>
<ng-template #bulkShareTemplate></ng-template>
<ng-template #updateKeyTemplate></ng-template>

View File

@@ -13,8 +13,6 @@ import {
Router,
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { CipherType } from 'jslib/enums/cipherType';
import { CipherView } from 'jslib/models/view/cipherView';
@@ -25,10 +23,6 @@ import { OrganizationsComponent } from '../settings/organizations.component';
import { UpdateKeyComponent } from '../settings/update-key.component';
import { AddEditComponent } from './add-edit.component';
import { AttachmentsComponent } from './attachments.component';
import { BulkDeleteComponent } from './bulk-delete.component';
import { BulkMoveComponent } from './bulk-move.component';
import { BulkRestoreComponent } from './bulk-restore.component';
import { BulkShareComponent } from './bulk-share.component';
import { CiphersComponent } from './ciphers.component';
import { CollectionsComponent } from './collections.component';
import { FolderAddEditComponent } from './folder-add-edit.component';
@@ -52,19 +46,15 @@ const BroadcasterSubscriptionId = 'VaultComponent';
templateUrl: 'vault.component.html',
})
export class VaultComponent implements OnInit, OnDestroy {
@ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent;
@ViewChild(CiphersComponent) ciphersComponent: CiphersComponent;
@ViewChild(OrganizationsComponent) organizationsComponent: OrganizationsComponent;
@ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef;
@ViewChild('folderAddEdit', { read: ViewContainerRef }) folderAddEditModalRef: ViewContainerRef;
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
@ViewChild('share', { read: ViewContainerRef }) shareModalRef: ViewContainerRef;
@ViewChild('collections', { read: ViewContainerRef }) collectionsModalRef: ViewContainerRef;
@ViewChild('bulkDeleteTemplate', { read: ViewContainerRef }) bulkDeleteModalRef: ViewContainerRef;
@ViewChild('bulkRestoreTemplate', { read: ViewContainerRef }) bulkRestoreModalRef: ViewContainerRef;
@ViewChild('bulkMoveTemplate', { read: ViewContainerRef }) bulkMoveModalRef: ViewContainerRef;
@ViewChild('bulkShareTemplate', { read: ViewContainerRef }) bulkShareModalRef: ViewContainerRef;
@ViewChild('updateKeyTemplate', { read: ViewContainerRef }) updateKeyModalRef: ViewContainerRef;
@ViewChild(GroupingsComponent, { static: true }) groupingsComponent: GroupingsComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@ViewChild(OrganizationsComponent, { static: true }) organizationsComponent: OrganizationsComponent;
@ViewChild('attachments', { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef;
@ViewChild('folderAddEdit', { read: ViewContainerRef, static: true }) folderAddEditModalRef: ViewContainerRef;
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
@ViewChild('share', { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef;
@ViewChild('collections', { read: ViewContainerRef, static: true }) collectionsModalRef: ViewContainerRef;
@ViewChild('updateKeyTemplate', { read: ViewContainerRef, static: true }) updateKeyModalRef: ViewContainerRef;
favorites: boolean = false;
type: CipherType = null;
@@ -76,15 +66,15 @@ export class VaultComponent implements OnInit, OnDestroy {
showPremiumCallout = false;
deleted: boolean = false;
private modal: ModalComponent = null;
modal: ModalComponent = null;
constructor(private syncService: SyncService, private route: ActivatedRoute,
private router: Router, private changeDetectorRef: ChangeDetectorRef,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private tokenService: TokenService, private cryptoService: CryptoService,
private messagingService: MessagingService, private userService: UserService,
private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService,
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
private platformUtilsService: PlatformUtilsService, private broadcasterService: BroadcasterService,
private ngZone: NgZone) { }
async ngOnInit() {
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
@@ -391,119 +381,6 @@ export class VaultComponent implements OnInit, OnDestroy {
component.cloneMode = true;
}
bulkDelete() {
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkDeleteModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkDeleteComponent>(BulkDeleteComponent, this.bulkDeleteModalRef);
childComponent.permanent = this.deleted;
childComponent.cipherIds = selectedIds;
childComponent.onDeleted.subscribe(async () => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
bulkRestore() {
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkRestoreModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkRestoreComponent>(BulkRestoreComponent, this.bulkRestoreModalRef);
childComponent.cipherIds = selectedIds;
childComponent.onRestored.subscribe(async () => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
bulkShare() {
const selectedCiphers = this.ciphersComponent.getSelected();
if (selectedCiphers.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkShareModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkShareComponent>(BulkShareComponent, this.bulkShareModalRef);
childComponent.ciphers = selectedCiphers;
childComponent.onShared.subscribe(async () => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(async () => {
this.modal = null;
});
}
bulkMove() {
const selectedIds = this.ciphersComponent.getSelectedIds();
if (selectedIds.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nothingSelected'));
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkMoveModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkMoveComponent>(BulkMoveComponent, this.bulkMoveModalRef);
childComponent.cipherIds = selectedIds;
childComponent.onMoved.subscribe(async () => {
this.modal.close();
await this.ciphersComponent.refresh();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
selectAll(select: boolean) {
this.ciphersComponent.selectAll(select);
}
updateKey() {
if (this.modal != null) {
this.modal.close();

31
src/connectors/sso.html Normal file
View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1010">
<meta name="theme-color" content="#175DDC">
<title>Bitwarden</title>
<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">
</head>
<body class="layout_frontend">
<div class="mt-5 d-flex justify-content-center">
<div>
<img src="../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
<div id="content">
<p class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="Loading" aria-hidden="true"></i>
</p>
</div>
</div>
</div>
</body>
</html>

1
src/connectors/sso.scss Normal file
View File

@@ -0,0 +1 @@
@import "../scss/styles.scss";

55
src/connectors/sso.ts Normal file
View File

@@ -0,0 +1,55 @@
// tslint:disable-next-line
require('./sso.scss');
document.addEventListener('DOMContentLoaded', (event) => {
const code = getQsParam('code');
const state = getQsParam('state');
if (state != null && state.includes(':clientId=browser')) {
initiateBrowserSso(code, state);
} else {
window.location.href = window.location.origin + '/#/sso?code=' + code + '&state=' + state;
// Match any characters between "_returnUri='" and the next "'"
const returnUri = extractFromRegex(state, '(?<=_returnUri=\')(.*)(?=\')');
if (returnUri) {
window.location.href = window.location.origin + `/#${returnUri}`;
} else {
window.location.href = window.location.origin + '/#/sso?code=' + code + '&state=' + state;
}
}
});
function getQsParam(name: string) {
const url = window.location.href;
name = name.replace(/[\[\]]/g, '\\$&');
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
const results = regex.exec(url);
if (!results) {
return null;
}
if (!results[2]) {
return '';
}
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
function initiateBrowserSso(code: string, state: string) {
window.postMessage({ command: 'authResult', code: code, state: state }, '*');
let handOffMessage = ('; ' + document.cookie).split('; ssoHandOffMessage=').pop().split(';').shift();
document.cookie = 'ssoHandOffMessage=;SameSite=strict;max-age=0'
document.getElementById('content').innerHTML =
`<p>${handOffMessage}</p>`;
}
function extractFromRegex(s: string, regexString: string) {
const regex = new RegExp(regexString);
const results = regex.exec(s);
if (!results) {
return null;
}
return results[0];
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,9 @@
"password": {
"message": "Пароль"
},
"newPassword": {
"message": "Новы пароль"
},
"passphrase": {
"message": "Парольная фраза"
},
@@ -527,22 +530,22 @@
"message": "What should we call you?"
},
"masterPass": {
"message": "Майстар-пароль"
"message": "Асноўны пароль"
},
"masterPassDesc": {
"message": "Майстар-пароль — ключ да вашага бяспечнага сховішча. Ён вельмі важны, таму не забывайце яго. Аднавіць майстар-пароль немагчыма."
"message": "Асноўны пароль — ключ да вашага бяспечнага сховішча. Ён вельмі важны, таму не забывайце яго. Аднавіць асноўны пароль немагчыма."
},
"masterPassHintDesc": {
"message": "Падказка да майстра-пароля можа дапамагчы вам яго ўспомніць."
"message": "Падказка да асноўнага пароля можа дапамагчы вам яго ўспомніць."
},
"reTypeMasterPass": {
"message": "Увядзіце майстар-пароль паўторна"
"message": "Увядзіце асноўны пароль паўторна"
},
"masterPassHint": {
"message": "Падказка да майстра-пароля (неабавязкова)"
"message": "Падказка да асноўнага пароля (неабавязкова)"
},
"masterPassHintLabel": {
"message": "Падказка да майстра-пароля"
"message": "Падказка да асноўнага пароля"
},
"settings": {
"message": "Налады"
@@ -551,10 +554,10 @@
"message": "Падказка да пароля"
},
"enterEmailToGetHint": {
"message": "Увядзіце адрас электроннай пошты ўліковага запісу для атрымання падказкі для пароля."
"message": "Увядзіце адрас электроннай пошты ўліковага запісу для атрымання падказкі для асноўнага пароля."
},
"getMasterPasswordHint": {
"message": "Атрымаць падказку для майстра-пароля"
"message": "Атрымаць падказку для асноўнага пароля"
},
"emailRequired": {
"message": "Патрабуецца адрас электроннай пошты."
@@ -563,19 +566,19 @@
"message": "Памылковы адрас электроннай пошты."
},
"masterPassRequired": {
"message": "Патрабуецца майстар-пароль."
"message": "Патрабуецца асноўны пароль."
},
"masterPassLength": {
"message": "Майстар-пароль павінен быць даўжынёй не менш за 8 сімвалаў."
"message": "Асноўны пароль павінен быць даўжынёй не менш за 8 сімвалаў."
},
"masterPassDoesntMatch": {
"message": "Майстры-паролі не супадаюць."
"message": "Асноўныя паролі не супадаюць."
},
"newAccountCreated": {
"message": "Ваш уліковы запіс створаны! Вы можаце ўвайсці."
},
"masterPassSent": {
"message": "Мы адправілі вам на электронную пошту падказку для майстра-пароля."
"message": "Мы адправілі вам на электронную пошту падказку для асноўнага пароля."
},
"unexpectedError": {
"message": "Адбылася нечаканая памылка."
@@ -584,7 +587,7 @@
"message": "Адрас эл. пошты"
},
"yourVaultIsLocked": {
"message": "Ваша сховішча заблакіравана. Каб працягнуць, увядзіце майстар-пароль."
"message": "Ваша сховішча заблакіравана. Каб працягнуць, увядзіце асноўны пароль."
},
"unlock": {
"message": "Разблакіраваць"
@@ -603,7 +606,7 @@
}
},
"invalidMasterPassword": {
"message": "Памылковы майстар-пароль"
"message": "Памылковы асноўны пароль"
},
"lockNow": {
"message": "Заблакіраваць"
@@ -791,7 +794,7 @@
"message": "Экспартуемы файл утрымлівае даныя вашага сховішча ў незашыфраваным фармаце. Яго не варта захоўваць ці адпраўляць па небяспечным каналам (напрыклад, па электроннай пошце). Выдаліце яго адразу пасля выкарыстання."
},
"exportMasterPassword": {
"message": "Увядзіце ваш майстар-пароль для экспарту даных са сховішча."
"message": "Увядзіце ваш асноўны пароль для экспарту даных са сховішча."
},
"exportVault": {
"message": "Экспарт сховішча"
@@ -881,19 +884,19 @@
"message": "Please log back in. If you are using other Bitwarden applications log out and back in to those as well."
},
"changeMasterPassword": {
"message": "Змяніць майстар-пароль"
"message": "Змяніць асноўны пароль"
},
"masterPasswordChanged": {
"message": "Майстар-пароль зменены"
"message": "Асноўны пароль зменены"
},
"currentMasterPass": {
"message": "Current Master Password"
"message": "Бягучы асноўны пароль"
},
"newMasterPass": {
"message": "New Master Password"
"message": "Новы асноўны пароль"
},
"confirmNewMasterPass": {
"message": "Confirm New Master Password"
"message": "Пацвердзіць новы асноўны пароль"
},
"encKeySettings": {
"message": "Encryption Key Settings"
@@ -980,13 +983,13 @@
"message": "Your account has been closed and all associated data has been deleted."
},
"myAccount": {
"message": "My Account"
"message": "Мой уліковы запіс"
},
"tools": {
"message": "Tools"
"message": "Інструменты"
},
"importData": {
"message": "Import Data"
"message": "Імпарт даных"
},
"importSuccess": {
"message": "Data has been successfully imported into your vault."
@@ -1971,7 +1974,7 @@
"message": "Add and share with unlimited users"
},
"createUnlimitedCollections": {
"message": "Create unlimited collections"
"message": "Create unlimited Collections"
},
"gbEncryptedFileStorage": {
"message": "$SIZE$ encrypted file storage",
@@ -1986,13 +1989,13 @@
"message": "On-premise hosting (optional)"
},
"usersGetPremium": {
"message": "Users get access to premium membership features"
"message": "Users get access to Premium Features"
},
"controlAccessWithGroups": {
"message": "Control user access with groups"
"message": "Control user access with Groups"
},
"syncUsersFromDirectory": {
"message": "Sync your users and groups from a directory"
"message": "Sync your users and Groups from a directory"
},
"trackAuditLogs": {
"message": "Track user actions with audit logs"
@@ -2810,9 +2813,11 @@
"nothingSelected": {
"message": "You have not selected anything."
},
"submitAgreePolicies": {
"message": "By clicking the \"Submit\" button, you agree to the following policies:",
"description": "A policy is something like Terms of Service, Privacy Policy, etc."
"acceptPolicies": {
"message": "By checking this box you agree to the following:"
},
"acceptPoliciesError": {
"message": "Terms of Service and Privacy Policy have not been acknowledged."
},
"termsOfService": {
"message": "Terms of Service"
@@ -2893,10 +2898,10 @@
"description": "ex. A very weak password. Scale: Very Weak -> Weak -> Good -> Strong"
},
"weakMasterPassword": {
"message": "Weak Master Password"
"message": "Слабы асноўны пароль"
},
"weakMasterPasswordDesc": {
"message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?"
"message": "Асноўны пароль, выбраны вамі, з'яўляецца слабым. Для належнай абароны ўліковага запісу Bitwarden, вы павінны выкарыстоўваць моцны асноўны пароль (або парольную фразу). Вы ўпэўнены, што хочаце выкарыстоўваць гэты асноўны пароль?"
},
"rotateAccountEncKey": {
"message": "Also rotate my account's encryption key"
@@ -2948,6 +2953,12 @@
"apiKeyWarning": {
"message": "Your API key has full access to the organization. It should be kept secret."
},
"userApiKeyDesc": {
"message": "Your API key can be used to authenticate in the Bitwarden CLI."
},
"userApiKeyWarning": {
"message": "Your API key is an alternative authentication mechanism. It should be kept secret."
},
"oauth2ClientCredentials": {
"message": "OAuth 2.0 Client Credentials",
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
@@ -2980,13 +2991,13 @@
"message": "Clone"
},
"masterPassPolicyDesc": {
"message": "Set minimum requirements for master password strength."
"message": "Задайце мінімальныя патрабаванні да надзейнасці асноўнага пароля."
},
"twoStepLoginPolicyDesc": {
"message": "Require users to set up two-step login on their personal accounts."
},
"twoStepLoginPolicyWarning": {
"message": "Organization members who do not have two-step login enabled for their personal account will be removed from the organization and will receive an email notifying them about the change."
"message": "Organization members who are not Owners or Administrators and do not have two-step login enabled for their personal account will be removed from the organization and will receive an email notifying them about the change."
},
"twoStepLoginPolicyUserWarning": {
"message": "You are a member of an organization that requires two-step login to be enabled on your user account. If you disable all two-step login providers you will be automatically removed from these organizations."
@@ -2998,7 +3009,7 @@
"message": "One or more organization policies are affecting your generator settings."
},
"masterPasswordPolicyInEffect": {
"message": "One or more organization policies require your master password to meet the following requirements:"
"message": "Згодна з адной або некалькімі палітыкамі арганізацыі неабходна, каб ваш асноўны пароль адказваў наступным патрабаванням:"
},
"policyInEffectMinComplexity": {
"message": "Minimum complexity score of $SCORE$",
@@ -3037,7 +3048,7 @@
}
},
"masterPasswordPolicyRequirementsNotMet": {
"message": "Your new master password does not meet the policy requirements."
"message": "Ваш новы асноўны пароль не адпавядае патрабаванням палітыкі арганізацыі."
},
"minimumNumberOfWords": {
"message": "Minimum Number of Words"
@@ -3163,5 +3174,180 @@
},
"taxInfoUpdated": {
"message": "Tax information updated."
},
"setMasterPassword": {
"message": "Задаць асноўны пароль"
},
"ssoCompleteRegistration": {
"message": "In order to complete logging in with SSO, please set a master password to access and protect your vault."
},
"identifier": {
"message": "Identifier"
},
"organizationIdentifier": {
"message": "Organization Identifier"
},
"ssoLogInWithOrgIdentifier": {
"message": "Log in using your organization's single sign-on portal. Please enter your organization's identifier to begin."
},
"enterpriseSingleSignOn": {
"message": "Enterprise Single Sign-On"
},
"ssoHandOff": {
"message": "You may now close this tab and continue in the extension."
},
"businessPortal": {
"message": "Business Portal",
"description": "The web portal used by business organizations for configuring certain features."
},
"includeAllTeamsFeatures": {
"message": "All Teams features, plus:"
},
"includeSsoAuthentication": {
"message": "SSO Authentication via SAML2.0 and OpenID Connect"
},
"includeEnterprisePolicies": {
"message": "Enterprise Policies"
},
"ssoValidationFailed": {
"message": "SSO Validation Failed"
},
"ssoIdentifierRequired": {
"message": "Organization Identifier is required."
},
"unlinkSso": {
"message": "Unlink SSO"
},
"linkSso": {
"message": "Link SSO"
},
"webPoliciesDeprecationWarning": {
"message": "Policy configuration has been moved, and this page will soon be deprecated. Please click below to use the Business Portal policies page instead."
},
"singleOrg": {
"message": "Single Organization"
},
"singleOrgDesc": {
"message": "Restrict users from being able to join any other organizations."
},
"singleOrgBlockCreateMessage": {
"message": "Your current organization has a policy that does not allow you to join more than one organization. Please contact your organization admins or sign up from a different Bitwarden account."
},
"singleOrgPolicyWarning": {
"message": "Organization members who are not Owners or Administrators and are already a member of another organization will be removed from your organization."
},
"requireSso": {
"message": "Single Sign-On Authentication"
},
"requireSsoPolicyDesc": {
"message": "Require users to log in with the Enterprise Single Sign-On method."
},
"prerequisite": {
"message": "Prerequisite"
},
"requireSsoPolicyReq": {
"message": "The Single Organization enterprise policy must be enabled before activating this policy."
},
"requireSsoPolicyReqError": {
"message": "Single Organization policy not enabled."
},
"requireSsoExemption": {
"message": "Organization Owners and Administrators are exempt from this policy's enforcement."
},
"sendTypeFile": {
"message": "File"
},
"sendTypeText": {
"message": "Text"
},
"createSend": {
"message": "Create New Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editSend": {
"message": "Edit Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"createdSend": {
"message": "Created Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editedSend": {
"message": "Edited Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletedSend": {
"message": "Deleted Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSend": {
"message": "Delete Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSendConfirmation": {
"message": "Are you sure you want to delete this Send?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"whatTypeOfSend": {
"message": "What type of Send is this?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletionDate": {
"message": "Deletion Date"
},
"expirationDate": {
"message": "Expiration Date"
},
"maxAccessCount": {
"message": "Maximum Access Count"
},
"currentAccessCount": {
"message": "Current Access Count"
},
"disabled": {
"message": "Disabled"
},
"sendLink": {
"message": "Send Link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"copySendLink": {
"message": "Copy Send Link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"removePassword": {
"message": "Remove Password"
},
"removedPassword": {
"message": "Removed Password"
},
"removePasswordConfirmation": {
"message": "Are you sure you want to remove the password?"
},
"allSends": {
"message": "All Sends"
},
"searchSends": {
"message": "Search Sends",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendProtectedPassword": {
"message": "This Send is protected with a password. Please type the password below to continue.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendProtectedPasswordDontKnow": {
"message": "Don't know the password? Ask the Sender for the password needed to access this Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendHiddenByDefault": {
"message": "This send is hidden by default. You can toggle its visibility using the button below.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"downloadFile": {
"message": "Download File"
},
"noSendsInList": {
"message": "There are no Sends to list.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}
}

View File

@@ -37,6 +37,9 @@
"password": {
"message": "Парола"
},
"newPassword": {
"message": "Нова парола"
},
"passphrase": {
"message": "Парола-фраза"
},
@@ -2810,9 +2813,11 @@
"nothingSelected": {
"message": "Не сте избрали нищо."
},
"submitAgreePolicies": {
"message": "С натискането на бутона „Подаване“ се съгласявате със следните политики:",
"description": "A policy is something like Terms of Service, Privacy Policy, etc."
"acceptPolicies": {
"message": "Чрез тази отметка вие се съгласявате със следните:"
},
"acceptPoliciesError": {
"message": "Условията за използване и политиката за поверителност не бяха приети."
},
"termsOfService": {
"message": "Общи условия"
@@ -2948,6 +2953,12 @@
"apiKeyWarning": {
"message": "Ключът за API дава пълен достъп до организацията. Трябва да го пазите в тайна."
},
"userApiKeyDesc": {
"message": "Your API key can be used to authenticate in the Bitwarden CLI."
},
"userApiKeyWarning": {
"message": "Вашият API ключ е алтернативен механизъм автентикация. Той трябва да се пази в тайна."
},
"oauth2ClientCredentials": {
"message": "Идентификация за клиент за OAuth 2.0",
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
@@ -3001,7 +3012,7 @@
"message": "Поне една политика на организация има следните изисквания към главната ви парола:"
},
"policyInEffectMinComplexity": {
"message": "Минимална сила от $SCORE$",
"message": "Минимална сложност от $SCORE$",
"placeholders": {
"score": {
"content": "$1",
@@ -3163,5 +3174,180 @@
},
"taxInfoUpdated": {
"message": "Данъчната информация е обновена."
},
"setMasterPassword": {
"message": "Задаване на главна парола"
},
"ssoCompleteRegistration": {
"message": "За да завършите настройките за еднократна идентификация, трябва да зададете главна парола за трезора."
},
"identifier": {
"message": "Идентификатор"
},
"organizationIdentifier": {
"message": "Идентификатор на организация"
},
"ssoLogInWithOrgIdentifier": {
"message": "Вписване чрез портала на организацията ви за еднократна идентификация. За да продължите, въведете идентификатора на организацията."
},
"enterpriseSingleSignOn": {
"message": "Еднократна идентификация (SSO)"
},
"ssoHandOff": {
"message": "You may now close this tab and continue in the extension."
},
"businessPortal": {
"message": "Бизнес портал",
"description": "The web portal used by business organizations for configuring certain features."
},
"includeAllTeamsFeatures": {
"message": "Всички възможности в екип плюс:"
},
"includeSsoAuthentication": {
"message": "Еднократна идентификация чрез SAML2.0 и OpenID Connect"
},
"includeEnterprisePolicies": {
"message": "Политики на бизнеса"
},
"ssoValidationFailed": {
"message": "Неуспешна еднократна идентификация"
},
"ssoIdentifierRequired": {
"message": "Идентификаторът на организация е задължителен."
},
"unlinkSso": {
"message": "Прекъсване на еднократна идентификация"
},
"linkSso": {
"message": "Свързване на еднократна идентификация"
},
"webPoliciesDeprecationWarning": {
"message": "Policy configuration has been moved, and this page will soon be deprecated. Please click below to use the Business Portal policies page instead."
},
"singleOrg": {
"message": "Една организация"
},
"singleOrgDesc": {
"message": "Restrict users from being able to join any other organizations."
},
"singleOrgBlockCreateMessage": {
"message": "Вашата настояща организация има политика, която не позволява да участвате в повече от една организация. Моля свържете се с администратора на организацията или се впишете с друг Bitwarden потребител."
},
"singleOrgPolicyWarning": {
"message": "Organization members who are not Owners or Administrators and are already a member of another organization will be removed from your organization."
},
"requireSso": {
"message": "Single Sign-On Authentication"
},
"requireSsoPolicyDesc": {
"message": "Require users to log in with the Enterprise Single Sign-On method."
},
"prerequisite": {
"message": редпоставкa"
},
"requireSsoPolicyReq": {
"message": "The Single Organization enterprise policy must be enabled before activating this policy."
},
"requireSsoPolicyReqError": {
"message": "Single Organization policy not enabled."
},
"requireSsoExemption": {
"message": "Organization Owners and Administrators are exempt from this policy's enforcement."
},
"sendTypeFile": {
"message": "Файл"
},
"sendTypeText": {
"message": "Текст"
},
"createSend": {
"message": "Create New Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editSend": {
"message": "Edit Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"createdSend": {
"message": "Created Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editedSend": {
"message": "Edited Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletedSend": {
"message": "Deleted Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSend": {
"message": "Delete Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSendConfirmation": {
"message": "Are you sure you want to delete this Send?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"whatTypeOfSend": {
"message": "What type of Send is this?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletionDate": {
"message": "Deletion Date"
},
"expirationDate": {
"message": "Срок на валидност"
},
"maxAccessCount": {
"message": "Maximum Access Count"
},
"currentAccessCount": {
"message": "Current Access Count"
},
"disabled": {
"message": "Disabled"
},
"sendLink": {
"message": "Send Link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"copySendLink": {
"message": "Copy Send Link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"removePassword": {
"message": "Премахване на парола"
},
"removedPassword": {
"message": "Removed Password"
},
"removePasswordConfirmation": {
"message": "Are you sure you want to remove the password?"
},
"allSends": {
"message": "All Sends"
},
"searchSends": {
"message": "Search Sends",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendProtectedPassword": {
"message": "This Send is protected with a password. Please type the password below to continue.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendProtectedPasswordDontKnow": {
"message": "Don't know the password? Ask the Sender for the password needed to access this Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendHiddenByDefault": {
"message": "This send is hidden by default. You can toggle its visibility using the button below.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"downloadFile": {
"message": "Изтеглете файл"
},
"noSendsInList": {
"message": "There are no Sends to list.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}
}

View File

@@ -37,6 +37,9 @@
"password": {
"message": "Contrasenya"
},
"newPassword": {
"message": "Nova contrasenya"
},
"passphrase": {
"message": "Frase de pas"
},
@@ -2810,9 +2813,11 @@
"nothingSelected": {
"message": "No heu seleccionat res."
},
"submitAgreePolicies": {
"message": "Fent clic al botó \"Envia\", accepteu les polítiques següents:",
"description": "A policy is something like Terms of Service, Privacy Policy, etc."
"acceptPolicies": {
"message": "Si activeu aquesta casella, indiqueu que esteu dacord amb el següent:"
},
"acceptPoliciesError": {
"message": "No shan reconegut les condicions del servei i la declaració de privadesa."
},
"termsOfService": {
"message": "Condicions del servei"
@@ -2948,6 +2953,12 @@
"apiKeyWarning": {
"message": "La clau de l'API té accés total a l'organització. S'ha de mantenir en secret."
},
"userApiKeyDesc": {
"message": "La vostra clau API es pot utilitzar per autenticar-se al CLI de Bitwarden."
},
"userApiKeyWarning": {
"message": "La vostra clau API és un mecanisme d'autenticació alternatiu. Sha de mantenir en secret."
},
"oauth2ClientCredentials": {
"message": "Credencials de client OAuth 2.0",
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
@@ -3163,5 +3174,180 @@
},
"taxInfoUpdated": {
"message": "Informació fiscal actualitzada."
},
"setMasterPassword": {
"message": "Estableix la contrasenya mestra"
},
"ssoCompleteRegistration": {
"message": "Per completar la sessió amb SSO, configureu una contrasenya mestra per accedir i protegir la vostra caixa forta."
},
"identifier": {
"message": "Identificador"
},
"organizationIdentifier": {
"message": "Identificador dorganització"
},
"ssoLogInWithOrgIdentifier": {
"message": "Inicieu la sessió ràpidament mitjançant el portal d'inici de sessió únic de la vostra organització. Introduïu l'identificador de la vostra organització per començar."
},
"enterpriseSingleSignOn": {
"message": "Inici de sessió únic d'empresa"
},
"ssoHandOff": {
"message": "You may now close this tab and continue in the extension."
},
"businessPortal": {
"message": "Portal empresarial",
"description": "The web portal used by business organizations for configuring certain features."
},
"includeAllTeamsFeatures": {
"message": "Característiques de tots els equips, a més de:"
},
"includeSsoAuthentication": {
"message": "Autenticació SSO mitjançant SAML2.0 i OpenID Connect"
},
"includeEnterprisePolicies": {
"message": "Polítiques empresarials"
},
"ssoValidationFailed": {
"message": "La validació SSO ha fallat"
},
"ssoIdentifierRequired": {
"message": "Cal identificador dorganització."
},
"unlinkSso": {
"message": "Desenllaça SSO"
},
"linkSso": {
"message": "Enllaça SSO"
},
"webPoliciesDeprecationWarning": {
"message": "La configuració de la política s'ha desplaçat i aquesta pàgina quedarà obsoleta aviat. Feu clic a continuació per utilitzar la pàgina de polítiques del portal empresarial."
},
"singleOrg": {
"message": "Organització única"
},
"singleOrgDesc": {
"message": "Restringeix els usuaris perquè no puguen unir-se a qualsevol altra organització."
},
"singleOrgBlockCreateMessage": {
"message": "La vostra organització actual té una política que no us permet unir-vos a més d'una organització. Poseu-vos en contacte amb els administradors de la vostra organització o registreu-vos des dun altre compte de Bitwarden."
},
"singleOrgPolicyWarning": {
"message": "Els membres que no siguen propietaris ni administradors i que ja siguen membres d'una altra organització se suprimiran de la vostra organització."
},
"requireSso": {
"message": "Autenticació d'inici de sessió únic"
},
"requireSsoPolicyDesc": {
"message": "Sol·liciteu als usuaris que inicien la sessió amb el mètode dinici de sessió únic de lempresa."
},
"prerequisite": {
"message": "Requisit previ"
},
"requireSsoPolicyReq": {
"message": "La política empresarial d'una organització única s'ha d'activar abans d'activar aquesta política."
},
"requireSsoPolicyReqError": {
"message": "La política d'una única organització no està habilitada."
},
"requireSsoExemption": {
"message": "Els propietaris i administradors dorganitzacions estan exempts de fer complir aquesta política."
},
"sendTypeFile": {
"message": "Fitxer"
},
"sendTypeText": {
"message": "Text"
},
"createSend": {
"message": "Crea un nou Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editSend": {
"message": "Edita Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"createdSend": {
"message": "Send creat",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editedSend": {
"message": "Send editat",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletedSend": {
"message": "Send suprimit",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSend": {
"message": "Suprimeix el Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSendConfirmation": {
"message": "Esteu segur que voleu suprimir aquest Send?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"whatTypeOfSend": {
"message": "Quin tipus de Send és aquest?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletionDate": {
"message": "Data de supressió"
},
"expirationDate": {
"message": "Data de venciment"
},
"maxAccessCount": {
"message": "Recompte màxim d'accés"
},
"currentAccessCount": {
"message": "Recompte daccés actual"
},
"disabled": {
"message": "Deshabilitat"
},
"sendLink": {
"message": "Enllaç Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"copySendLink": {
"message": "Copia l'enllaç Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"removePassword": {
"message": "Suprimeix la contrasenya"
},
"removedPassword": {
"message": "Contrasenya suprimida"
},
"removePasswordConfirmation": {
"message": "Esteu segur que voleu suprimir la contrasenya?"
},
"allSends": {
"message": "Tots els Send"
},
"searchSends": {
"message": "Cerca Sends",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendProtectedPassword": {
"message": "Aquest Send està protegit amb una contrasenya. Escriviu la contrasenya següent per continuar.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendProtectedPasswordDontKnow": {
"message": "No sabeu la contrasenya? Demaneu al remitent la contrasenya necessària per accedir a aquest Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendHiddenByDefault": {
"message": "Aquest Send està ocult per defecte. Podeu canviar la seua visibilitat mitjançant el botó següent.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"downloadFile": {
"message": "Baixa el fitxer"
},
"noSendsInList": {
"message": "No hi ha cap Send a llistar.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}
}

View File

@@ -37,6 +37,9 @@
"password": {
"message": "Heslo"
},
"newPassword": {
"message": "Nové heslo"
},
"passphrase": {
"message": "Heslová fráze"
},
@@ -461,10 +464,10 @@
"message": "Opravdu chcete tuto položku smazat?"
},
"deletedItem": {
"message": "Položka byla smazána."
"message": "Položka byla smazána"
},
"deletedItems": {
"message": "Položky byly smazány."
"message": "Položky byly smazány"
},
"movedItems": {
"message": "Položky byly přesunuty"
@@ -1045,11 +1048,11 @@
"message": "Použije profilový obrázek načtený z gravatar.com."
},
"enableFullWidth": {
"message": "Enable Full Width Layout",
"message": "Zapnout rozvržení na celou šířku stránky",
"description": "Allows scaling the web vault UI's width"
},
"enableFullWidthDesc": {
"message": "Allow the web vault to expand the full width of the browser window."
"message": "Povolit webovému trezoru roztáhnout se na celou šířku okna."
},
"default": {
"message": "Výchozí"
@@ -2654,7 +2657,7 @@
"description": "A billing plan/package. For example: families, teams, enterprise, etc."
},
"changeBillingPlanUpgrade": {
"message": "Upgrade your account to another plan be providing the information below. Please ensure that you have an active payment method added to the account.",
"message": "Povyšte svůj účet na jiný plán zadáním údajů níže. Ujistěte se prosím, že máte k účtu přidaný platný způsob platby.",
"description": "A billing plan/package. For example: families, teams, enterprise, etc."
},
"changeBillingPlanDesc": {
@@ -2810,9 +2813,11 @@
"nothingSelected": {
"message": "Nevybrali jste žádné položky."
},
"submitAgreePolicies": {
"message": "Klepnutím na tlačítko \"Odeslat\" souhlasíte s následující podmínkami:",
"description": "A policy is something like Terms of Service, Privacy Policy, etc."
"acceptPolicies": {
"message": "Zaškrtnutím tohoto políčka souhlasím s následujícím:"
},
"acceptPoliciesError": {
"message": "Terms of Service and Privacy Policy have not been acknowledged."
},
"termsOfService": {
"message": "Podmínky služby"
@@ -2948,6 +2953,12 @@
"apiKeyWarning": {
"message": "Váš API klíč má plný přístup k organizaci. Měl by být uchován v tajnosti."
},
"userApiKeyDesc": {
"message": "Your API key can be used to authenticate in the Bitwarden CLI."
},
"userApiKeyWarning": {
"message": "Your API key is an alternative authentication mechanism. It should be kept secret."
},
"oauth2ClientCredentials": {
"message": "OAuth 2.0 klientské údaje",
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
@@ -3150,18 +3161,193 @@
"message": "Potvrzení akce při vypršení časového limitu"
},
"hidePasswords": {
"message": "Hide Passwords"
"message": "Skrýt hesla"
},
"countryPostalCodeRequiredDesc": {
"message": "We require this information for calculating sales tax and financial reporting only."
"message": "Tyto informace potřebujeme pouze pro výpočet daně a pro finanční přehledy."
},
"includeVAT": {
"message": "Include VAT/GST Information (optional)"
"message": "Zahrnout údaje o DPH (volitelné)"
},
"taxIdNumber": {
"message": "VAT/GST Tax ID"
"message": "DIČ"
},
"taxInfoUpdated": {
"message": "Tax information updated."
"message": "Údaje pro DPH aktualizovány."
},
"setMasterPassword": {
"message": "Nastavit hlavní heslo"
},
"ssoCompleteRegistration": {
"message": "Chcete-li dokončit přihlášení pomocí SSO, nastavte prosím hlavní heslo pro přístup a ochranu vašeho trezoru."
},
"identifier": {
"message": "Identifikátor"
},
"organizationIdentifier": {
"message": "Identifikátor organizace"
},
"ssoLogInWithOrgIdentifier": {
"message": "Přihlaste se pomocí přihlašovacího portálu vaší organizace. Chcete-li začít, zadejte prosím identifikátor vaší organizace."
},
"enterpriseSingleSignOn": {
"message": "Jednotné podnikové přihlášení"
},
"ssoHandOff": {
"message": "You may now close this tab and continue in the extension."
},
"businessPortal": {
"message": "Podnikový portál",
"description": "The web portal used by business organizations for configuring certain features."
},
"includeAllTeamsFeatures": {
"message": "Všechny funkce Týmů, navíc:"
},
"includeSsoAuthentication": {
"message": "Podnikové přihlášení prostřednictvím SAML2.0 a OpenID Connect"
},
"includeEnterprisePolicies": {
"message": "Podnikové politiky"
},
"ssoValidationFailed": {
"message": "Ověření pomocí SSO selhalo"
},
"ssoIdentifierRequired": {
"message": "Je vyžadován identifikátor organizace."
},
"unlinkSso": {
"message": "Odebrat podnikové přihlášení"
},
"linkSso": {
"message": "Propojit s podnikovým přihlášením"
},
"webPoliciesDeprecationWarning": {
"message": "Policy configuration has been moved, and this page will soon be deprecated. Please click below to use the Business Portal policies page instead."
},
"singleOrg": {
"message": "Single Organization"
},
"singleOrgDesc": {
"message": "Restrict users from being able to join any other organizations."
},
"singleOrgBlockCreateMessage": {
"message": "Your current organization has a policy that does not allow you to join more than one organization. Please contact your organization admins or sign up from a different Bitwarden account."
},
"singleOrgPolicyWarning": {
"message": "Organization members who are not Owners or Administrators and are already a member of another organization will be removed from your organization."
},
"requireSso": {
"message": "Single Sign-On Authentication"
},
"requireSsoPolicyDesc": {
"message": "Require users to log in with the Enterprise Single Sign-On method."
},
"prerequisite": {
"message": "Prerequisite"
},
"requireSsoPolicyReq": {
"message": "The Single Organization enterprise policy must be enabled before activating this policy."
},
"requireSsoPolicyReqError": {
"message": "Single Organization policy not enabled."
},
"requireSsoExemption": {
"message": "Organization Owners and Administrators are exempt from this policy's enforcement."
},
"sendTypeFile": {
"message": "Soubor"
},
"sendTypeText": {
"message": "Text"
},
"createSend": {
"message": "Create New Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editSend": {
"message": "Edit Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"createdSend": {
"message": "Created Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editedSend": {
"message": "Edited Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletedSend": {
"message": "Deleted Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSend": {
"message": "Delete Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSendConfirmation": {
"message": "Are you sure you want to delete this Send?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"whatTypeOfSend": {
"message": "What type of Send is this?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletionDate": {
"message": "Datum odstranění"
},
"expirationDate": {
"message": "Datum expirace"
},
"maxAccessCount": {
"message": "Maximum Access Count"
},
"currentAccessCount": {
"message": "Current Access Count"
},
"disabled": {
"message": "Disabled"
},
"sendLink": {
"message": "Odeslat odkaz",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"copySendLink": {
"message": "Copy Send Link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"removePassword": {
"message": "Odstranit heslo"
},
"removedPassword": {
"message": "Removed Password"
},
"removePasswordConfirmation": {
"message": "Are you sure you want to remove the password?"
},
"allSends": {
"message": "All Sends"
},
"searchSends": {
"message": "Search Sends",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendProtectedPassword": {
"message": "This Send is protected with a password. Please type the password below to continue.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendProtectedPasswordDontKnow": {
"message": "Don't know the password? Ask the Sender for the password needed to access this Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendHiddenByDefault": {
"message": "This send is hidden by default. You can toggle its visibility using the button below.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"downloadFile": {
"message": "Stáhnout soubor"
},
"noSendsInList": {
"message": "There are no Sends to list.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}
}

View File

@@ -37,6 +37,9 @@
"password": {
"message": "Adgangskode"
},
"newPassword": {
"message": "Ny adgangskode"
},
"passphrase": {
"message": "Adgangssætning"
},
@@ -1986,13 +1989,13 @@
"message": "Lokal-hosting (valgfri)"
},
"usersGetPremium": {
"message": "Brugere får adgang til premium-medlemskabsfunktioner"
"message": "Brugere får adgang til premium-funktioner"
},
"controlAccessWithGroups": {
"message": "Kontroller brugeradgang med grupper"
},
"syncUsersFromDirectory": {
"message": "Synkroniser dine brugere og grupper fra et kartotek"
"message": "Synkronisér dine brugere og grupper fra et kartotek"
},
"trackAuditLogs": {
"message": "Spor brugerhandlinger med revisionslogfiler"
@@ -2810,9 +2813,11 @@
"nothingSelected": {
"message": "Du har ikke valgt noget."
},
"submitAgreePolicies": {
"message": "Ved at klikke på \"Indsend\"-knappen accepterer du følgende politikker:",
"description": "A policy is something like Terms of Service, Privacy Policy, etc."
"acceptPolicies": {
"message": "Ved at markere dette felt accepterer du følgende:"
},
"acceptPoliciesError": {
"message": "Servicevilkår og fortrolighedspolitik er ikke blevet bekræftet."
},
"termsOfService": {
"message": "Servicevilkår"
@@ -2948,6 +2953,12 @@
"apiKeyWarning": {
"message": "Din API-nøgle har fuld adgang til organisationen. Den skal holdes hemmelig."
},
"userApiKeyDesc": {
"message": "Din API-nøgle kan bruges til godkendelse i Bitwarden-CLI."
},
"userApiKeyWarning": {
"message": "Din API-nøgle er en alternativ godkendelsesmekanisme. Den bør holdes hemmelig."
},
"oauth2ClientCredentials": {
"message": "OAuth 2.0 legitimationsoplysninger",
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
@@ -2986,7 +2997,7 @@
"message": "Kræv at brugerne konfigurerer to-trins-login på deres personlige konti."
},
"twoStepLoginPolicyWarning": {
"message": "Organisationsmedlemmer, der ikke har to-trins login aktiveret på deres personlige konto, fjernes fra organisationen og vil modtage en e-mail, der giver dem besked om ændringen."
"message": "Organisationsmedlemmer, der ikke er ejere eller administratorer og ikke har to-trins login aktiveret på deres personlige konto, fjernes fra organisationen og modtager en e-mail med besked om ændringen."
},
"twoStepLoginPolicyUserWarning": {
"message": "Du er medlem af en organisation, der kræver at to-trins login er aktiveret på din brugerkonto. Hvis du deaktiverer alle to-trins login-udbydere, fjernes du automatisk fra disse organisationer."
@@ -3163,5 +3174,180 @@
},
"taxInfoUpdated": {
"message": "Skatteoplysninger opdateret."
},
"setMasterPassword": {
"message": "Indstil hovedadgangskode"
},
"ssoCompleteRegistration": {
"message": "For at fuldføre indlogning vha. SSO skal en hovedadgangskode opsættes for at tilgå og beskytte din boks."
},
"identifier": {
"message": "Identifikator"
},
"organizationIdentifier": {
"message": "Organisationsidentifikator"
},
"ssoLogInWithOrgIdentifier": {
"message": "Log ind vha. din organisations single sign-on portal. Angiv din organisations identifikator for at begynde."
},
"enterpriseSingleSignOn": {
"message": "Virksomheds Single Sign On"
},
"ssoHandOff": {
"message": "You may now close this tab and continue in the extension."
},
"businessPortal": {
"message": "Virksomhedssportal",
"description": "The web portal used by business organizations for configuring certain features."
},
"includeAllTeamsFeatures": {
"message": "Alle teamfunktioner, plus:"
},
"includeSsoAuthentication": {
"message": "SSO godkendelse via SAML2.0 og OpenID Connect"
},
"includeEnterprisePolicies": {
"message": "Virksomhedspolitikker"
},
"ssoValidationFailed": {
"message": "SSO validering mislykkedes"
},
"ssoIdentifierRequired": {
"message": "Organisationsidentifikator er påkrævet."
},
"unlinkSso": {
"message": "Fjern SSO tilknytning"
},
"linkSso": {
"message": "Tilknyt SSO"
},
"webPoliciesDeprecationWarning": {
"message": "Politikkonfiguration er flyttet, og denne side vil snart blive udfaset. Klik nedenfor for at bruge siden Politikker i forretningsportalen i stedet."
},
"singleOrg": {
"message": "Enkel organisation"
},
"singleOrgDesc": {
"message": "Begræns brugere fra at kunne deltage i andre organisationer."
},
"singleOrgBlockCreateMessage": {
"message": "Din nuværende organisation har en politik, der ikke tillader dig at deltage i mere end en organisation. Kontakt din organisations administratorer, eller tilmeld dig fra en anden Bitwarden-konto."
},
"singleOrgPolicyWarning": {
"message": "Organisationsmedlemmer, der ikke er ejere eller administratorer og allerede er medlem af en anden organisation, fjernes fra din organisation."
},
"requireSso": {
"message": "Single Sign-On autentificering"
},
"requireSsoPolicyDesc": {
"message": "Kræv at brugerne logger ind med Virksomheds Single Sign-On-metoden."
},
"prerequisite": {
"message": "Forudsætning"
},
"requireSsoPolicyReq": {
"message": "Enkel organisations virksomhedspolitikken skal aktiveres, før denne politik aktiveres."
},
"requireSsoPolicyReqError": {
"message": "Enkelt organisationspolitik er ikke aktiveret."
},
"requireSsoExemption": {
"message": "Organisationsejere og administratorer er undtaget fra denne politik."
},
"sendTypeFile": {
"message": "Fil"
},
"sendTypeText": {
"message": "Tekst"
},
"createSend": {
"message": "Opret ny Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editSend": {
"message": "Redigér Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"createdSend": {
"message": "Send oprettet",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editedSend": {
"message": "Send opdateret",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletedSend": {
"message": "Send slettet",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSend": {
"message": "Slet Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSendConfirmation": {
"message": "Er du sikker på, at du vil slette denne Send?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"whatTypeOfSend": {
"message": "Hvilken type Send er denne?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletionDate": {
"message": "Sletningsdato"
},
"expirationDate": {
"message": "Udløbsdato"
},
"maxAccessCount": {
"message": "Maksimal antal tilgange"
},
"currentAccessCount": {
"message": "Aktuelt antal tilgange"
},
"disabled": {
"message": "Deaktiveret"
},
"sendLink": {
"message": "Send link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"copySendLink": {
"message": "Kopiér Send link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"removePassword": {
"message": "Fjern adgangskode"
},
"removedPassword": {
"message": "Adgangskode fjernet"
},
"removePasswordConfirmation": {
"message": "Er du sikker på, at du vil fjerne adgangskoden?"
},
"allSends": {
"message": "Alle Send"
},
"searchSends": {
"message": "Søg Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendProtectedPassword": {
"message": "Denne Send er beskyttet med en adgangskode. Indtast adgangskoden nedenfor for at fortsætte.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendProtectedPasswordDontKnow": {
"message": "Kender du ikke adgangskoden? Bed afsenderen om den nødvendige adgangskode for at få adgang til denne Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendHiddenByDefault": {
"message": "Denne Send er som standard skjult. Du kan skifte synlighed ved hjælp af knappen nedenfor.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"downloadFile": {
"message": "Download fil"
},
"noSendsInList": {
"message": "Der er ingen Send at vise.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}
}

View File

@@ -37,6 +37,9 @@
"password": {
"message": "Passwort"
},
"newPassword": {
"message": "Neues Passwort"
},
"passphrase": {
"message": "Passphrase"
},
@@ -831,7 +834,7 @@
"message": "Worttrennzeichen"
},
"capitalize": {
"message": "Wortanfänge großschreiben",
"message": "Großschreiben",
"description": "Make the first letter of a work uppercase."
},
"includeNumber": {
@@ -1049,7 +1052,7 @@
"description": "Allows scaling the web vault UI's width"
},
"enableFullWidthDesc": {
"message": "Dem Web-Tresor erlauben die volle Breite des Browserfenster zu benutzen."
"message": "Dem Web-Tresor erlauben, die volle Breite des Browserfensters zu benutzen."
},
"default": {
"message": "Standard"
@@ -2643,7 +2646,7 @@
"message": "Informationen zur Steuer"
},
"taxInformationDesc": {
"message": "Bitte kontaktieren Sie unseren Support um Ihre Steuerinformationen Ihrer Rechnung abzufragen (oder zu aktualisieren)."
"message": "Für Kunden innerhalb der USA ist die Postleitzahl erforderlich, um die Umsatzsteuer-Anforderungen zu erfüllen, für andere Länder können Sie optional eine Steuernummer (VAT/GST) und/oder eine Adresse angeben, die auf Ihren Rechnungen erscheint."
},
"billingPlan": {
"message": "Abo",
@@ -2810,9 +2813,11 @@
"nothingSelected": {
"message": "Sie haben keine Auswahl getroffen."
},
"submitAgreePolicies": {
"message": "Durch Klicken des Absenden-Buttons stimmen Sie den folgenden Bedingungen zu:",
"description": "A policy is something like Terms of Service, Privacy Policy, etc."
"acceptPolicies": {
"message": "Durch Anwählen dieses Kästchens erklären Sie sich mit folgendem einverstanden:"
},
"acceptPoliciesError": {
"message": "Die Nutzungsbedingungen und Datenschutzerklärung wurden nicht akzeptiert."
},
"termsOfService": {
"message": "Allgemeine Geschäftsbedingungen"
@@ -2948,6 +2953,12 @@
"apiKeyWarning": {
"message": "Ihr API-Schlüssel hat vollen Zugriff auf die Organisation. Er sollte geheim gehalten werden."
},
"userApiKeyDesc": {
"message": "Ihr API-Schlüssel kann zur Authentifizierung im Bitwarden CLI verwendet werden."
},
"userApiKeyWarning": {
"message": "Ihr API-Schlüssel ist ein alternativer Authentifizierungsmechanismus. Er sollte geheim gehalten werden."
},
"oauth2ClientCredentials": {
"message": "OAuth 2.0 Client Anmeldeinformationen",
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
@@ -3153,15 +3164,190 @@
"message": "Passwörter verstecken"
},
"countryPostalCodeRequiredDesc": {
"message": "We require this information for calculating sales tax and financial reporting only."
"message": "Wir benötigen diese Informationen nur zur Berechnung der Umsatzsteuer und Finanzberichterstattung."
},
"includeVAT": {
"message": "Include VAT/GST Information (optional)"
"message": "MwSt./GST-Informationen einschließen (optional)"
},
"taxIdNumber": {
"message": "Umsatzsteuernummer"
},
"taxInfoUpdated": {
"message": "Steuerinformationen aktualisiert."
},
"setMasterPassword": {
"message": "Masterpasswort festlegen"
},
"ssoCompleteRegistration": {
"message": "Bitte legen Sie ein Masterpasswort für den Schutz Ihres Tresors fest, um die Anmeldung über SSO abzuschließen."
},
"identifier": {
"message": "Kennung"
},
"organizationIdentifier": {
"message": "Organisationskennung"
},
"ssoLogInWithOrgIdentifier": {
"message": "Über den Single Sign-on Ihrer Organisation anmelden. Bitte geben Sie Ihre Organisationskennung an, um zu beginnen."
},
"enterpriseSingleSignOn": {
"message": "Enterprise Single Sign-On"
},
"ssoHandOff": {
"message": "You may now close this tab and continue in the extension."
},
"businessPortal": {
"message": "Unternehmensportal",
"description": "The web portal used by business organizations for configuring certain features."
},
"includeAllTeamsFeatures": {
"message": "Alle Teams Funktionen, zusätzlich:"
},
"includeSsoAuthentication": {
"message": "SSO Authentifikation über SAML2.0 und OpenID Connect"
},
"includeEnterprisePolicies": {
"message": "Unternehmensrichtlinien"
},
"ssoValidationFailed": {
"message": "SSO Validierung fehlgeschlagen"
},
"ssoIdentifierRequired": {
"message": "Unternehmenskennung ist erforderlich."
},
"unlinkSso": {
"message": "SSO aufheben"
},
"linkSso": {
"message": "SSO verknüpfen"
},
"webPoliciesDeprecationWarning": {
"message": "Die Richtlinien-Konfiguration wurde verschoben und diese Seite wird bald nicht mehr aktualisiert. Benutzen Sie stattdessen über den Link die Richtlinien-Seite des Unternehmensportal."
},
"singleOrg": {
"message": "Einzelne Organisation"
},
"singleOrgDesc": {
"message": "Benutzern verbieten, anderen Organisationen beizutreten."
},
"singleOrgBlockCreateMessage": {
"message": "Ihre aktuelle Organisation hat eine Richtlinie, die es Ihnen nicht erlaubt, mehr als einer Organisation beizutreten. Bitte kontaktieren Sie die Administratoren Ihrer Organisation oder melden Sie sich mit einem anderen Bitwarden-Konto an."
},
"singleOrgPolicyWarning": {
"message": "Organisationsmitglieder, die nicht Eigentümer oder Administratoren sind und bereits Mitglied einer anderen Organisation sind, werden aus Ihrer Organisation entfernt."
},
"requireSso": {
"message": "Single Sign-On Authentifizierung"
},
"requireSsoPolicyDesc": {
"message": "Benutzer müssen sich per Enterprise Single Sign-On anmelden."
},
"prerequisite": {
"message": "Voraussetzung"
},
"requireSsoPolicyReq": {
"message": "Die Unternehmensrichtlinie für eine einzelne Organisation muss aktiviert sein, bevor diese Richtlinie aktiviert werden kann."
},
"requireSsoPolicyReqError": {
"message": "Richtlinie für eine einzelne Organisation nicht aktiviert."
},
"requireSsoExemption": {
"message": "Organisationseigentümer und Administratoren sind von der Durchsetzung dieser Richtlinie ausgenommen."
},
"sendTypeFile": {
"message": "Datei"
},
"sendTypeText": {
"message": "Text"
},
"createSend": {
"message": "Neues Send erstellen",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editSend": {
"message": "Send bearbeiten",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"createdSend": {
"message": "Send erstellt",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editedSend": {
"message": "Bearbeitetes Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletedSend": {
"message": "Gelöschtes Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSend": {
"message": "Send löschen",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSendConfirmation": {
"message": "Sind Sie sicher, dass Sie dieses Send löschen möchten?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"whatTypeOfSend": {
"message": "Welche Art von Send ist das?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletionDate": {
"message": "Löschdatum"
},
"expirationDate": {
"message": "Ablaufdatum"
},
"maxAccessCount": {
"message": "Maximale Zugriffsanzahl"
},
"currentAccessCount": {
"message": "Aktuelle Zugriffsanzahl"
},
"disabled": {
"message": "Deaktiviert"
},
"sendLink": {
"message": "Send Link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"copySendLink": {
"message": "Send Link kopieren",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"removePassword": {
"message": "Passwort entfernen"
},
"removedPassword": {
"message": "Passwort entfernt"
},
"removePasswordConfirmation": {
"message": "Sind Sie sicher, dass Sie das Passwort entfernen möchten?"
},
"allSends": {
"message": "Alle Sends"
},
"searchSends": {
"message": "Sends suchen",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendProtectedPassword": {
"message": "Dieses Send ist mit einem Passwort geschützt. Bitte geben Sie unten das Passwort ein, um fortzufahren.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendProtectedPasswordDontKnow": {
"message": "Kennen Sie das Passwort nicht? Fragen Sie den Absender nach dem benötigten Passwort, um auf dieses Send zuzugreifen.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendHiddenByDefault": {
"message": "Dieses Send ist standardmäßig ausgeblendet. Sie können die Sichtbarkeit mit dem Button unten umschalten.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"downloadFile": {
"message": "Datei herunterladen"
},
"noSendsInList": {
"message": "Es gibt keine Sends aufzulisten.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}
}

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