diff --git a/data/security/securitylog.txt b/data/security/securitylog.txt index 8b02239..e027179 100644 --- a/data/security/securitylog.txt +++ b/data/security/securitylog.txt @@ -1,3 +1,4 @@ 127.0.0.1;2024-03-25 21:48:49;login: wrong password 127.0.0.1;2024-04-20 12:51:39;login: wrong password 127.0.0.1;2024-04-21 19:24:11;login: invalid data +127.0.0.1;2024-04-22 14:38:20;loginlink: loginlink for user member is not activated. diff --git a/system/typemill/Controllers/ControllerApiSystemUsers.php b/system/typemill/Controllers/ControllerApiSystemUsers.php index e9d74b0..ef15f70 100644 --- a/system/typemill/Controllers/ControllerApiSystemUsers.php +++ b/system/typemill/Controllers/ControllerApiSystemUsers.php @@ -221,8 +221,15 @@ class ControllerApiSystemUsers extends Controller return $response->withHeader('Content-Type', 'application/json')->withStatus(400); } + # check if loginlink is activated + $loginlink = false; + if($userdata['userrole'] == 'member' && isset($this->settings['loginlink']) && $this->settings['loginlink']) + { + $loginlink = true; + } + # we have to validate again because of additional dynamic fields - $formdefinitions = $user->getUserFields($this->c->get('acl'), $request->getAttribute('c_userrole')); + $formdefinitions = $user->getUserFields($this->c->get('acl'), $request->getAttribute('c_userrole'), NULL, $loginlink); $validatedOutput = $validate->recursiveValidation($formdefinitions, $userdata); if(!empty($validate->errors)) { diff --git a/system/typemill/Controllers/ControllerWebAuth.php b/system/typemill/Controllers/ControllerWebAuth.php index 4707a1e..5f2ff28 100644 --- a/system/typemill/Controllers/ControllerWebAuth.php +++ b/system/typemill/Controllers/ControllerWebAuth.php @@ -241,6 +241,136 @@ class ControllerWebAuth extends Controller return $response->withHeader('Location', $this->routeParser->urlFor($redirect))->withStatus(302); } + public function loginlink(Request $request, Response $response, $args) + { + if(!isset($this->settings['loginlink']) OR !$this->settings['loginlink']) + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('loginlink: not activated'); + } + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + # optionally check trusted ips + $trustedLogin = ( isset($this->settings['trustedloginreferrer']) && !empty($this->settings['trustedloginreferrer']) ) ? explode(",", $this->settings['trustedloginreferrer']) : []; + $ipAddress = $_SERVER['REMOTE_ADDR'] ?? null; + if ( + !empty($trustedLogin) + && !in_array($ipAddress, $trustedLogin) + ) + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('loginlink: remote address is not a trusted ip'); + } + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + $input = $request->getQueryParams(); + $validation = new Validation(); + $securitylog = $this->settings['securitylog'] ?? false; + + if($validation->signin($input) !== true) + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('loginlink: invalid data'); + } + + if($this->c->get('flash')) + { + $this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.')); + } + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + $user = new User(); + + if(!$user->setUserWithPassword($input['username'])) + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('loginlink: user not found'); + } + + if($this->c->get('flash')) + { + $this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.')); + } + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + $userdata = $user->getUserData(); + + if($userdata['userrole'] != 'member') + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('loginlink: user has not a member role. Only members can use loginlinks.'); + } + + if($this->c->get('flash')) + { + $this->c->get('flash')->addMessage('error', Translations::translate('User is not a member.')); + } + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + if(!isset($userdata['linkaccess']) OR ($userdata['linkaccess'] !== true)) + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('loginlink: loginlink for user ' . $userdata['username'] . ' is not activated.'); + } + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + if($userdata && !password_verify($input['password'], $userdata['password'])) + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('login: wrong password'); + } + + if($this->c->get('flash')) + { + $this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.')); + } + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + # check if user has confirmed the account + if(isset($userdata['optintoken']) && $userdata['optintoken']) + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('login: user not confirmed yet.'); + } + + if($this->c->get('flash')) + { + $this->c->get('flash')->addMessage('error', Translations::translate('Your registration is not confirmed yet. Please check your e-mails and use the confirmation link.')); + } + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + $user->login(); + + $redirect = $this->getRedirectDestination($userdata['userrole']); + + return $response->withHeader('Location', $this->routeParser->urlFor($redirect))->withStatus(302); + } + + private function getRedirectDestination(string $userrole) { # decide where to redirect after login, configurable in settings -> system.yaml diff --git a/system/typemill/Controllers/ControllerWebSystem.php b/system/typemill/Controllers/ControllerWebSystem.php index 97ca36d..9db44fa 100644 --- a/system/typemill/Controllers/ControllerWebSystem.php +++ b/system/typemill/Controllers/ControllerWebSystem.php @@ -361,7 +361,12 @@ class ControllerWebSystem extends Controller $userdata = $user->getUserData(); $inspector = $request->getAttribute('c_userrole'); - $userfields = $user->getUserFields($this->c->get('acl'), $userdata['userrole'], $inspector); + $loginlink = false; + if($userdata['userrole'] == 'member' && isset($this->settings['loginlink']) && $this->settings['loginlink']) + { + $loginlink = true; + } + $userfields = $user->getUserFields($this->c->get('acl'), $userdata['userrole'], $inspector, $loginlink); return $this->c->get('view')->render($response, 'system/user.twig', [ 'settings' => $this->settings, diff --git a/system/typemill/Models/User.php b/system/typemill/Models/User.php index 4a47570..d2bad5e 100644 --- a/system/typemill/Models/User.php +++ b/system/typemill/Models/User.php @@ -169,7 +169,7 @@ class User return false; } - public function getUserFields($acl, $userrole, $inspectorrole = NULL) + public function getUserFields($acl, $userrole, $inspectorrole = NULL, $loginlink = NULL) { $storage = new StorageWrapper('\Typemill\Models\Storage'); $userfields = $storage->getYaml('systemSettings', '', 'user.yaml'); @@ -217,7 +217,12 @@ class User $userfields['userrole'] = ['label' => Translations::translate('Role'), 'type' => 'select', 'options' => $options]; # can activate api access - $userfields['apiaccess'] = ['label' => Translations::translate('API access'), 'checkboxlabel' => Translations::translate('Activate API access for this user. Use username and password for api calls'), 'type' => 'checkbox']; + $userfields['apiaccess'] = ['label' => Translations::translate('API access'), 'checkboxlabel' => Translations::translate('Activate API access for this user. Use username and password for api calls. Whitelist calling domains in the developer settings.'), 'type' => 'checkbox']; + + if($loginlink) + { + $userfields['linkaccess'] = ['label' => Translations::translate('Link access'), 'checkboxlabel' => Translations::translate('Activate link access for this user (only for member role). Use username and password for the link. Optionally whitelist IPs in the developer settings.'), 'type' => 'checkbox']; + } } return $userfields; diff --git a/system/typemill/routes/web.php b/system/typemill/routes/web.php index ecf18a4..36ff27e 100644 --- a/system/typemill/routes/web.php +++ b/system/typemill/routes/web.php @@ -19,6 +19,11 @@ $app->group('/tm', function (RouteCollectorProxy $group) use ($settings) { $group->get('/login', ControllerWebAuth::class . ':show')->setName('auth.show'); $group->post('/login', ControllerWebAuth::class . ':login')->setName('auth.login'); + if(isset($settings['loginlink']) && $settings['loginlink']) + { + $group->get('/loginlink', ControllerWebAuth::class . ':loginlink')->setName('auth.link'); + } + if(isset($settings['authcode']) && $settings['authcode']) { $group->post('/authcode', ControllerWebAuth::class . ':loginWithAuthcode')->setName('auth.authcode'); diff --git a/system/typemill/settings/system.yaml b/system/typemill/settings/system.yaml index abd824e..46971bb 100644 --- a/system/typemill/settings/system.yaml +++ b/system/typemill/settings/system.yaml @@ -289,4 +289,11 @@ fieldsetdeveloper: label: "Allowed Domains for API-Access (CORS-Headers)" placeholder: 'https://my-website-that-uses-the-api.org,https://another-website-using-the-api.org' description: "List all domains, separated by comma, that should have access to the Typemill API. Domains will be added to the cors-header." - \ No newline at end of file + loginlink: + type: checkbox + label: "Login with link" + checkboxlabel: "Allow selected users to login with a login link." + description: "If activated, you can allow login-links with a checkbox in the user profile. This is only available for member-roles since members have very limited rights. Login with a link can be helpful if you link from your software to a non-public documentation. Be aware of the low protection that this kind of logins have. If you integrate such links in a SaaS-software, then you should restrict access to your ips." + trustedloginreferrer: + type: text + label: "Trusted IPs for the login-link-referrer (comma separated)" \ No newline at end of file diff --git a/system/typemill/settings/user.yaml b/system/typemill/settings/user.yaml index b0ade9e..de4b4f4 100644 --- a/system/typemill/settings/user.yaml +++ b/system/typemill/settings/user.yaml @@ -21,6 +21,11 @@ userrole: label: 'Role' type: 'text' readonly: true +darkmode: + name: darkmode + label: 'Darkmode' + checkboxlabel: 'Activate the darkmode for me' + type: 'checkbox' password: name: password label: 'Actual Password' @@ -31,9 +36,4 @@ newpassword: label: 'New Password' type: 'password' autocomplete: 'new-password' - generator: true -darkmode: - name: darkmode - label: 'Darkmode' - checkboxlabel: 'Activate the darkmode for me' - type: 'checkbox' \ No newline at end of file + generator: true \ No newline at end of file