diff --git a/README.md b/README.md index 3baee3d..b590ded 100755 --- a/README.md +++ b/README.md @@ -63,7 +63,20 @@ as dependencies. * Change directory to project home * `git pull` * `composer update` +* look for changes * `php bin/phpmig migrate` ## Translations ## -This app uses Symfony Translator. It's bootstraped in `Util\TranslationHelper` and locales are placed under `data/locale/`. Adjust to your needs or help translating. \ No newline at end of file +This app uses Symfony Translator. It's bootstraped in `Util\TranslationHelper` and locales are placed under `data/locale/`. Adjust to your needs or help translating. + +## Changelog ## +- 0.1.1: + - updated readme and `env.example` + - fix some language validator inconsistencies + - added admin notifications + - added possiblity for users to delete their account + - added back index page + - works with mod_admin_rest version [afc42d7](https://github.com/snowblindroan/mod_admin_rest/commit/afc42d70f0aceb2351a1bc786d61e3f4dbdfb948) +- 0.1: + - initial release + - works with mod_admin_rest version [afc42d7](https://github.com/snowblindroan/mod_admin_rest/commit/afc42d70f0aceb2351a1bc786d61e3f4dbdfb948) \ No newline at end of file diff --git a/config/Routes.php b/config/Routes.php index 9a33509..b2b324b 100644 --- a/config/Routes.php +++ b/config/Routes.php @@ -16,15 +16,18 @@ $container[InternalApplicationError::class] = function ($c) { }; // pages -$container[IndexAction::class] = function ($c) { - return new IndexAction($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator')); +$container[HomeAction::class] = function ($c) { + return new HomeAction($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator')); }; $container[SignUpAction::class] = function ($c) { - return new SignUpAction($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator')); + return new SignUpAction($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator'), $c->get('router')); }; $container[VerificationAction::class] = function ($c) { return new VerificationAction($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator')); }; +$container[DeleteAction::class] = function ($c) { + return new DeleteAction($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator')); +}; // Routes // error @@ -34,6 +37,7 @@ $app->get('/404', NotFoundAction::class)->setName('404'); $app->get('/500', InternalApplicationError::class)->setName('500'); // pages -$app->get('/', IndexAction::class)->setName('/'); +$app->get('/', HomeAction::class)->setName('/'); $app->map(['GET', 'POST'], '/signup', SignUpAction::class)->setName('signup'); -$app->get('/verification/{verificationCode}', VerificationAction::class)->setName('verification'); \ No newline at end of file +$app->get('/verification/{verificationCode}', VerificationAction::class)->setName('verification'); +$app->map(['GET', 'POST'], '/delete', DeleteAction::class)->setName('delete'); \ No newline at end of file diff --git a/config/env.example b/config/env.example index 1319f73..44a4a79 100644 --- a/config/env.example +++ b/config/env.example @@ -1,6 +1,8 @@ # site settings site_title="" -site_navbar_index_displayname="Sign Up" +site_navbar_home_displayname="Home" +site_navbar_signup_displayname="Sign up" +site_navbar_delete_displayname="Delete Account" site_navbar_backlink_enabled="false" # enables a link in the navbar to go back to e.g. main server site site_navbar_backlink_uri="" site_navbar_backlink_displayname="" @@ -10,16 +12,21 @@ site_xmpp_server_displayname="jabber.server.org" verification_cleanup_time="7 day" # mod_admin_rest Settings -xmpp_curl_uri="/admin_rest" # uri to admin_rest +xmpp_curl_uri="/admin_rest" # full uri to admin_rest xmpp_curl_auth_admin_username="" # configured in prosody lua file xmpp_curl_auth_admin_password="" # configured in prosody lua file # Mail Settings +# main mail_host="" mail_port="587" mail_secure="tls" -mail_auth="true" +mail_auth=true mail_username="" mail_password="" mail_from="webmaster@jabber.server.org" -mail_from_name="jabber.server.org" \ No newline at end of file +mail_from_name="jabber.server.org" + +# notification +mail_notify="true" # sends an email to mail_notify_to if a new user successfully verified their account +mail_notify_to=${mail_from} # defaults to sender mail, e.g. webmaster, maybe change this e.g. to "xx@xx" \ No newline at end of file diff --git a/data/locale/messages.en.yml b/data/locale/messages.en.yml index cef04e2..4daf59a 100644 --- a/data/locale/messages.en.yml +++ b/data/locale/messages.en.yml @@ -1,3 +1,9 @@ +# Home +home.title: Home +home.text: | + Hi, + welcome to the free jabber service %server%. Sign up now by clicking the "Sign up" button in the navigation bar. + # Sign up sign.up.title: Sign Up sign.up.flash.success: Signed up successfully. Check your inbox. @@ -13,7 +19,7 @@ sign.up.form.password: Password: sign.up.form.password.placeholder: Password: # Verification -verification.mail.subject: %server% jabber account verification +verification.mail.subject: %server%: jabber account verification verification.mail.body: | Hello %username%, you've signed up for a jabber account on %server%. @@ -22,14 +28,29 @@ verification.code.invalid: Verification code %verificationCode% is not valid. verification.flash.already_in_use_username: %username% is already in use. verification.flash.success: Verification successful. You can now sign in to your newly created jabber account %username%@hoth.one. verification.flash.unknown_error: Could not process sign up of %username%. Please contact administrator. -verification.mail.success.subject: %server% jabber account information +verification.mail.success.subject: %server%: jabber account information verification.mail.success.body: | Hello %username%, you've verified your email address successfully and your jabber account on %server% has been created. - Your password is "%password%". Keep this mail safe! + Your password is "%password%". + If you wish to delete your account, use %deleteCode% on the website. + Keep this mail safe! +verification.mail.success.notify.subject: %server%: user verified their account +verification.mail.success.notify.body: A user (%email%) verified their account %username%@%server% successfully. + +# Delete +delete.title: Delete Account +delete.form.username: Username: +delete.form.username.placeholder: username +delete.form.delete_code: Deletion Code: +delete.form.delete_code.placeholder: received via mail on sign up, 32 in length +delete.form.button: Delete +delete.flash.combination_not_found: Could not find a combination match for username and delete code. +delete.flash.success: Successfully deleted your account %username%@%server%. +delete.flash.unknown_error: Could not process deletion of %username%. Please contact administrator. # Cleanup -cleanup.mail.subject: %server% jabber account verification expired +cleanup.mail.subject: %server%: jabber account verification expired cleanup.mail.body: | Hello %username%, you've recently signed up for a jabber account on %server% but you did not verify your account within 7 days. @@ -42,6 +63,8 @@ log.verification.invalid: Tried to use code %verificationCode% but it is invalid log.verification.sucess: %username% verified. log.verification.unknown_error: Unknown error in XMPP Rest API. log.verification.cleanup: %username% did not verify. Deleted. +log.delete.success: %username% deleted their account. +log.delete.unknown_error: Unknown error in XMPP Rest API. # Error error.401.title: 401 diff --git a/data/migrations/20160710194830_UsersRegisteredTable.php b/data/migrations/20160710194830_UsersRegisteredTable.php new file mode 100644 index 0000000..fb6aee5 --- /dev/null +++ b/data/migrations/20160710194830_UsersRegisteredTable.php @@ -0,0 +1,35 @@ +db->create($this->tableName, function($table) { + $table->string('username')->unique()->primary(); + $table->string('delete_code', 64); + }); + } + + /** + * Undo the migration + */ + public function down() + { + $this->db->dropIfExists($this->tableName); + } + + /** + * Init the migration + */ + public function init() + { + $this->db = $this->container['schema']; + } +} \ No newline at end of file diff --git a/src/Control/Actions/DeleteAction.php b/src/Control/Actions/DeleteAction.php new file mode 100644 index 0000000..02a5d7e --- /dev/null +++ b/src/Control/Actions/DeleteAction.php @@ -0,0 +1,85 @@ +view = $view; + $this->translator = $translator; + $this->logger = $logger; + $this->flash = $flash; + } + + public function __invoke(Request $request, Response $response, $args) + { + $body = $request->getParsedBody(); + + if ($request->isPost()) { + + // Form validation + $validator = new Validator(); + $validator->filter_rules([ + 'username' => 'trim|sanitize_string', + 'delete_code' => 'trim|sanitize_string', + ]); + $validator->validation_rules([ + 'username' => 'required|alpha_numeric|max_len,64|min_len,3', + 'delete_code' => 'required|exact_len,64', + ]); + if (!$validator->run($body)) { + $validator->addErrorsToFlashMessage($this->flash); + return $response->withRedirect('/delete'); + } + + $username = $body['username']; + $deleteCode = $body['delete_code']; + + // check if combination matches + $usersRegistered = UserRegistered::with([])->where('username', $username)->where('delete_code', $deleteCode)->get(); + + if (empty($usersRegistered) || $usersRegistered->count() == 0) { + $this->flash->addMessage('error', $this->translator->trans('delete.flash.combination_not_found')); + return $response->withRedirect('/delete'); + } else { + $userRegistered = $usersRegistered->pop(); + + $curl = new Curl(); + $curl->setBasicAuthentication(getenv('xmpp_curl_auth_admin_username'), getenv('xmpp_curl_auth_admin_password')); + $curl->delete(getenv('xmpp_curl_uri') . '/user/' . $username); + $curl->close(); + + if ($curl->http_status_code == 200) { + $userRegistered->delete(); + + $this->flash->addMessage('success', $this->translator->trans('delete.flash.success', ['%username%' => $username, '%server%' => getenv('site_xmpp_server_displayname')])); + $this->logger->info($this->translator->trans('log.delete.success', ['%username%' => $username])); + return $response->withRedirect('/'); + } else { + $this->flash->addMessage('error', $this->translator->trans('delete.flash.unknown_error', ['%username%' => $username])); + $this->logger->warning($this->translator->trans('log.delete.flash.unknown_error'), ['code' => $curl->http_status_code, 'message' => $curl->http_error_message]); + return $response->withRedirect('/delete'); + } + } + } + + // render GET + $this->view->render($response, 'delete.twig', [ + 'title' => $this->translator->trans('delete.title'), + ]); + + return $response; + } +} \ No newline at end of file diff --git a/src/Control/Actions/IndexAction.php b/src/Control/Actions/HomeAction.php similarity index 68% rename from src/Control/Actions/IndexAction.php rename to src/Control/Actions/HomeAction.php index 54428c7..15eeb66 100644 --- a/src/Control/Actions/IndexAction.php +++ b/src/Control/Actions/HomeAction.php @@ -7,7 +7,7 @@ use Slim\Http\Request; use Slim\Http\Response; use Symfony\Component\Translation\Translator; -final class IndexAction +final class HomeAction { private $view; private $translator; @@ -24,6 +24,9 @@ final class IndexAction public function __invoke(Request $request, Response $response, $args) { - return $response->withRedirect('/signup'); + return $this->view->render($response, 'home.twig', [ + 'title' => $this->translator->trans('home.title'), + 'content' => $this->translator->trans('home.text', ['%server%' => getenv('site_xmpp_server_displayname')]) + ]); } } \ No newline at end of file diff --git a/src/Control/Actions/SignUpAction.php b/src/Control/Actions/SignUpAction.php index c6ac3c0..decf79f 100644 --- a/src/Control/Actions/SignUpAction.php +++ b/src/Control/Actions/SignUpAction.php @@ -2,6 +2,7 @@ use Curl\Curl; use Slim\Flash\Messages; +use Slim\Interfaces\RouterInterface; use Slim\Views\Twig; use Psr\Log\LoggerInterface; use Slim\Http\Request; @@ -14,13 +15,15 @@ final class SignUpAction private $translator; private $logger; private $flash; + private $router; - public function __construct(Twig $view, LoggerInterface $logger, Messages $flash, Translator $translator) + public function __construct(Twig $view, LoggerInterface $logger, Messages $flash, Translator $translator, RouterInterface $router) { $this->view = $view; $this->translator = $translator; $this->logger = $logger; $this->flash = $flash; + $this->router = $router; } public function __invoke(Request $request, Response $response, $args) @@ -102,7 +105,11 @@ final class SignUpAction $mailer->addAddress($userAwaiting->email); - $verificationLink = $request->getUri()->getScheme() . '://' . $request->getUri()->getHost() . (!empty($p = $request->getUri()->getPort()) ? ':' .$p : '') .'/verification/' . $userAwaiting->verification_code; + $verificationLink = $request->getUri()->getScheme(); + $verificationLink .= '://'; + $verificationLink .= $request->getUri()->getHost(); + $verificationLink .= (!empty($p = $request->getUri()->getPort()) ? ':' .$p : ''); + $verificationLink .= $this->router->pathFor('verification', ['verificationCode' => $userAwaiting->verification_code]); $mailer->Subject = $this->translator->trans('verification.mail.subject', ['%server%' => getenv('site_xmpp_server_displayname')]); $mailer->Body = $this->translator->trans('verification.mail.body', ['%username%' => $userAwaiting->username, '%verificationLink%' => $verificationLink, '%server%' => getenv('site_xmpp_server_displayname')]); diff --git a/src/Control/Actions/VerificationAction.php b/src/Control/Actions/VerificationAction.php index 365fd05..5bbf7a2 100644 --- a/src/Control/Actions/VerificationAction.php +++ b/src/Control/Actions/VerificationAction.php @@ -52,32 +52,53 @@ final class VerificationAction $this->flash->addMessage('success', $this->translator->trans('verification.flash.success', ['%username%' => $userAwaiting->username])); $this->logger->info($this->translator->trans('log.verification.sucess', ['%username%' => $userAwaiting->username])); + if (getenv('mail_notify') == true) { + $mailer = new PHPMailer(); + $mailer->CharSet = 'UTF-8'; + $mailer->ContentType = 'text/plain'; + $mailer->isSMTP(); + $mailer->SMTPSecure = getenv('mail_secure'); + $mailer->SMTPAuth = getenv('mail_auth'); + $mailer->Host = getenv('mail_host'); + $mailer->Port = getenv('mail_port'); + $mailer->Username = getenv('mail_username'); + $mailer->Password = getenv('mail_password'); + $mailer->From = getenv('mail_from'); + $mailer->FromName = getenv('mail_from_name'); + $mailer->addAddress(getenv('mail_notify_to')); + $mailer->Subject = $this->translator->trans('verification.mail.success.notify.subject', ['%server%' => getenv('site_xmpp_server_displayname')]); + $mailer->Body = $this->translator->trans('verification.mail.success.notify.body', ['%username%' => $userAwaiting->username, '%server%' => getenv('site_xmpp_server_displayname'), '%email%' => $userAwaiting->email]); + $mailer->send(); + } + + $userRegistered = new UserRegistered(); + $userRegistered->username = $userAwaiting->username; + $userRegistered->delete_code = hash('sha256', (time() . $userAwaiting->username . rand())); + $userRegistered->save(); + $mailer = new PHPMailer(); $mailer->CharSet = 'UTF-8'; $mailer->ContentType = 'text/plain'; $mailer->isSMTP(); $mailer->SMTPSecure = getenv('mail_secure'); $mailer->SMTPAuth = getenv('mail_auth'); - $mailer->Host = getenv('mail_host'); $mailer->Port = getenv('mail_port'); $mailer->Username = getenv('mail_username'); $mailer->Password = getenv('mail_password'); $mailer->From = getenv('mail_from'); $mailer->FromName = getenv('mail_from_name'); - $mailer->addAddress($userAwaiting->email); - $mailer->Subject = $this->translator->trans('verification.mail.success.subject', ['%server%' => getenv('site_xmpp_server_displayname')]); - $mailer->Body = $this->translator->trans('verification.mail.success.body', ['%username%' => $userAwaiting->username, '%server%' => getenv('site_xmpp_server_displayname'), '%password%' => $userAwaiting->password]); + $mailer->Body = $this->translator->trans('verification.mail.success.body', ['%username%' => $userAwaiting->username, '%server%' => getenv('site_xmpp_server_displayname'), '%password%' => $userAwaiting->password, '%deleteCode%' => $userRegistered->delete_code]); $mailer->send(); $userAwaiting->delete(); - return $response->withRedirect('/signup'); + return $response->withRedirect('/'); } else { $this->flash->addMessage('error', $this->translator->trans('verification.flash.unknown_error', ['%username%' => $userAwaiting->username])); $this->logger->warning($this->translator->trans('verification.flash.unknown_error'), ['code' => $curl->http_status_code, 'message' => $curl->http_error_message]); - return $response->withRedirect('/signup'); + return $response->withRedirect('/'); } } } \ No newline at end of file diff --git a/src/Model/UserRegistered.php b/src/Model/UserRegistered.php new file mode 100644 index 0000000..e724e55 --- /dev/null +++ b/src/Model/UserRegistered.php @@ -0,0 +1,9 @@ +translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_required': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_valid_json_string': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_valid_email': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_max_len': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_min_len': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_exact_len': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_alpha': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_alpha_numeric': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_alpha_dash': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_numeric': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_integer': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_boolean': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_float': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_valid_url': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_url_exists': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_valid_ip': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_valid_cc': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_valid_name': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_contains': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => implode(', ', $param)]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => implode(', ', $param)]); break; case 'validate_street_address': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_date': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_min_numeric': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_max_numeric': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_equals': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; case 'validate_set_min_len': - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); break; default: - $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]); + $resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param%' => $param]); } } diff --git a/src/View/base.twig b/src/View/base.twig index dbb67e7..ce78fb3 100644 --- a/src/View/base.twig +++ b/src/View/base.twig @@ -23,9 +23,11 @@