The AppGini Blog
A few tips and tricks to make your coding life a tiny bit better.

Automatically create users from records

I was recently working on a rental property maintenance management system. The system allows tenants to submit maintenance requests, and the property manager can assign these requests to technicians for resolution. Each technician has a user account in the system, and the property manager is responsible for creating these accounts manually.

To simplify the process, I decided to automatically create a user account for each technician when a new record is added to the technicians table. This way, the property manager can simply add a new technician record, and the system will automatically create a user account for them.

This article explains how I achieved this using AppGini’s hooks., a powerful way to extend the functionality of your AppGini application.

Overview of the workflow

Here’s an overview of the workflow we’ll implement:

  1. To add a new technician, the property manager will add a new record to the technicians table.
    • This new record includes the technician’s name, email, desired username, and other details.
  2. After saving the new record, the system will automatically create a new user account for the technician.
    • The account would belong to the Technicians group and have a random password.
    • The technician will need to reset their password when they first log in.
  3. The username field in the technicians table will become read-only to prevent accidental changes to the username.

For reference, here is the technicians detail view form:

Technicians detail view form

Creating user accounts automatically

To make this tutorial as generic as possible, I’ve created a generic addUserToGroup() function that you can be used in any AppGini project to add a user to a group. This function should be added to hooks/__global.php or hooks/__bootstrap.php file. This makes it available to all hooks in your project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
 * Check if a user is a member of a group and create it if it doesn't exist.
 * 
 * @param string $username The username of the user to check.
 * @param string $email The email of the user to check.
 * @param string $group The name of the group to check.
 * @param string $error The error message to return if checks fail.
 * 
 * @return bool True if the user is a member of the group or was successfully created and added to the group, false otherwise.
 */
function addUserToGroup($username, $email, $group, &$error) {
	// get group ID
	$group = makeSafe($group);
	$groupID = sqlValue("SELECT `groupID` FROM `membership_groups` WHERE `name`='{$group}'");
	if(!$groupID) {
		$error = "Group '{$group}' not found.";
		return false;
	}

	// if user doesn't exist, create it
	if(!sqlValue("SELECT COUNT(1) FROM `membership_users` WHERE `memberID`='" . makeSafe($username) . "'")) {
		$randPass = substr(md5(mt_rand()), 0, 12);

		// create user
		insert('membership_users', [
			'memberID' => $username,
			'passMD5' => password_hash($randPass, PASSWORD_DEFAULT),
			'email' => $email,
			'signupDate' => date('Y-m-d'),
			'groupID' => $groupID,
			'isApproved' => 1,
			'comments' => 'Created by checkUserGroup()',
		]);

		// if not created, abort with error
		if(!sqlValue("SELECT COUNT(1) FROM `membership_users` WHERE `memberID`='" . makeSafe($username) . "'")) {
			$error = "Failed to create user '{$username}'.";
			return false;
		}
	}

	// if user exists but in a different group, abort with error
	if(sqlValue("SELECT COUNT(1) FROM `membership_users` WHERE `memberID`='" . makeSafe($username) . "' AND `groupID`<>'{$groupID}'")) {
		$error = "User '{$username}' already exists and is not a member of '{$group}' group.";
		return false;
	}

	// assuming the username field in the user table is unique, it's safe to assume that
	// no other tenant is associated with this username
	return true;
}

Here is how the above function works:

  • It checks if the given user is a member of the given group.
  • If the user doesn’t exist, it creates a new user account and adds it to the group.
  • If the user exists but is not a member of the group, it sets an error message in the $error variable and aborts.
  • If the user exists and is already a member of the group, it returns true.

Note that $error is passed by reference, so you can check its value after calling the function to see if an error occurred.

Implementing the workflow

To implement the workflow, we’ll customize several technicians table hooks. These hooks exist in the hooks/technicians.php file.

technicians_before_insert hook

