The Learning Pass is an application that is written in NodeJS. It is a re-write of the University of Albert's L-Pass. The move to Learning Pass was prompted by Edmonton Public Library moving off of University servers, offering an opportunity to modernize this business system.
Learning Pass is a web service that creates customer accounts on a SirsiDynix Symphony ILS. It was originally designed to allow students to register their student ID as a library card at Edmonton Public Library. The project is re-written to meet the following objectives.
- Allow organizations that partner with a library to create library accounts on behalf of that partner.
- Scale to allow multiple organizations to use the same web service.
- Allow different business rules for different organizations. For example, age restrictions, or expiry dates can be managed independently.
Copyright 2021 Andrew Nisbet and Edmonton Public Library
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Learning Pass expects registration data to conform to the following JSON schema.
{
"firstName": "Stacey",
"lastName": "Milner",
"dob": "1974-08-22",
"gender": "",
"email": "example@gmail.com",
"phone": "780-555-1212",
"street": "11535 74 Ave.",
"city": "Edmonton",
"province": "AB",
"country": "",
"postalCode": "T6G0G9",
"barcode": "21221012345678",
"pin": "I_like_Bread!",
"type": "STUDENT",
"expiry": "20210822",
"branch": "",
"status": "OK",
"notes": "Grad student"
}
The example above include a complete set of fields, but the library can control which fields are required and those that are optional.
There is a plan to Docker-ize Learning Pass but that work is out of scope for the current version of the project. That being said, here are the instructions for installing Learning Pass.
- Ensure a recent version of NodeJS is installed.
- Create a directory where the server will live (
$LPASS_HOME
from here on). - Clone the Learning Pass repo in
$LPASS_HOME
. - Install dependencies
cd $LPASS_HOME; npm install
- Create a
config.json
of the base library settings. See setup section. - Create a
[partner_name_here].json
of the partner organization's settings. See setup section. - Create a
.env
file as in this example. - On Linux, create a service to run Learning Pass.
- Start service, diagnose issues, fix, repeat as required.
- Create a
lpass.service
file, as per this example. - Copy the
lpass.service
file to/etc/systemd/system/
. You will need to do this withsudo
. - Setup and start the service.
sudo systemctl daemon-reload
sudo systemctl enable lpass
sudo systemctl start lpass
sudo systemctl status lpass
. Fix any reported issues and repeat as necessary.- Test Learning Pass with
http://server:port/status
- Set up a service like watcher.sh to do something useful with the flat files produced.
Learning Pass has a main config.json
file for the library and server settings, including a dictionary of partners and where to find their configuration file. Each partner organization has a [partner_name_here].json
that has settings that ensure registrations conform to an agreed SLA. Both the config.json
and partner.json
configurations are explained below.
The server can run with either http or https. If https is desired, ensure SSL key and certificate are installed correctly, and up-to-date.
Set the following variables to the values for your server.
LPASS_SSL_PRIVATE_KEY=/etc/ssl/private/eplwild.key
LPASS_SSL_CERTIFICATE=/etc/ssl/certs/eplwild.crt
See here for more information.
{
"application" : "Learning Pass",
"version" : "1.0",
"loopbackMode" : false,
"testMode" : false,
"customerSettings" : { },
"production" : { },
"staging" : { },
"partners" : { }
}
(Optional)
The name of the application, which can be anything you wish. It can be used in welcome messaging.
"application" : "Learning Pass",
(Optional)
The version of the config.json
file. This can be anything and is meant to help with version control.
"version" : "1.1",
(Required)
Puts the server into loopback mode. When registrations arrive the server will write flat files, but will append '.loopback' to the file name. This will stop any service from attempting to load the data on the ILS if it is not available during a planned outage. Once the outage is over, change the file(s)' name(s) by removing '.loopback' and they should be loaded on the next tick of watcher.sh. See watcher.sh for more details.
"loopbackMode" : false,
(Required)
Similar to loopback mode, but the files are output with a '.test' extension. This allows inspection of Learning Pass flat files with actually loading test data.
"testMode" : false
(Required)
Dictionaries for controlling what ports Learning Pass will listen on for inbound requests, depending on if the instance is a test or production server.
"production" : {
"httpPort" : 5000,
"httpsPort" : 5001,
"envName" : "production",
"directories" : {
"flat" : "../Incoming",
"certs" : "./https"
}
}
An array of partner dictionaries that list which organizations that are allowed to use Learning Pass. Each dictionary has three required entries of name of the partner, key API key, and config which contains the partners configurations.
[{
"name" : "Partner Name",
"key" : "partners_api_key",
"config" : "./path/to/partner.json",
"strictChecks" : false
},
]
The optional field is strictChecks a boolean that is true
by default or if not defined, but when false
tells LPass to not alter the following data from the partner.
- First name
- Last name
- Middle name
- Street
- Care of name
It is NOT recommended that this option be used because it allows the partners to pollute the ILS with inconsistent data that may contravene library policies and break search indexes.
The API key can be any string you want but should be shared with only that organization. Learning Pass uses that API key to identify which organization is sending a registration request and will parse, and modify registration information to meet the SLA of the library and partner.
You can add as many partner dictionaries as are needed to differentiate customer registrations. Think; for every class of customer, have a different partner configuration file. For example, if a school wanted student accounts to expire on August 31, but staff registration to never expire, the partner could have 2 different configuration files though, in this case it is highly likely students and staff could be differentiated in a more efficient way.
It is also helpful to have a test partner configuration to sandbox settings.
A dictionary of settings used by Learning Pass to correctly configure customer data for loading in the ILS. There are controls for things like valid password limitations, reasonable default values for missing data, and other features explored in the sections below.
"customerSettings" : {
"library" : "EPL",
"expiry" : { },
"branch" : { },
"flatDefaults" : { },
"defaults" : { },
"required" : [ ],
"optional" : [ ],
"merge" : { },
"passwords" : { }
}
(Required)
Name of the library implementing Learning Pass.
"library" : "Edmonton Public Library",
(Required)
Controls when accounts expire by default. The partner may also have an expiry dictionary which will supersede the library's. For example, if the library, by default, does not expire cards, the keyword 'NEVER' should be used.
"expiry" : {
"date" : "NEVER"
},
Other values are allowed such as a specific date in the future, or "date" can be replaced with "days", which requires an integer of the number of days an account will have before expiry.
"expiry" : {
"days": 365
}
or
"expiry" : {
"date" : "2021-08-22"
}
or
"expiry" : {
"date" : "NEVER"
}
Describes choices of branches customers can choose as their 'home' branch. The 'valid' list is all the possible branches of the library.
"branch" : {
"default" : "EPLMNA",
"valid" : ["EPLMNA","EPLWMC","EPLCAL","EPLJPL",
"EPLCPL","EPLSTR","EPLWOO","EPLHIG","EPLCSD",
"EPLMCN","EPLLON","EPLRIV","EPLWHP","EPLMEA",
"EPLIDY","EPLMLW","EPLABB"
]},
(Optional)
Flat defaults are values used during customer creation that are standard for all regular library patrons.
"flatDefaults" : {
"USER_CATEGORY5" : "ECONSENT",
"USER_ACCESS" : "PUBLIC",
"USER_ENVIRONMENT" : "PUBLIC",
"USER_MAILINGADDR" : 1,
"NOTIFY_VIA" : "PHONE",
"RETRNMAIL" : "YES"
},
(Optional)
Accounts that are missing required data can be rejected. To help improve registration success rates, reasonable default values can be substituted for missing or malformed data fields.
"defaults" : {
"city" : "Edmonton",
"province" : "AB"
}
(Optional)
Learning Pass can be configured to enforce good password selection, or to guard against local limitations. For example, the ILS may allow a wide variety of characters in passwords, but the web interface to the OPAC may have login restrictions. In such a case Learning Pass can reject accounts that do not conform to password restrictions. Some sites may require the PIN to be a four-digit number, or not contain any special characters.
Learning Pass uses javascript regular expression syntax consistent with Google's V-8 engine.
"passwords" : {
"minimum" : 4,
"maximum" : 125,
"passwordToPin" : false,
"regex" : "^[a-zA-Z0-9-!_\\s]{4,125}$"
}
In the above example passwords must be a minimum of four characters and a maximum of 125, consist of upper and lower-case letters, numbers, dashes, underscores, exclamation marks, and / or spaces in any combination. Note the use of double-escaped 's' for spaces in JSON.
The default regex restricts passwords using the following regular expression.
^[a-zA-Z0-9-!#$@&^,.:;()[\]~^%@*_+=\s]{4,125}$
(Optional)
It may be necessary to merge two or more fields in customer data to make a new value. An example could be city and province. In some Symphony instances they may be concatenated into a single value of 'city, province'. Use the merge dictionary to indicate which fields are to be merged.
"merge" : {
"delimiter" : ", ",
"fields" : {
"CITY_STATE" : ["city","province"],
"USER_NAME" : ["lastName","firstName"]
}
}
In the above example, province would be appended to the end of the city value, separated by a comma and space, and replaces the CITY/STATE
field. Note in the second example, the last name and first name are appended with a comma and space, then used as the flat field 'USER_NAME' in the final flat file.
(Required)
A successful registration contains valid data in all the fields marked required. In the following example config, the library specifies that the minimum registration information is first name, last name, barcode, and email.
"required" : [
"firstName",
"lastName",
"barcode",
"email"
],
Missing or malformed data in these fields will cause the account to be rejected with an explanation sent back in the response to the caller.
(Optional)
Optional fields are fields that may or may not be present in the customer data. If they are they are filtered and cleaned like required fields, but unlike required fields missing optional fields do not cause the registration to be rejected.
"optional" : [
"city",
"province",
"phone",
"gender",
"dob"
],
[x] The few the fields that are marked as required, the less chance the account will be rejected.
[x] The library should decide what their minimal requirement is and set that in their config.json
.
[x] Further refinement can be controlled in the partner.json
file, depending on their ability to provide information. For example, if a partner can, and agrees to supply gender information, the server can treat it as required or optional without impacting other organizations.
Each partner has a configuration json file that can be named anything.json. In it are the partner organization's settings, each of which define the agreement of expectations between the library and partner organization.
Many of the dictionaries in the partner.json are similar to the library, and if present supersede the library settings, except for "age"
. Age is the only setting where the library setting trumps the partner's. In this way the library can stop anyone under age using the service, but since that can be confusing and lead to complicated rules later, it is not recommended to add an "age"
dictionary to the config.json
.
In other cases some settings are only available in the partner.json file. These include "typeProfiles"
, "genderMap"
, "barcodes"
,"statusMap"
, and "notes"
.
{
"name" : "default",
"barcodes" : { },
"expiry" : { },
"typeProfiles" : { },
"genderMap": { },
"statusMap" : { },
"age" : { },
"required" : [ ],
"optional" : [ ],
"defaults" : { },
"flatDefaults" : { },
"notes" : { }
}
(Required)
The name of the partner organization. This value is used for reporting and logging, and can be any descriptive string.
(Optional)
TODO required or optional
Controls expected values from the customer. A partner want the registrant's barcode to be the employee number or student number. minimum
and maximum
indicate the range of the number of characters in the user's library ID. If the organization has IDs that conflict with other partners' ID, a prefix can be pre-pended to stop each organization over-writing the other's data.
For example, two companies become partners and register employee number 1234. In this case a prefix can be added to each registration to ensure the IDs do not clash during registration. The ID is further padded with '0' (zeros) to create an ID of "maximum"
characters. In this example the resultant ID is 21221800001234
.
"barcodes" : {
"prefix" : "212218",
"minimum" : 13,
"maximum" : 14
},
"prefix"
is required but can be an empty string.
"minimum"
required. TODO guard for reasonable values.
(Optional)
Functions exactly like the library's settings, but supersede those values.
(Required)
This dictionary maps types or categories of customers from the partner to a Symphony profile.
"typeProfiles" : {
"Accounting" : "EPL_ADULT",
"ITStaff" : "EPL_JUV",
"Manager" : "EPL_SPECIAL"
},
(Optional)
Gender is a tricky and complicated metric. Some libraries don't bother collecting this data any more, but some do. If the partner is allowed, willing, and able to supply it, they may have potentially dozens of categories. "genderMap"
translates these data to values meaningful to the ILS (as a USER_CATEGORY usually).
"genderMap": {
"male" : "M",
"dude" : "M",
"female" : "F",
"zirs" : "NA",
"zi" : "NA",
"prefers not to say" : "X"
},
(Optional)
Very occasionally a partner may provide some status for the registrant that may be useful to the library. The "statusMap"
translates their definition of status to something the library can act on.
"statusMap" : {
"GOOD" : "OK",
"SUSPENDED" : "DELINQUENT",
"FIRED" : "BLOCKED"
},
(Optional)
Use this dictionary if the partner stipulates an age restriction for registrations. In the example, the organization wants only people over the age of 18
to be able to register. Anyone less than this age is rejected. All dates are computed based on the first second of the date provided.
"age" : {
"minimum" : 18
},
(Optional)
Functions exactly like the library's settings, but supersede those values.
(Optional)
Functions exactly like the library's settings, but supersede those values.
(Optional)
Functions exactly like the library's settings, but supersede those values.
(Optional)
Functions exactly like the library's settings, but supersede those values.
(Optional)
The notes field in a customer registration can be used for two purposes.
- If the
"notes"
dictionary is not included in the partner's configuration settings, any note is added as-is to the account. - If the
"notes"
dictionary is used, the"require"
path indicates the location of the plugin that Learning Pass uses to further process the customer account. For example, the plugin could compute a user category, access a street address validation service, or check for a duplicate account. Its only limit is your imagination.
"notes" : { "require" : "../path/to/partner.js" }
A template of how to set up this functionality can be found in the project's plugin/notes.js
file.
At this time there are only two Learning Pass dependencies; Winston and dotenv. This may change, but the documentation may not, so always reference the package.json
for more information.
There is a Makefile
which has all the rules for running either individual or all unit tests.
[Unit]
Description=Runs Learning Pass (LPass) as a service
[Service]
ExecStart=/usr/bin/node /home/ils/LPass/server/index
Restart=always
User=ils
Group=ils
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=staging
WorkingDirectory=/home/ils/LPass/server
[Install]
WantedBy=multi-user.target
Example of a .env
file that lives in the $LPASS_HOME
directory.
TEST_API_KEY=test_api_key_also_secret
NEOS_API_KEY=NEOS_partner_secret_api_key
LPASS_SSL_PRIVATE_KEY=/foo/bar/key.pem
LPASS_SSL_CERTIFICATE=/foo/bar/cert.pem
The following http status values can be returned in the response header.
-
"SUCCESS" - Success.
-
"ACCEPTED" - Valid accounts created, but not not loaded if the ILS was put into loopback or test mode.
-
"NO_CONTENT" - rejected because there was no customer data.
-
"PARTIAL_CONTENT" - rejected account missing required fields.
-
"NOT_AUTHORIZED" - invalid or no API key.
-
"NOT_FOUND" - function not supported by Learning Pass API.
-
"NOT_ALLOWED" - attempt to use the service to register someone that is underage or otherwise not allowed to have an account created by Learning Pass.
-
"INTERNAL_ERROR" - ILS unavailable or Learning Pass server configuration issue. Check
sudo systemctl status lpass
or log files for diagnostic information.
[x] Handle customer status.
[x] Complete PIN helpers.
[x] Complete password checking for customer.
[x] Manage expiry.
[x] Manage customer's preferred branch.
[x] Stub notes.js for future dev if required.
[ ] Implement issuing library cards in barcodes section.
[x] Create flat file of customer data.
[x] Server
[ ] Upload library card list if admin.
[ ] New route to show available branches.
[ ] Allow custom settings by partner. This would allow a partner to turn off strict checking of address formats or string capitalization.