From 16f897e9ca6c49f8c17ac5ea3b05070761929d6c Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Sun, 18 Feb 2024 14:00:48 +0100 Subject: [PATCH 1/3] implemented helper for password reset, special case not needing auth and a user we are still missing a sendmail agent on the docker setup to make sending emails working --- src/api/v2/index.php | 3 +- .../apiv2/helper/resetUserPassword.routes.php | 39 ++++++++++++++ src/inc/handlers/ForgotHandler.class.php | 51 +++++-------------- src/inc/utils/UserUtils.class.php | 26 ++++++++++ 4 files changed, 79 insertions(+), 40 deletions(-) create mode 100644 src/inc/apiv2/helper/resetUserPassword.routes.php diff --git a/src/api/v2/index.php b/src/api/v2/index.php index c02c7e82a..777f47b9e 100644 --- a/src/api/v2/index.php +++ b/src/api/v2/index.php @@ -132,7 +132,7 @@ public function get($key): string { include(dirname(__FILE__) . '/../../inc/confv2.php'); return new JwtAuthentication([ "path" => "/", - "ignore" => ["/api/v2/auth/token", "/api/v2/openapi.json"], + "ignore" => ["/api/v2/auth/token", "/api/v2/helper/resetUserPassword", "/api/v2/openapi.json"], "secret" => $PEPPER[0], "attribute" => false, "secure" => false, @@ -272,6 +272,7 @@ public function process(Request $request, RequestHandler $handler): Response { require __DIR__ . "/../../inc/apiv2/helper/importFile.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/purgeTask.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/resetChunk.routes.php"; +require __DIR__ . "/../../inc/apiv2/helper/resetUserPassword.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/setUserPassword.routes.php"; $app->run(); diff --git a/src/inc/apiv2/helper/resetUserPassword.routes.php b/src/inc/apiv2/helper/resetUserPassword.routes.php new file mode 100644 index 000000000..972ed2a7a --- /dev/null +++ b/src/inc/apiv2/helper/resetUserPassword.routes.php @@ -0,0 +1,39 @@ + ["type" => "str"], + User::USERNAME => ["type" => "str"], + ]; + } + + public function actionPost($data): array|null { + UserUtils::userForgotPassword($data[User::USERNAME], $data[User::EMAIL]); + + return []; + } +} + +ResetUserPasswordHelperAPI::register($app); \ No newline at end of file diff --git a/src/inc/handlers/ForgotHandler.class.php b/src/inc/handlers/ForgotHandler.class.php index a2ffbe18e..20939b9ad 100644 --- a/src/inc/handlers/ForgotHandler.class.php +++ b/src/inc/handlers/ForgotHandler.class.php @@ -1,51 +1,24 @@ forgot($_POST['username'], $_POST['email']); - break; - default: - UI::addMessage(UI::ERROR, "Invalid action!"); - break; - } - } - - private function forgot($username, $email) { - $username = htmlentities($username, ENT_QUOTES, "UTF-8"); - $qF = new QueryFilter(User::USERNAME, $username, "="); - $res = Factory::getUserFactory()->filter([Factory::FILTER => $qF]); - if ($res == null || sizeof($res) == 0) { - UI::addMessage(UI::ERROR, "No such user!"); - return; - } - $user = $res[0]; - if ($user->getEmail() != $email) { - UI::addMessage(UI::ERROR, "No such user!"); - return; - } - $newSalt = Util::randomString(20); - $newPass = Util::randomString(10); - $newHash = Encryption::passwordHash($newPass, $newSalt); - - $tmpl = new Template("email/forgot"); - $tmplPlain = new Template("email/forgot.plain"); - $obj = array('username' => $user->getUsername(), 'password' => $newPass); - if (Util::sendMail($user->getEmail(), "Password reset", $tmpl->render($obj), $tmplPlain->render($obj))) { - Factory::getUserFactory()->mset($user, [User::PASSWORD_HASH => $newHash, User::PASSWORD_SALT => $newSalt, User::IS_COMPUTED_PASSWORD => 1]); - UI::addMessage(UI::SUCCESS, "Password reset! You should receive an email soon."); + try { + switch ($action) { + case DForgotAction::RESET: + UserUtils::userForgotPassword($_POST['username'], $_POST['email']); + UI::addMessage(UI::SUCCESS, "Password reset! You should receive an email soon."); + break; + default: + UI::addMessage(UI::ERROR, "Invalid action!"); + break; + } } - else { - UI::addMessage(UI::ERROR, "Password reset failed because of an error when sending the email! Please check if PHP is able to send emails."); + catch (HTException $e) { + UI::addMessage(UI::ERROR, $e->getMessage()); } } } \ No newline at end of file diff --git a/src/inc/utils/UserUtils.class.php b/src/inc/utils/UserUtils.class.php index 94357f526..a7b7dcab9 100644 --- a/src/inc/utils/UserUtils.class.php +++ b/src/inc/utils/UserUtils.class.php @@ -48,6 +48,32 @@ public static function deleteUser($userId, $adminUser) { Factory::getUserFactory()->delete($user); } + public static function userForgotPassword($username, $email) { + $username = htmlentities($username, ENT_QUOTES, "UTF-8"); + $qF = new QueryFilter(User::USERNAME, $username, "="); + $res = Factory::getUserFactory()->filter([Factory::FILTER => $qF]); + if ($res == null || sizeof($res) == 0) { + throw new HTException("No such user!"); + } + $user = $res[0]; + if ($user->getEmail() != $email) { + throw new HTException("No such user!"); + } + $newSalt = Util::randomString(20); + $newPass = Util::randomString(10); + $newHash = Encryption::passwordHash($newPass, $newSalt); + + $tmpl = new Template("email/forgot"); + $tmplPlain = new Template("email/forgot.plain"); + $obj = array('username' => $user->getUsername(), 'password' => $newPass); + if (Util::sendMail($user->getEmail(), "Password reset", $tmpl->render($obj), $tmplPlain->render($obj))) { + Factory::getUserFactory()->mset($user, [User::PASSWORD_HASH => $newHash, User::PASSWORD_SALT => $newSalt, User::IS_COMPUTED_PASSWORD => 1]); + } + else { + throw new HTException("Password reset failed because of an error when sending the email! Please check if PHP is able to send emails."); + } + } + /** * @param int $userId * @throws HTException From 1ca1b22acca9b13a8fe9bf026657ba82f054a5cd Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Sun, 18 Feb 2024 15:28:31 +0100 Subject: [PATCH 2/3] updated response sending a success message --- src/inc/apiv2/helper/resetUserPassword.routes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inc/apiv2/helper/resetUserPassword.routes.php b/src/inc/apiv2/helper/resetUserPassword.routes.php index 972ed2a7a..e973907c2 100644 --- a/src/inc/apiv2/helper/resetUserPassword.routes.php +++ b/src/inc/apiv2/helper/resetUserPassword.routes.php @@ -32,7 +32,7 @@ public function getFormFields(): array { public function actionPost($data): array|null { UserUtils::userForgotPassword($data[User::USERNAME], $data[User::EMAIL]); - return []; + return ["reset" => "success"]; } } From fb75f9c7021749ccd47534e9d2c5b58c5f095e10 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 21 Feb 2024 10:52:58 +0000 Subject: [PATCH 3/3] added docker setup for ssmtp and configuration of smtp connections for emails --- Dockerfile | 15 +++++++++++++++ docker-compose.yml | 9 +++++++++ docker-entrypoint.sh | 3 +++ env.example | 9 +++++++++ second-level-docker-entry.sh | 16 ++++++++++++++++ .../apiv2/helper/resetUserPassword.routes.php | 1 + 6 files changed, 53 insertions(+) create mode 100755 second-level-docker-entry.sh diff --git a/Dockerfile b/Dockerfile index 378fd3a83..150328e0e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,15 @@ ENV HASHTOPOLIS_LOG_PATH=${HASHTOPOLIS_PATH}/log ENV HASHTOPOLIS_CONFIG_PATH=${HASHTOPOLIS_PATH}/config ENV HASHTOPOLIS_BINARIES_PATH=${HASHTOPOLIS_PATH}/binaries +ENV HASHTOPOLIS_SSMTP_ENABLE=0 +ENV HASHTOPOLIS_SSMTP_ROOT=hashtopolis@example.org +ENV HASHTOPOLIS_SSMTP_MAILHUB=example.org:465 +ENV HASHTOPOLIS_SSMTP_HOSTNAME=hashtopolis.example.org +ENV HASHTOPOLIS_SSMTP_USE_TLS=Yes +ENV HASHTOPOLIS_SSMTP_USE_STARTTLS=No +ENV HASHTOPOLIS_SSMTP_AUTH_USER=xxxx +ENV HASHTOPOLIS_SSMTP_AUTH_PASS=xxxx + # Add support for TLS inspection corporate setups, see .env.sample for details ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt @@ -39,6 +48,7 @@ RUN apt-get update \ && apt-get -y install git iproute2 procps lsb-release \ && apt-get -y install mariadb-client \ && apt-get -y install libpng-dev \ + && apt-get -y install ssmtp sudo \ \ # Install extensions (optional) && docker-php-ext-install pdo_mysql gd \ @@ -77,6 +87,11 @@ COPY --from=preprocess /HEA[D] ${HASHTOPOLIS_DOCUMENT_ROOT}/../.git/ COPY composer.json ${HASHTOPOLIS_DOCUMENT_ROOT}/../ RUN composer install --working-dir=${HASHTOPOLIS_DOCUMENT_ROOT}/.. +RUN echo "www-data ALL=NOPASSWD:SETENV: /usr/local/bin/second-level-docker-entry.sh" >> /etc/sudoers.d/10_docker +COPY second-level-docker-entry.sh /usr/local/bin + +RUN echo "" > /etc/ssmtp/ssmtp.conf + ENV DEBIAN_FRONTEND=dialog COPY docker-entrypoint.sh /usr/local/bin diff --git a/docker-compose.yml b/docker-compose.yml index dc17be161..8182dbcfe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,15 @@ services: HASHTOPOLIS_ADMIN_USER: $HASHTOPOLIS_ADMIN_USER HASHTOPOLIS_ADMIN_PASSWORD: $HASHTOPOLIS_ADMIN_PASSWORD HASHTOPOLIS_APIV2_ENABLE: $HASHTOPOLIS_APIV2_ENABLE + + HASHTOPOLIS_SSMTP_ENABLE: $HASHTOPOLIS_SSMTP_ENABLE + HASHTOPOLIS_SSMTP_ROOT: $HASHTOPOLIS_SSMTP_ROOT + HASHTOPOLIS_SSMTP_MAILHUB: $HASHTOPOLIS_SSMTP_MAILHUB + HASHTOPOLIS_SSMTP_HOSTNAME: $HASHTOPOLIS_SSMTP_HOSTNAME + HASHTOPOLIS_SSMTP_USE_TLS: $HASHTOPOLIS_SSMTP_USE_TLS + HASHTOPOLIS_SSMTP_USE_STARTTLS: $HASHTOPOLIS_SSMTP_USE_STARTTLS + HASHTOPOLIS_SSMTP_AUTH_USER: $HASHTOPOLIS_SSMTP_AUTH_USER + HASHTOPOLIS_SSMTP_AUTH_PASS: $HASHTOPOLIS_SSMTP_AUTH_PASS depends_on: - db ports: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 7bed554f6..75d08a089 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -10,6 +10,9 @@ for path in ${paths[@]}; do fi done +echo "Running required root setups." +sudo -E /usr/local/bin/second-level-docker-entry.sh + echo "Testing database." MYSQL="mysql -u${HASHTOPOLIS_DB_USER} -p${HASHTOPOLIS_DB_PASS} -h ${HASHTOPOLIS_DB_HOST}" $MYSQL -e "SELECT 1" > /dev/null 2>&1 diff --git a/env.example b/env.example index f8d3ba184..75b091644 100644 --- a/env.example +++ b/env.example @@ -9,3 +9,12 @@ HASHTOPOLIS_DB_HOST=db HASHTOPOLIS_APIV2_ENABLE=0 HASHTOPOLIS_BACKEND_URL=http://localhost:8080/api/v2 + +HASHTOPOLIS_SSMTP_ENABLE=0 +HASHTOPOLIS_SSMTP_ROOT=hashtopolis@example.org +HASHTOPOLIS_SSMTP_MAILHUB=example.org:465 +HASHTOPOLIS_SSMTP_HOSTNAME=hashtopolis.example.org +HASHTOPOLIS_SSMTP_USE_TLS=Yes +HASHTOPOLIS_SSMTP_USE_STARTTLS=No +HASHTOPOLIS_SSMTP_AUTH_USER=username +HASHTOPOLIS_SSMTP_AUTH_PASS=password \ No newline at end of file diff --git a/second-level-docker-entry.sh b/second-level-docker-entry.sh new file mode 100755 index 000000000..4fee18623 --- /dev/null +++ b/second-level-docker-entry.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# set up SSMTP config +if [[ $HASHTOPOLIS_SSMTP_ENABLE == 1 ]]; then + echo "Setting up SSMTP config..." + echo -e "\ +root=${HASHTOPOLIS_SSMTP_ROOT}\n\ +mailhub=${HASHTOPOLIS_SSMTP_MAILHUB}\n\ +hostname=${HASHTOPOLIS_SSMTP_HOSTNAME}\n\ +UseTLS=${HASHTOPOLIS_SSMTP_USE_TLS}\n\ +UseSTARTTLS=${HASHTOPOLIS_SSMTP_USE_STARTTLS}\n\ +AuthUser=${HASHTOPOLIS_SSMTP_AUTH_USER}\n\ +AuthPass=${HASHTOPOLIS_SSMTP_AUTH_PASS}\n\ +FromLineOverride=NO\n\ +#Debug=YES\n" > /etc/ssmtp/ssmtp.conf +fi \ No newline at end of file diff --git a/src/inc/apiv2/helper/resetUserPassword.routes.php b/src/inc/apiv2/helper/resetUserPassword.routes.php index e973907c2..82b23d2b7 100644 --- a/src/inc/apiv2/helper/resetUserPassword.routes.php +++ b/src/inc/apiv2/helper/resetUserPassword.routes.php @@ -32,6 +32,7 @@ public function getFormFields(): array { public function actionPost($data): array|null { UserUtils::userForgotPassword($data[User::USERNAME], $data[User::EMAIL]); + # TODO: Check how to handle custom return messages that are not object, probably we want that to be in some kind of standardized form. return ["reset" => "success"]; } }