From 90bb4e31e3a5f1e54aa6fe692a79e80bad8811f6 Mon Sep 17 00:00:00 2001 From: Aselsan Date: Tue, 9 Jul 2024 05:15:28 +0700 Subject: [PATCH 1/5] add example login controller --- examples/Controllers/LoginController.php | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 examples/Controllers/LoginController.php diff --git a/examples/Controllers/LoginController.php b/examples/Controllers/LoginController.php new file mode 100644 index 000000000..dc78f5582 --- /dev/null +++ b/examples/Controllers/LoginController.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Shield\Controllers; + +use App\Controllers\BaseController; +use CodeIgniter\HTTP\RedirectResponse; + +class LoginController extends BaseController +{ + /** + * Displays the form the login to the site. + * + * @return RedirectResponse|string + */ + public function loginView() + { + if (auth()->loggedIn()) { + return redirect()->to(config('Auth')->loginRedirect()); + } + + /** @var Session $authenticator */ + $authenticator = auth('session')->getAuthenticator(); + + // If an action has been defined, start it up. + if ($authenticator->hasAction()) { + return redirect()->route('auth-action-show'); + } + + return $this->view(setting('Auth.views')['login']); + } +} From fc80769f4fb593089da76ec8f52f1b2f4c395fbf Mon Sep 17 00:00:00 2001 From: Aselsan Date: Tue, 9 Jul 2024 05:16:55 +0700 Subject: [PATCH 2/5] add example magic link controller --- examples/Controllers/MagicLinkController.php | 104 +++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 examples/Controllers/MagicLinkController.php diff --git a/examples/Controllers/MagicLinkController.php b/examples/Controllers/MagicLinkController.php new file mode 100644 index 000000000..2a72e1fcc --- /dev/null +++ b/examples/Controllers/MagicLinkController.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Shield\Controllers; + +use App\Controllers\BaseController; +use CodeIgniter\HTTP\RedirectResponse; + +/** + * Handles "Magic Link" logins - an email-based + * no-password login protocol. This works much + * like password reset would, but Shield provides + * this in place of password reset. It can also + * be used on it's own without an email/password + * login strategy. + */ +class MagicLinkController extends BaseController +{ + + /** + * Receives the email from the user, creates the hash + * to a user identity, and sends an email to the given + * email address. + * + * @return RedirectResponse|string + */ + public function loginAction() + { + if (! setting('Auth.allowMagicLinkLogins')) { + return redirect()->route('login')->with('error', lang('Auth.magicLinkDisabled')); + } + + // Validate email format + $rules = $this->getValidationRules(); + if (! $this->validateData($this->request->getPost(), $rules, [], config('Auth')->DBGroup)) { + return redirect()->route('magic-link')->with('errors', $this->validator->getErrors()); + } + + // Check if the user exists + $email = $this->request->getPost('email'); + $user = $this->provider->findByCredentials(['email' => $email]); + + if ($user === null) { + return redirect()->route('magic-link')->with('error', lang('Auth.invalidEmail')); + } + + /** @var UserIdentityModel $identityModel */ + $identityModel = model(UserIdentityModel::class); + + // Delete any previous magic-link identities + $identityModel->deleteIdentitiesByType($user, Session::ID_TYPE_MAGIC_LINK); + + // Generate the code and save it as an identity + helper('text'); + $token = random_string('crypto', 20); + + $identityModel->insert([ + 'user_id' => $user->id, + 'type' => Session::ID_TYPE_MAGIC_LINK, + 'secret' => $token, + 'expires' => Time::now()->addSeconds(setting('Auth.magicLinkLifetime')), + ]); + + /** @var IncomingRequest $request */ + $request = service('request'); + + $ipAddress = $request->getIPAddress(); + $userAgent = (string) $request->getUserAgent(); + $date = Time::now()->toDateTimeString(); + + // Send the user an email with the code + helper('email'); + $email = emailer(['mailType' => 'html']) + ->setFrom(setting('Email.fromEmail'), setting('Email.fromName') ?? ''); + $email->setTo($user->email); + $email->setSubject(lang('Auth.magicLinkSubject')); + $email->setMessage($this->view( + setting('Auth.views')['magic-link-email'], + ['token' => $token, 'ipAddress' => $ipAddress, 'userAgent' => $userAgent, 'date' => $date], + ['debug' => false] + )); + + if ($email->send(false) === false) { + log_message('error', $email->printDebugger(['headers'])); + + return redirect()->route('magic-link')->with('error', lang('Auth.unableSendEmailToUser', [$user->email])); + } + + // Clear the email + $email->clear(); + + return $this->displayMessage(); + } +} From a701ff1f20acaea260efe1fc657d20cb588c3ef2 Mon Sep 17 00:00:00 2001 From: Aselsan Date: Tue, 9 Jul 2024 05:17:44 +0700 Subject: [PATCH 3/5] add example register controller --- examples/Controllers/RegisterController.php | 125 ++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 examples/Controllers/RegisterController.php diff --git a/examples/Controllers/RegisterController.php b/examples/Controllers/RegisterController.php new file mode 100644 index 000000000..e7137fa24 --- /dev/null +++ b/examples/Controllers/RegisterController.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Shield\Controllers; + +use App\Controllers\BaseController; +use CodeIgniter\HTTP\RedirectResponse; + + +/** + * Class RegisterController + * + * Handles displaying registration form, + * and handling actual registration flow. + */ +class RegisterController extends BaseController +{ + /** + * Displays the registration form. + * + * @return RedirectResponse|string + */ + public function registerView() + { + if (auth()->loggedIn()) { + return redirect()->to(config('Auth')->registerRedirect()); + } + + // Check if registration is allowed + if (! setting('Auth.allowRegistration')) { + return redirect()->back()->withInput() + ->with('error', lang('Auth.registerDisabled')); + } + + /** @var Session $authenticator */ + $authenticator = auth('session')->getAuthenticator(); + + // If an action has been defined, start it up. + if ($authenticator->hasAction()) { + return redirect()->route('auth-action-show'); + } + + return $this->view(setting('Auth.views')['register']); + } + + /** + * Attempts to register the user. + */ + public function registerAction(): RedirectResponse + { + if (auth()->loggedIn()) { + return redirect()->to(config('Auth')->registerRedirect()); + } + + // Check if registration is allowed + if (! setting('Auth.allowRegistration')) { + return redirect()->back()->withInput() + ->with('error', lang('Auth.registerDisabled')); + } + + $users = $this->getUserProvider(); + + // Validate here first, since some things, + // like the password, can only be validated properly here. + $rules = $this->getValidationRules(); + + if (! $this->validateData($this->request->getPost(), $rules, [], config('Auth')->DBGroup)) { + return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); + } + + // Save the user + $allowedPostFields = array_keys($rules); + $user = $this->getUserEntity(); + $user->fill($this->request->getPost($allowedPostFields)); + + // Workaround for email only registration/login + if ($user->username === null) { + $user->username = null; + } + + try { + $users->save($user); + } catch (ValidationException $e) { + return redirect()->back()->withInput()->with('errors', $users->errors()); + } + + // To get the complete user object with ID, we need to get from the database + $user = $users->findById($users->getInsertID()); + + // Add to default group + $users->addToDefaultGroup($user); + + Events::trigger('register', $user); + + /** @var Session $authenticator */ + $authenticator = auth('session')->getAuthenticator(); + + $authenticator->startLogin($user); + + // If an action has been defined for register, start it up. + $hasAction = $authenticator->startUpAction('register', $user); + if ($hasAction) { + return redirect()->route('auth-action-show'); + } + + // Set the user active + $user->activate(); + + $authenticator->completeLogin($user); + + // Success! + return redirect()->to(config('Auth')->registerRedirect()) + ->with('message', lang('Auth.registerSuccess')); + } +} From 7ed58646b61f3c89f8165526c17d4df4cfc6f2bc Mon Sep 17 00:00:00 2001 From: Aselsan Date: Tue, 9 Jul 2024 05:22:00 +0700 Subject: [PATCH 4/5] add shield:extend command --- src/Commands/Extend.php | 198 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 src/Commands/Extend.php diff --git a/src/Commands/Extend.php b/src/Commands/Extend.php new file mode 100644 index 000000000..fb048f01a --- /dev/null +++ b/src/Commands/Extend.php @@ -0,0 +1,198 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Shield\Commands; +use CodeIgniter\CLI\CLI; +use CodeIgniter\Shield\Commands\Setup\ContentReplacer; + +class Extend extends BaseCommand +{ + /** + * The Command's Name + * + * @var string + */ + protected $name = 'shield:extend'; + + /** + * The Command's Description + * + * @var string + */ + protected $description = 'Extending the Controllers.'; + + /** + * The Command's Usage + * + * @var string + */ + protected $usage = 'shield:extend'; + + /** + * The Command's Arguments + * + * @var array + */ + protected $arguments = []; + + /** + * The Command's Options + * + * @var array + */ + protected $options = [ + '-i' => 'The index of shield controllers to be extending in your app.', + '-f' => 'Force overwrite ALL existing files in destination.', + ]; + + protected $sourcePath; + + protected $distPath = APPPATH; + private ContentReplacer $replacer; + + private const INFO_MESSAGE = " After extending, don't forget to change the route. See https://shield.codeigniter.com/customization/route_config"; + + /** + * Actually execute a command. + * + * @param array $params + */ + public function run(array $params) + { + $this->replacer = new ContentReplacer(); + $this->sourcePath = __DIR__ . '/../../examples/'; + // Get option -i value + $index = CLI::getOption('i'); + + // if no option -i provided show this prompt to user + if($params === [] || array_key_exists('i', $params) === false || $params['i'] === null ) { + $this->write('List of the controller that will be extend:'); + $this->write(); + $this->write(' [1] LoginController'); + $this->write(' [2] MagicLinkController'); + $this->write(' [3] RegisterController'); + $this->write(); + $index = $this->prompt('Please select one of these (1/2/3)'); + } + + switch ((int) $index) { + case 1: + $this->extendingLoginController(); + break; + case 2: + $this->extendingMagicLinkController(); + break; + case 3: + $this->extendingRegisterController(); + break; + + default: + $this->write(); + CLI::error(" Extending canceled: your input not match with any index."); + $this->write(); + break; + } + + return 0; + } + + private function extendingLoginController() + { + $file = 'Controllers/LoginController.php'; + $replaces = [ + 'namespace CodeIgniter\Shield\Controllers' => 'namespace App\Controllers', + 'use App\\Controllers\\BaseController' => 'use CodeIgniter\\Shield\\Controllers\\LoginController as ShieldLoginController', + 'extends BaseController' => 'extends ShieldLoginController', + ]; + + $this->copyAndReplace($file, $replaces); + } + + private function extendingMagicLinkController() + { + $file = 'Controllers/MagicLinkController.php'; + $replaces = [ + 'namespace CodeIgniter\Shield\Controllers' => 'namespace App\Controllers', + 'use App\\Controllers\\BaseController' => 'use CodeIgniter\\Shield\\Controllers\\MagicLinkController as ShieldMagicLinkController', + 'extends BaseController' => 'extends ShieldMagicLinkController', + ]; + + $this->copyAndReplace($file, $replaces); + } + + private function extendingRegisterController() + { + $file = 'Controllers/RegisterController.php'; + $replaces = [ + 'namespace CodeIgniter\Shield\Controllers' => 'namespace App\Controllers', + 'use App\\Controllers\\BaseController' => 'use CodeIgniter\\Shield\\Controllers\\RegisterController as ShieldRegisterController', + 'extends BaseController' => 'extends ShieldRegisterController', + ]; + + $this->copyAndReplace($file, $replaces); + } + + /** + * @param string $file Relative file path like 'Config/Auth.php'. + * @param array $replaces [search => replace] + */ + protected function copyAndReplace(string $file, array $replaces): void + { + $path = "{$this->sourcePath}/{$file}"; + + $content = file_get_contents($path); + + $content = $this->replacer->replace($content, $replaces); + + $this->writeFile($file, $content); + } + + /** + * Write a file, catching any exceptions and showing a + * nicely formatted error. + * + * @param string $file Relative file path like 'Config/Auth.php'. + */ + protected function writeFile(string $file, string $content): void + { + $path = $this->distPath . $file; + $cleanPath = clean_path($path); + + $directory = dirname($path); + + if (! is_dir($directory)) { + mkdir($directory, 0777, true); + } + + if (file_exists($path)) { + $overwrite = (bool) CLI::getOption('f'); + + if ( + ! $overwrite + && $this->prompt(" File '{$cleanPath}' already exists in destination. Overwrite?", ['n', 'y']) === 'n' + ) { + $this->error(" Skipped {$cleanPath}. If you wish to overwrite, please use the '-f' option or reply 'y' to the prompt."); + + return; + } + } + + if (write_file($path, $content)) { + $this->write(); + $this->write(CLI::color(' Created: ', 'green') . $cleanPath); + $this->write(self::INFO_MESSAGE, 'light_green'); + $this->write(); + } else { + $this->error(" Error creating {$cleanPath}."); + } + } +} From eaa2bbbcb062212d8b664b2fc11f428fe53333c0 Mon Sep 17 00:00:00 2001 From: Aselsan Date: Tue, 9 Jul 2024 09:18:51 +0700 Subject: [PATCH 5/5] update: add missing Events class --- examples/Controllers/RegisterController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/Controllers/RegisterController.php b/examples/Controllers/RegisterController.php index e7137fa24..64dacaba5 100644 --- a/examples/Controllers/RegisterController.php +++ b/examples/Controllers/RegisterController.php @@ -15,6 +15,7 @@ use App\Controllers\BaseController; use CodeIgniter\HTTP\RedirectResponse; +use CodeIgniter\Events\Events; /**