User Membership
If you do not want just anyone on the Mercata Network to login and use your app, your app will have to implement some sort of user registration and membership process.
In general, there are two approaches to this - User Registration request flow, and an automatic membership flow. Both methods assume that the node that the user is accessing the application from is already a member of the Dapp Privacy Realm. (For more information on getting nodes into your Dapp Realm, see Multinode Dapp Deployment.)
Regardless of approach, both solutions require a record of a user’s membership in the app, usually capturing some type of extra information with that record, like their role, email, personal information, etc.
See the User Role Documentation for more information on roles in apps.
You can use the Asset Framework's user membership manager as a starting point to implement user membership, which includes:
- User Membership request/approve/deny flow
- User Membership records only contain user addresses and their role in the application
Here is an example of a basic User Membership record which stores a user's address, and their corresponding role:
contract UserMembershipState {
enum UserMembershipStateEnum {
NULL,
NEW,
ACCEPTED,
REJECTED,
MAX
}
}
contract UserMembership {
RoleEnum role;
address userAddress;
UserMembershipState state;
address owner;
constructor(RoleEnum _r) {
role = _r;
userAddress = _tx.origin;
owner = msg.sender;
state = UserMembershipState.NEW;
}
function setState(UserMembershipState _state) public returns (uint) {
//only the original owner can change the state
if(owner != msg.sender) {
return RestStatus.FORBIDDEN;
}
state = _state;
return RestStatus.OK;
}
}
This UserMembership
record (contract) serves as an easy to query piece of data that can be used on the server side or on the smart contract side to determine if a user is a member of the app, and their role in the app.
Membership Request Flow
This flow requires user’s to manually submit a membership request and have it approved by another party, usually a user with Admin level permissions. When building an app this way, it is important to note that you should always deploy the app with an Admin user pre-configured, otherwise there will be no user able to approve memberships into the app.
The built-in UserMembershipManager uses a FSM to determine the proper state transition of a request, ensuring that a request always goes from NEW
to ACCEPTED
/REJECTED
, and nothing else.
Below is a basic format for a User Membership Manager.
contract UserMembershipEvent {
enum UserMembershipEventEnum {
NULL,
ACCEPT,
REJECT,
MAX
}
}
contract UserMembershipManager is
UserMembershipEvent,
UserMembershipState,
Role
{
UserMembershipFSM public userMembershipFSM;
AppPermissionManager public appPermissionManager;
constructor(address _appPermissionManager) public {
userMembershipFSM = new UserMembershipFSM();
appPermissionManager = AppPermissionManager(_appPermissionManager);
}
function requestUserMembership(Role _role) public returns (uint, address) {
// note no permission checks here, any user can request membership
UserMembership userMembership = new UserMembership(_role);
return (RestStatus.CREATED, address(userMembership));
}
function handleUserMembershipEvent(
address _userMembershipAddress,
UserMembershipEventEnum _userMembershipEvent
) public returns (uint, UserMembershipStateEnum, address)
{
// handles an approve/deny membership event with the FSM and checks if caller has permission to approve/deny
}
}
Note that if more information is needed for a membership, then that should be recorded in the UserMembership
record itself, and all instances of the Manager
creating a new record should likewise be updated.
Automatic Membership
This method is only recommended if your application has a suitable default role to assign to users when they initially login or register. This role should have the least amount of privileges in the app so new users cannot instantly perform restricted operations without approval.
You may also automatically assign their role based on their Organization membership, as found in their X.509 identity.
This method can still implement a registration process, however that request for membership would not never need to be approved, it would just instantly assign the user their respective role and create their membership record.
contract Role {
enum RoleEnum {
NULL,
DEFAULT,
USER,
ADMIN
MAX
}
}
contract UserMembership is Role {
string email;
string phone;
address userAddress;
RoleEnum role;
address owner;
constructor(string _email, string _phone) {
email = _email;
phone = _phone;
role = RoleEnum.DEFAULT;
userAddress = tx.origin;
owner = msg.sender;
}
function setRole(RoleEnum _role) {
require(msg.sender == owner. "Only the contract owner can call this");
role = _role;
}
}
contract UserMembershipManager is Role {
AppPermissionManager public appPermissionManager;
constructor(address _appPermissionManager) public {
appPermissionManager = AppPermissionManager(_appPermissionManager);
}
function createUserMembership(string _email, string _phone) public returns (uint, address) {
// note no permission checks here, any user can request membership
UserMembership userMembership = new UserMembership(_email, _phone);
return (RestStatus.CREATED, address(userMembership));
}
function setUserMembershipRole(
address _userMembershipAddress,
RoleEnum _role
) public returns (uint)
{
// callable by an admin
// sets the role of a user
}
}
API Membership Middleware
If you would like to restrict all API calls to your app to registered members, you can write a JS server middleware function that can check for a user’s membership, and redirect them accordingly. This approach can also be adapted to the Automatic Membership Flow, as described above, so that users are instantly granted a default role when first interacting with the app's server.
import RestStatus from "http-status-codes"
import { RestError } from "blockapps-rest/dist/util/rest.util"
// A JS representation of the MembershipState enum values
const membershipState = {
NULL: 0,
NEW: 1,
APPROVED: 2,
REJECTED: 3,
MAX: 4,
}
const requireMembership = async (req, res, next) => {
// assumes the user's address and a "bound" dapp
// has already been attached to the request object
let { dapp, userAddress } = req
let member
try {
// get the user's membership from Cirrus
member = await dapp.getUserMembership({
userAddress: address,
})
if (!member || member.state !== membershipState.ACCEPTED) {
throw new RestError(RestStatus.FORBIDDEN, 'User membership is not approved', member)
}
//attach the membership record for possible future use
req.member = member
next()
} catch (e) {
next(e)
}
}