This hook runs before a new record is inserted into the technicians table. We’ll use it to create a new user account for the technician. If the specified username already exists, the hook will abort and display an error message, preventing the record from being saved.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function technicians_before_insert(&$data, $memberInfo, &$args) {
	// username: allow only alphanumeric characters, underscores, and dots, and convert to lowercase
	$data['username'] = preg_replace('/[^a-z0-9_.]/', '', strtolower($data['username']));
	
	$userError = '';
	if(!addUserToGroup($data['username'], $data['email'], 'Technicians', $userError)) {
		$args['error_message'] = $userError;
		return false;
	}

	return TRUE;
}

The above code does the following:

  • It sanitizes the username field to allow only alphanumeric characters, underscores, and dots, and converts it to lowercase.
  • It calls the addUserToGroup() function to create a new user account for the technician in the Technicians group.
  • If the function returns false, the hook sets an error message in the $args['error_message'] variable and aborts.

    The $args['error_message'] variable is used to display an error message to the user letting them know why the record couldn’t be saved.

  • If the function returns true, the insert operation continues.

technicians_after_insert hook

This hook runs after a new record is inserted into the technicians table. We’ll use it to set the owner of the new record to the newly created user account. This way, the technician can log in and view their record, and the property manager can assign maintenance requests to them.

1
2
3
4
5
6
7
function technicians_after_insert($data, $memberInfo, &$args) {
	// set owner of the new record to the specified username
	set_record_owner('technicians', $data['selectedID'], $data['username']);
	WindowMessages::add("Username <code>{$data['username']}</code> has been associated with this techncian. The technician might have to reset their password to access the system.", 'alert alert-success');

	return false; // to prevent the default ownership insert operation
}

The above code does the following:

  • It calls the set_record_owner() function to set the owner of the new record to the specified username.

    $data['selectedID'] contains the ID of the newly inserted record.

  • It displays a message informing the user (the property manager) that the username has been associated with the technician.

    The WindowMessages::add() function is used to display a message to the user, applying the alert alert-success CSS class to style the message.

  • It returns false to prevent the default ownership insert operation.

    This is necessary because otherwise, the current user would be set as the owner of the new record, which is not what we want.

technicians_dv hook

This hook runs when the detail view of a technicians table record is displayed. We’ll use it to make the username field read-only to prevent accidental changes to the username.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function technicians_dv($selectedID, $memberInfo, &$html, &$args) {
	// set the username field to read-only if the record is not new and the logged member can edit the record
	if($selectedID && check_record_permission('technicians', $selectedID, 'edit')) {
		ob_start(); ?>
		<script>
			$j(() => {
				$j('#username').prop('readonly', true);
			});
		</script>
		<?php
		$html .= ob_get_clean();
	}
}

The above code does the following:

  • It checks if the record is not new and the logged member can edit the record.
  • If the above conditions are met, it adds a JavaScript snippet to the detail view form to set the username field to read-only.

This way, the username field will be editable only when adding a new technician record, and read-only when viewing an existing record.

But since the above code only makes the field read-only on the client-side, it’s a good idea to also make the field read-only on the server-side. Otherwise, a malicious user could bypass the client-side restriction and change the username field.

To do this, we’ll need to enforce the read-only restriction in the technicians_before_update hook.

technicians_before_update hook

This hook runs before an existing record in the technicians table is updated. We’ll use it to enforce the read-only restriction on the username field.

1
2
3
4
5
function technicians_before_update(&$data, $memberInfo, &$args) {
	// don't allow updating username
	$data['username'] = $args['old_data']['username'];
	return TRUE;
}

The above code does one thing: it sets the username field to its old value, effectively preventing the field from being updated. The old value is available in the $args['old_data']['username'] variable.

Previewing the workflow

After implementing the above hooks, here is a video preview of the entire workflow:

Using the above code in your project

To use the above code in your project, just add the addUserToGroup() function to your hooks/__global.php or hooks/__bootstrap.php file. There is no need to modify the function as it’s generic and can be used as is.

Then, customize the relevant table hooks adding the code snippets provided above. Replace technicians with the name of the actual table you’re working with. And replace Technicians with the name of the group you want to add the user to. You can actually have multiple tables associated with different groups. For example, you can have a technicians table associated with the Technicians group and a tenants table associated with the Tenants group.

That’s it! You’ve now automated the process of creating user accounts from records in your AppGini application. Happy coding!