Search code examples
codeigniter-4

Codeigniter 4 - Shield: What is the workflow for creating new users?


I have been playing around with CI4 and Shield for authentication and authorisation. There is very little information out there on the work flow for registering new users and how to notify the user that they have been registered into the app. Any help would appreciated?

Here is my controller function so far:

type// Add a member
    public function add_member()
    {
        $data = [];
        //$users = model('UserModel');

        if ($this->request->getMethod() == 'post') {
            $rules = [
                //'membership_status' =>
                'firstname' => 'required|min_length[3]|max_length[50]',
                'lastname' => 'required|min_length[3]|max_length[50]',
                'prefered_name' => 'min_length[3]|max_length[50]',
                'gender' => 'required|min_length[3]|max_length[25]',
                'date_of_birth' => 'required|valid_date',
                'address_1' => 'required|min_length[3]|max_length[100]',
                'address_2' => 'required|min_length[3]|max_length[100]',
                'postal_code' => 'required|min_length[3]|max_length[10]|integer',
                'city' => 'required|min_length[3]|max_length[50]',
                'home_phone' => 'min_length[3]|max_length[50]',
                'work_phone' => 'min_length[3]|max_length[50]',
                'mobile' => 'required_without[home_phone,work_phone]|min_length[3]|max_length[50]',
                'consent' => 'min_length[2]|max_length[50]',
                //'email' => 'required|min_length[6]|max_length[50]|valid_email',
            ];

            if (!$this->validate($rules)) {
                $data['validation'] = $this->validator;
                
                $this->session->set('Details', $_POST);
                //dd($_SESSION);
            } else {
                $newData = [
                    //'id' => $id,
                    'membership_status' => 'Active',
                    'firstname' => $this->request->getVar('firstname', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
                    'lastname' => $this->request->getVar('lastname', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
                    'prefered_name' => $this->request->getVar('prefered_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
                    'date_joined' => date('Y-m-d'),
                    'gender' => $this->request->getVar('gender', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
                    'date_of_birth' => $this->request->getVar('date_of_birth'),
                    'address_1' => $this->request->getVar('address_1', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
                    'address_2' => $this->request->getVar('address_2', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
                    'postal_code' => $this->request->getVar('postal_code', FILTER_SANITIZE_NUMBER_INT),
                    'city' => $this->request->getVar('city', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
                    'home_phone' => $this->request->getVar('home_phone', FILTER_SANITIZE_NUMBER_INT),
                    'work_phone' => $this->request->getVar('work_phone', FILTER_SANITIZE_NUMBER_INT),
                    'mobile' => $this->request->getVar('mobile', FILTER_SANITIZE_NUMBER_INT),
                    'consent' => $this->request->getVar('consent'),
                    //'email' => $this->request->getVar('email', FILTER_SANITIZE_EMAIL)
                ];

                $user = new User([
                    'username' => NULL,
                    'email'    => $this->request->getVar('email', FILTER_SANITIZE_EMAIL),
                    'password' => $this->request->getVar('email', FILTER_SANITIZE_EMAIL),
                ]);
                $this->users->save($user);
                $newData['id'] = $this->users->getInsertID();
                // To get the complete user object with ID, we need to get from the database
                $user = $this->users->findById($this->users->getInsertID());

                // Add to default group
                $this->users->addToDefaultGroup($user);

                $this->users->save($newData);
                
                $userEmail['email'] = $user->getEmail();

                // Send the user an email with the code
                $email = emailer()->setFrom(setting('Email.fromEmail'), setting('Email.fromName') ?? '');
                $email->setTo($user->email);
                $email->setSubject(lang('DojoApp.mailSubject'));
                $email->setMessage($this->view('email/add_new_user_email', $userEmail));

                if ($email->send(false) === false) {
                    log_message('error', $email->printDebugger(['headers']));

                    return redirect()->route('login')->with('error', lang('Auth.unableSendEmailToUser', [$user->email]));
                }

                // Clear the email
                $email->clear();

                $this->session->setFlashdata('success', 'Successfuly added new member information');
                return redirect()->to('/admin/members');
            }
        }
        echo view('templates/header');
        echo view('admin/left_nav_bar');
        echo view('admin/add_member', $data);
        echo view('admin/left_nav_bar_closure');
        echo view('admin/javascript');
        echo view('templates/footer');
    } here

No information on the topic internet wide.


Solution

  • Looks like you might not be looking in the right place for this.


    FIRST: Make sure you have completed the setup - Composer will be your best option


    1. Installation Link
    2. Setup Link

    Using this LINK you can see the flow for adding users. Looks like you are missing the 2 lines below: Here is the snippet from that link you may have overlooked during your search:

    use CodeIgniter\Shield\Entities\User;
    use CodeIgniter\Events\Events;
    
    // Get the User Provider (UserModel by default)
    $users = auth()->getProvider();
    
    $user = new User([
        'username' => 'foo-bar',
        'email'    => '[email protected]',
        'password' => 'secret plain text password',
    ]);
    $users->save($user);
    
    // 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);
    

    Once you have added the user using the above method or the default registration form provided by Shield, an email will get sent to the new user. Below is the section on the same site describing the actions needed:

    Note You need to configure app/Config/Email.php to allow Shield to send emails. See Installation.

    By default, once a user registers they have an active account that can be used. You can enable Shield's built-in, email-based activation flow within the Auth config file.

    public array $actions = [
        'register' => \CodeIgniter\Shield\Authentication\Actions\EmailActivator::class,
        'login'    => null,
    ];
    

    Additional information on Authentication Actions can be found here. This will allow you to configure more if needed and defines the two provided by Shield.

    Ok, Let's get to work!

    app/Config/Events.php We need to add an event to send the new user an email

    ....
    use CodeIgniter\I18n\Time;
    use CodeIgniter\Shield\Exceptions\LogicException;
    use CodeIgniter\Shield\Exceptions\RuntimeException;
    
    // notice I am passing two variables from the controller $user and $tmpPass
    // I will force the user to change password on first login
    Events::on('newRegistration', static function ($user, $tmpPass) {
    
        $userEmail = $user->email;
        if ($userEmail === null) {
            throw new LogicException(
                'Email Activation needs user email address. user_id: ' . $user->id
            );
        }
    
        $date      = Time::now()->toDateTimeString();
    
        // Send the email
        $email = emailer()->setFrom(setting('Email.fromEmail'), setting('Email.fromName') ?? '');
        $email->setTo($userEmail);
        $email->setSubject(lang('Auth.emailActivateSubject'));
        $email->setMessage(view(setting('Auth.views')['email_manual_activate_email'], ['userEmail' => $userEmail,'tmpPass' => $tmpPass, 'date' => $date]));
    
        if ($email->send(false) === false) {
            throw new RuntimeException('Cannot send email for user: ' . $user->email . "\n" . $email->printDebugger(['headers']));
        }
    
        // Clear the email
        $email->clear();
    
    });
    

    app/Controllers/Users.php

    use \CodeIgniter\Events\Events;
    use \CodeIgniter\Config\Factories;
    
    // modify however you want. I use Ajax...
        public function add() {
    
            checkAjax();
    
            if (!$this->user->hasPermission('users.create')) {
                $response['success']        = false;
                $response['messages']       = lang("App.invalid_permission");
                return $this->response->setJSON($response);
            }
    
            $response = array();
    
            $fields['username']         = $this->request->getPost('username');
            $fields['password']         = $this->request->getPost('password');
            $fields['email']            = $this->request->getPost('email');
            $fields['group']            = $this->request->getPost('group');
    
            $this->validation->setRules([
                'username'              => ['label' => 'Username', 'rules'          => 'required|max_length[30]|min_length[3]|regex_match[/\A[a-zA-Z0-9\.]+\z/]|is_unique[users.username,id,{id}]'],
                'password'              => ['label' => 'Password', 'rules'          => 'required|min_length[8]'],
                'email'                 => ['label' => 'Email Address', 'rules'     => 'required|valid_email|is_unique[auth_identities.secret,id,{id}]'],
                'group'                 => ['label' => 'Group', 'rules'             => 'required'],
            ]);
    
            if ($this->validation->run($fields) == FALSE) {
    
                $response['success']    = false;
                $response['messages']   = $this->validation->getErrors(); //Show Error in Input Form
            } else {
    
                $users                          = auth()->getProvider();
                $user                           = new User([
                    'username'                  => $fields['username'],
                    'email'                     => $fields['email'],
                    'password'                  => $fields['password'],
                    'status'                    => 'OFF',
                    'status_message'            => 'New User',
                ]);
                // save the user with the above information
                $users->save($user);
                // get the new ID as we still have work to do
                $user                           = $users->findById($users->getInsertID());
                // set the flag to make user change password on first login
                $user->forcePasswordReset();
                // make sure this is the only group(s) for the user
                $user->syncGroups($fields['group']);
    
                // Additional work done here..
                $actionClass                    = setting('Auth.actions')['register'] ?? null;
                $action                         = Factories::actions($actionClass)->createIdentity($user);
                $code                           = $action; // do not need this yet though it is set
                $tmpPass                        = $fields['password'];
                // trigger our new Event and send the two variables 
                $confirm                        = Events::trigger('newRegistration', $user, $tmpPass);
    
                // if eveything went well, notifuy the Admin the user has been added and email sent
                if ($confirm) {
                    $response['success']        = true;
                    $response['messages']       = lang("App.insert-success");
                } else {
                    $response['success']        = false;
                    $response['messages']       = lang("App.insert-error");
                }
            }
            return $this->response->setJSON($response);
        }
    

    email template -> CodeIgniter\Shield\Views\Email\email_manual_activate_email

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    
    <head>
        <meta name="x-apple-disable-message-reformatting">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title><?= lang('Auth.emailActivateSubject') ?></title>
    </head>
    
    <body>
        <p><?= lang('App.emailActivateMail') ?></p>
        <div style="text-align: center">
            <h3><?= site_url(); ?></h3>
            <p>User ID: <?= $userEmail?></p>
            <p>Temporary Password: <?= $tmpPass; ?></p>
        </div>
        <table role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
            <tbody>
                <tr>
                    <td style="line-height: 20px; font-size: 20px; width: 100%; height: 20px; margin: 0;" align="left" width="100%" height="20">
                        &#160;
                    </td>
                </tr>
            </tbody>
        </table>
        <b><?= lang('Auth.emailInfo') ?></b>
        <p><?= lang('Auth.emailDate') ?> <?= esc($date) ?><br>
        Email System Generated for a New User.</p>
    </body>
    
    </html>
    

    Make sure you add this line in app/Config/Auth.php

     public array $views = [
    'email_manual_activate_email' => '\CodeIgniter\Shield\Views\Email\email_manual_activate_email',
        ];
    

    Now your new users will receive an email when you set them up, even if

    $allowRegistration = false;
    

    Once they login for the first time, it will also send them a new code. Verifying the code will send them to wherever you have the force_reset redirect set to.

    With all of the extra data you are entering into the tables, you can either expand the users table, or create a new table. More information on that here with code to assist.