diff --git a/src/apps/haikudepot/Jamfile b/src/apps/haikudepot/Jamfile index 50965713bd..83e075616d 100644 --- a/src/apps/haikudepot/Jamfile +++ b/src/apps/haikudepot/Jamfile @@ -140,6 +140,7 @@ local applicationSources = PackageInfoListener.cpp PackageInfoView.cpp PackageListView.cpp + PasswordRequirements.cpp RatePackageWindow.cpp RatingView.cpp RatingStability.cpp diff --git a/src/apps/haikudepot/model/PasswordRequirements.cpp b/src/apps/haikudepot/model/PasswordRequirements.cpp new file mode 100644 index 0000000000..e5be7774fb --- /dev/null +++ b/src/apps/haikudepot/model/PasswordRequirements.cpp @@ -0,0 +1,93 @@ +/* + * Copyright 2023, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ + + +#include "PasswordRequirements.h" + +// These are keys that are used to store this object's data into a BMessage +// instance. + +#define KEY_MIN_PASSWORD_LENGTH "minPasswordLength" +#define KEY_MIN_PASSWORD_UPPERCASE_CHAR "minPasswordUppercaseChar" +#define KEY_MIN_PASSWORD_DIGITS_CHAR "minPasswordDigitsChar" + + +PasswordRequirements::PasswordRequirements() + : + fMinPasswordLength(0), + fMinPasswordUppercaseChar(0), + fMinPasswordDigitsChar(0) +{ +} + + +PasswordRequirements::PasswordRequirements(BMessage* from) + : + fMinPasswordLength(0), + fMinPasswordUppercaseChar(0), + fMinPasswordDigitsChar(0) +{ + uint32 value; + + if (from->FindUInt32(KEY_MIN_PASSWORD_LENGTH, &value) == B_OK) { + fMinPasswordLength = value; + } + + if (from->FindUInt32( + KEY_MIN_PASSWORD_UPPERCASE_CHAR, &value) == B_OK) { + fMinPasswordUppercaseChar = value; + } + + if (from->FindUInt32( + KEY_MIN_PASSWORD_DIGITS_CHAR, &value) == B_OK) { + fMinPasswordDigitsChar = value; + } +} + + +PasswordRequirements::~PasswordRequirements() +{ +} + + +void +PasswordRequirements::SetMinPasswordLength(uint32 value) +{ + fMinPasswordLength = value; +} + + +void +PasswordRequirements::SetMinPasswordUppercaseChar(uint32 value) +{ + fMinPasswordUppercaseChar = value; +} + + +void +PasswordRequirements::SetMinPasswordDigitsChar(uint32 value) +{ + fMinPasswordDigitsChar = value; +} + + +status_t +PasswordRequirements::Archive(BMessage* into, bool deep) const +{ + status_t result = B_OK; + if (result == B_OK) { + result = into->AddUInt32( + KEY_MIN_PASSWORD_LENGTH, fMinPasswordLength); + } + if (result == B_OK) { + result = into->AddUInt32( + KEY_MIN_PASSWORD_UPPERCASE_CHAR, fMinPasswordUppercaseChar); + } + if (result == B_OK) { + result = into->AddUInt32( + KEY_MIN_PASSWORD_DIGITS_CHAR, fMinPasswordDigitsChar); + } + return result; +} diff --git a/src/apps/haikudepot/model/PasswordRequirements.h b/src/apps/haikudepot/model/PasswordRequirements.h new file mode 100644 index 0000000000..0224cd1c04 --- /dev/null +++ b/src/apps/haikudepot/model/PasswordRequirements.h @@ -0,0 +1,43 @@ +/* + * Copyright 2023, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ +#ifndef PASSWORD_REQUIREMENTS_H +#define PASSWORD_REQUIREMENTS_H + + +#include +#include + + +/*! When a user enters their password there are requirements around that + password such as the length of the password in characters as well as + how many digits it must contain. This class models those + requirements so that they can be conveyed to the user in the UI. +*/ + +class PasswordRequirements : public BArchivable { +public: + PasswordRequirements(BMessage* from); + PasswordRequirements(); + virtual ~PasswordRequirements(); + + const uint32 MinPasswordLength() const + { return fMinPasswordLength; } + const uint32 MinPasswordUppercaseChar() const + { return fMinPasswordUppercaseChar; } + const uint32 MinPasswordDigitsChar() const + { return fMinPasswordDigitsChar; } + + void SetMinPasswordLength(uint32 value); + void SetMinPasswordUppercaseChar(uint32 value); + void SetMinPasswordDigitsChar(uint32 value); + + status_t Archive(BMessage* into, bool deep = true) const; +private: + uint32 fMinPasswordLength; + uint32 fMinPasswordUppercaseChar; + uint32 fMinPasswordDigitsChar; +}; + +#endif // PASSWORD_REQUIREMENTS_H diff --git a/src/apps/haikudepot/server/WebAppInterface.cpp b/src/apps/haikudepot/server/WebAppInterface.cpp index 6c89217286..8bdfb5b232 100644 --- a/src/apps/haikudepot/server/WebAppInterface.cpp +++ b/src/apps/haikudepot/server/WebAppInterface.cpp @@ -1,6 +1,6 @@ /* * Copyright 2014, Stephan Aßmus . - * Copyright 2016-2022, Andrew Lindesay . + * Copyright 2016-2023, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ @@ -741,6 +741,57 @@ WebAppInterface::IncrementViewCounter(const PackageInfoRef package, } +status_t +WebAppInterface::RetrievePasswordRequirements( + PasswordRequirements& passwordRequirements) +{ + BMessage responseEnvelopeMessage; + status_t result = _RetrievePasswordRequirementsMeta( + responseEnvelopeMessage); + + if (result != B_OK) + return result; + + BMessage resultMessage; + + result = responseEnvelopeMessage.FindMessage("result", &resultMessage); + + if (result != B_OK) { + HDERROR("bad response envelope missing 'result' entry"); + return result; + } + + double value; + + if (resultMessage.FindDouble("minPasswordLength", &value) == B_OK) + passwordRequirements.SetMinPasswordLength((uint32) value); + + if (resultMessage.FindDouble("minPasswordUppercaseChar", &value) == B_OK) + passwordRequirements.SetMinPasswordUppercaseChar((uint32) value); + + if (resultMessage.FindDouble("minPasswordDigitsChar", &value) == B_OK) + passwordRequirements.SetMinPasswordDigitsChar((uint32) value); + + return result; +} + + +status_t +WebAppInterface::_RetrievePasswordRequirementsMeta(BMessage& message) +{ + BMallocIO* requestEnvelopeData = new BMallocIO(); + // BHttpRequest later takes ownership of this. + BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); + + requestEnvelopeWriter.WriteObjectStart(); + requestEnvelopeWriter.WriteObjectEnd(); + + return _SendJsonRequest("user/get-password-requirements", + requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), + 0, message); +} + + /*! JSON-RPC invocations return a response. The response may be either a result or it may be an error depending on the response structure. If it is an error then there may be additional detail that is the diff --git a/src/apps/haikudepot/server/WebAppInterface.h b/src/apps/haikudepot/server/WebAppInterface.h index ce13d7f012..e97148966d 100644 --- a/src/apps/haikudepot/server/WebAppInterface.h +++ b/src/apps/haikudepot/server/WebAppInterface.h @@ -1,6 +1,6 @@ /* * Copyright 2014, Stephan Aßmus . - * Copyright 2016-2022, Andrew Lindesay . + * Copyright 2016-2023, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #ifndef WEB_APP_INTERFACE_H @@ -13,6 +13,7 @@ #include #include "PackageInfo.h" +#include "PasswordRequirements.h" #include "UserCredentials.h" #include "UserDetail.h" #include "UserUsageConditions.h" @@ -129,6 +130,9 @@ public: const DepotInfoRef depot, BMessage& message); + status_t RetrievePasswordRequirements( + PasswordRequirements& passwordRequirements); + static int32 ErrorCodeFromResponse( BMessage& responseEnvelopeMessage); @@ -137,6 +141,8 @@ public: UserDetail& userDetail); private: + status_t _RetrievePasswordRequirementsMeta( + BMessage& message); status_t _RetrieveUserUsageConditionsMeta( const BString& code, BMessage& message); diff --git a/src/apps/haikudepot/ui/UserLoginWindow.cpp b/src/apps/haikudepot/ui/UserLoginWindow.cpp index d8a0f432cd..2e185cf10d 100644 --- a/src/apps/haikudepot/ui/UserLoginWindow.cpp +++ b/src/apps/haikudepot/ui/UserLoginWindow.cpp @@ -1,6 +1,6 @@ /* * Copyright 2014, Stephan Aßmus . - * Copyright 2019-2021, Andrew Lindesay . + * Copyright 2019-2023, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ @@ -47,6 +47,7 @@ #define KEY_USER_CREDENTIALS "userCredentials" #define KEY_CAPTCHA_IMAGE "captchaImage" #define KEY_USER_USAGE_CONDITIONS "userUsageConditions" +#define KEY_PASSWORD_REQUIREMENTS "passwordRequirements" #define KEY_VALIDATION_FAILURES "validationFailures" @@ -67,7 +68,8 @@ enum { MSG_LOGIN_ERROR = 'lter', MSG_CREATE_ACCOUNT_SUCCESS = 'csuc', MSG_CREATE_ACCOUNT_FAILED = 'cfai', - MSG_CREATE_ACCOUNT_ERROR = 'cfae' + MSG_CREATE_ACCOUNT_ERROR = 'cfae', + MSG_VIEW_PASSWORD_REQUIREMENTS = 'vpar' }; @@ -78,7 +80,8 @@ enum { enum CreateAccountSetupMask { CREATE_CAPTCHA = 1 << 1, - FETCH_USER_USAGE_CONDITIONS = 1 << 2 + FETCH_USER_USAGE_CONDITIONS = 1 << 2, + FETCH_PASSWORD_REQUIREMENTS = 1 << 3 }; @@ -121,6 +124,7 @@ UserLoginWindow::UserLoginWindow(BWindow* parent, BRect frame, Model& model) B_FLOATING_WINDOW_LOOK, B_FLOATING_SUBSET_WINDOW_FEEL, B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS | B_NOT_RESIZABLE | B_NOT_ZOOMABLE | B_CLOSE_ON_ESCAPE), + fPasswordRequirements(NULL), fUserUsageConditions(NULL), fCaptcha(NULL), fPreferredLanguageCode(LANGUAGE_DEFAULT_CODE), @@ -178,6 +182,10 @@ UserLoginWindow::UserLoginWindow(BWindow* parent, BRect frame, Model& model) B_TRANSLATE("View the usage conditions"), new BMessage(MSG_VIEW_LATEST_USER_USAGE_CONDITIONS)); fUserUsageConditionsLink->SetTarget(this); + fPasswordRequirementsLink = new LinkView("password requirements view", + B_TRANSLATE("View the password requirements"), + new BMessage(MSG_VIEW_PASSWORD_REQUIREMENTS)); + fPasswordRequirementsLink->SetTarget(this); // Setup modification messages on all text fields to trigger validation // of input @@ -208,14 +216,15 @@ UserLoginWindow::UserLoginWindow(BWindow* parent, BRect frame, Model& model) BLayoutBuilder::Grid<>(createAccountCard) .AddTextControl(fNewNicknameField, 0, 0) .AddTextControl(fNewPasswordField, 0, 1) - .AddTextControl(fRepeatPasswordField, 0, 2) - .AddTextControl(fEmailField, 0, 3) - .AddMenuField(fLanguageCodeField, 0, 4) - .Add(fCaptchaView, 0, 5) - .Add(fCaptchaResultField, 1, 5) - .Add(fConfirmMinimumAgeCheckBox, 1, 6) - .Add(fConfirmUserUsageConditionsCheckBox, 1, 7) - .Add(fUserUsageConditionsLink, 1, 8) + .Add(fPasswordRequirementsLink, 1, 2) + .AddTextControl(fRepeatPasswordField, 0, 3) + .AddTextControl(fEmailField, 0, 4) + .AddMenuField(fLanguageCodeField, 0, 5) + .Add(fCaptchaView, 0, 6) + .Add(fCaptchaResultField, 1, 6) + .Add(fConfirmMinimumAgeCheckBox, 1, 7) + .Add(fConfirmUserUsageConditionsCheckBox, 1, 8) + .Add(fUserUsageConditionsLink, 1, 9) .SetInsets(B_USE_DEFAULT_SPACING) ; fTabView->AddTab(createAccountCard); @@ -265,6 +274,10 @@ UserLoginWindow::MessageReceived(BMessage* message) _ViewUserUsageConditions(); break; + case MSG_VIEW_PASSWORD_REQUIREMENTS: + _ViewPasswordRequirements(); + break; + case MSG_SEND: switch (fMode) { case LOGIN: @@ -401,6 +414,7 @@ UserLoginWindow::_EnableMutableControls(bool enabled) fConfirmMinimumAgeCheckBox->SetEnabled(enabled); fConfirmUserUsageConditionsCheckBox->SetEnabled(enabled); fUserUsageConditionsLink->SetEnabled(enabled); + fPasswordRequirementsLink->SetEnabled(enabled); fSendButton->SetEnabled(enabled); } @@ -667,6 +681,8 @@ UserLoginWindow::_CreateAccountSetupIfNecessary() setupMask |= CREATE_CAPTCHA; if (fUserUsageConditions == NULL) setupMask |= FETCH_USER_USAGE_CONDITIONS; + if (fPasswordRequirements == NULL) + setupMask |= FETCH_PASSWORD_REQUIREMENTS; _CreateAccountSetup(setupMask); } @@ -695,6 +711,8 @@ UserLoginWindow::_CreateAccountSetup(uint32 mask) _SetCaptcha(NULL); if ((mask & FETCH_USER_USAGE_CONDITIONS) != 0) _SetUserUsageConditions(NULL); + if ((mask & FETCH_PASSWORD_REQUIREMENTS) != 0) + _SetPasswordRequirements(NULL); Unlock(); @@ -722,9 +740,13 @@ UserLoginWindow::_CreateAccountSetupThreadEntry(void* data) status_t result = B_OK; Captcha captcha; UserUsageConditions userUsageConditions; + PasswordRequirements passwordRequirements; + bool shouldCreateCaptcha = (threadData->mask & CREATE_CAPTCHA) != 0; bool shouldFetchUserUsageConditions = (threadData->mask & FETCH_USER_USAGE_CONDITIONS) != 0; + bool shouldFetchPasswordRequirements + = (threadData->mask & FETCH_PASSWORD_REQUIREMENTS) != 0; if (result == B_OK && shouldCreateCaptcha) result = threadData->window->_CreateAccountCaptchaSetupThread(captcha); @@ -732,6 +754,16 @@ UserLoginWindow::_CreateAccountSetupThreadEntry(void* data) result = threadData->window ->_CreateAccountUserUsageConditionsSetupThread(userUsageConditions); } + if (result == B_OK && shouldFetchPasswordRequirements) { + result = threadData->window + ->_CreateAccountPasswordRequirementsSetupThread( + passwordRequirements); + HDINFO("password requirements fetched; len %" B_PRId32 + ", caps %" B_PRId32 ", digits %" B_PRId32, + passwordRequirements.MinPasswordLength(), + passwordRequirements.MinPasswordUppercaseChar(), + passwordRequirements.MinPasswordUppercaseChar()); + } if (result == B_OK) { BMessage message(MSG_CREATE_ACCOUNT_SETUP_SUCCESS); @@ -749,6 +781,14 @@ UserLoginWindow::_CreateAccountSetupThreadEntry(void* data) &userUsageConditionsMessage); } } + if (result == B_OK && shouldFetchPasswordRequirements) { + BMessage passwordRequirementsMessage; + result = passwordRequirements.Archive(&passwordRequirementsMessage); + if (result == B_OK) { + result = message.AddMessage(KEY_PASSWORD_REQUIREMENTS, + &passwordRequirementsMessage); + } + } if (result == B_OK) { HDDEBUG("successfully completed collection of create account " "data from the server in background thread"); @@ -792,6 +832,27 @@ UserLoginWindow::_CreateAccountUserUsageConditionsSetupThread( } +status_t +UserLoginWindow::_CreateAccountPasswordRequirementsSetupThread( + PasswordRequirements& passwordRequirements) +{ + WebAppInterface interface = fModel.GetWebAppInterface(); + status_t result = interface.RetrievePasswordRequirements( + passwordRequirements); + + if (result != B_OK) { + AppUtils::NotifySimpleError( + B_TRANSLATE("Password requirements download problem"), + B_TRANSLATE("An error has arisen downloading the password " + "requirements required to create a new user. Check the log for " + "details and try again. " + ALERT_MSG_LOGS_USER_GUIDE)); + } + + return result; +} + + status_t UserLoginWindow::_CreateAccountCaptchaSetupThread(Captcha& captcha) { @@ -891,8 +952,10 @@ void UserLoginWindow::_HandleCreateAccountSetupSuccess(BMessage* message) { HDDEBUG("handling account setup success"); + BMessage captchaMessage; BMessage userUsageConditionsMessage; + BMessage passwordRequirementsMessage; if (message->FindMessage(KEY_CAPTCHA_IMAGE, &captchaMessage) == B_OK) _SetCaptcha(new Captcha(&captchaMessage)); @@ -903,6 +966,12 @@ UserLoginWindow::_HandleCreateAccountSetupSuccess(BMessage* message) new UserUsageConditions(&userUsageConditionsMessage)); } + if (message->FindMessage(KEY_PASSWORD_REQUIREMENTS, + &passwordRequirementsMessage) == B_OK) { + _SetPasswordRequirements( + new PasswordRequirements(&passwordRequirementsMessage)); + } + _EnableMutableControls(true); } @@ -953,6 +1022,24 @@ UserLoginWindow::_SetUserUsageConditions( } +void +UserLoginWindow::_SetPasswordRequirements( + PasswordRequirements* passwordRequirements) +{ + HDDEBUG("setting password requirements"); + if (fPasswordRequirements != NULL) + delete fPasswordRequirements; + fPasswordRequirements = passwordRequirements; + if (fPasswordRequirements != NULL) { + HDDEBUG("password requirements set to; len %" B_PRId32 + ", caps %" B_PRId32 ", digits %" B_PRId32, + fPasswordRequirements->MinPasswordLength(), + fPasswordRequirements->MinPasswordUppercaseChar(), + fPasswordRequirements->MinPasswordUppercaseChar()); + } +} + + // #pragma mark - Create Account @@ -1357,3 +1444,27 @@ UserLoginWindow::_ViewUserUsageConditions() fModel, *fUserUsageConditions); window->Show(); } + + +void +UserLoginWindow::_ViewPasswordRequirements() +{ + if (fPasswordRequirements == NULL) + HDFATAL("the password requirements must have been setup"); + BString msg = B_TRANSLATE("The password must be a minimum of " + "%MinPasswordLength% characters. " + "%MinPasswordUppercaseChar% characters must be upper-case and " + "%MinPasswordDigitsChar% characters must be digits."); + msg.ReplaceAll("%MinPasswordLength%", + BString() << fPasswordRequirements->MinPasswordLength()); + msg.ReplaceAll("%MinPasswordUppercaseChar%", + BString() << fPasswordRequirements->MinPasswordUppercaseChar()); + msg.ReplaceAll("%MinPasswordDigitsChar%", + BString() << fPasswordRequirements->MinPasswordDigitsChar()); + + BAlert* alert = new(std::nothrow) BAlert( + B_TRANSLATE("Password requirements"), msg, B_TRANSLATE("OK")); + + if (alert != NULL) + alert->Go(); +} diff --git a/src/apps/haikudepot/ui/UserLoginWindow.h b/src/apps/haikudepot/ui/UserLoginWindow.h index 2b50a81bf3..1ad554831b 100644 --- a/src/apps/haikudepot/ui/UserLoginWindow.h +++ b/src/apps/haikudepot/ui/UserLoginWindow.h @@ -1,6 +1,6 @@ /* * Copyright 2014, Stephan Aßmus . - * Copyright 2019, Andrew Lindesay . + * Copyright 2019-2023, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #ifndef USER_LOGIN_WINDOW_H @@ -12,6 +12,7 @@ #include "CreateUserDetail.h" #include "PackageInfo.h" +#include "PasswordRequirements.h" #include "UserCredentials.h" #include "ValidationFailure.h" @@ -95,6 +96,8 @@ private: Captcha& captcha); status_t _CreateAccountUserUsageConditionsSetupThread( UserUsageConditions& userUsageConditions); + status_t _CreateAccountPasswordRequirementsSetupThread( + PasswordRequirements& passwordRequirements); status_t _UnpackCaptcha(BMessage& responsePayload, Captcha& captcha); void _HandleCreateAccountSetupSuccess( @@ -103,12 +106,15 @@ private: void _SetCaptcha(Captcha* captcha); void _SetUserUsageConditions( UserUsageConditions* userUsageConditions); + void _SetPasswordRequirements( + PasswordRequirements* passwordRequirements); void _CollectValidationFailures( const BMessage& result, BString& error) const; void _ViewUserUsageConditions(); + void _ViewPasswordRequirements(); void _TakeUpCredentialsAndQuit( const UserCredentials& credentials); @@ -131,6 +137,7 @@ private: BTextControl* fNewNicknameField; BTextControl* fNewPasswordField; BTextControl* fRepeatPasswordField; + LinkView* fPasswordRequirementsLink; BTextControl* fEmailField; BMenuField* fLanguageCodeField; BitmapView* fCaptchaView; @@ -142,6 +149,8 @@ private: BButton* fSendButton; BButton* fCancelButton; + PasswordRequirements* + fPasswordRequirements; UserUsageConditions* fUserUsageConditions; Captcha* fCaptcha;