# Teams
The teams
feature is where team management code is located. This allows team owners to add/remove/modify team members, and restrict access based on permissions and roles.
# Enabling
The teams feature is enabled by default, but if your service does not require team management, you can disable this feature by modifying the features.wood
key in app/config/app.js
and omitting the teams
feature:
features: {
app: [
// your app features here
],
wood: [
'users',
'admin',
'subscriptions',
// 'teams',
],
},
It is important to note that all users will have a team, regardless of if the teams feature is enabled or not, and things like subscriptions will be attached to the team, not the user. This is a best practice to ensure that if you one day need to enable teams for your app, you are saved a truly monumental amount of conversion/migration work. In the meantime, it does not add a significant amount of overhead.
# Configuring
Team configuration values are stored in config/teams.js
.
# permissions
and roles
:
This is where you define roles that can be assigned to users, and permissions those roles grant, like so:
const permissions = {
manage_team: {
name: 'Manage Team',
description: 'Invite/manage/remove team members.',
},
order_supplies: {
name: 'Order Supplies',
description: 'Order supplies to be used for training.',
},
// ...
};
const roles = {
owner: {
name: 'Coach',
description: 'Manages team, can order supplies.',
permissions: ['manage_team', 'order_supplies'],
},
// ...
};
The ID of a permission
is its key in the permissions
object (manage_team
, order_supplies
in the example). The name and description are used when assigning a new role to a team member, so you can be sure of what permissions are being granted to them when granting them the new role.
The role
object works similarly. The ID is the key in the roles
object, and this is what is stored in the database and connected with the user. The name
and description
are self-descriptive, and the permissions
attribute is an inclusive list of the IDs of the permissions this role grants.
# teamNameOptional
:
When true
, the team name on the signup form is optional. If not provided, the team name will be set to ${userName}'s Team
.
# subscriptionMemberLimits
:
If this field is set to true
, the meta.max_members
field in each product in config/stripe/products.json
will configure the maximum number of team members allowed on a team, when that team is subscribed to that product. Any number value in max_members
will enforce that as the maximum number of members allowed on that team, while if that number is null
, undefined
, or simply missing, it means there is no limit placed on that product.
# inviteExpiry
:
This is the amount of time after an invite is issued that it remains valid for. These values are used as parameters for Moment.js's manipulation functions (opens new window).
# language
:
You may not want to refer to your teams as "Teams", instead opting to use something more-specific, like "Companies", or less-specific, like "Organizations". The language
object contains all all strings in the Teams feature that refer to teams as "Teams". By changing the text in these fields, you can change what "Teams" appear as to your users in the UI and in API responses.
# Important URLs
The primary important URL is /team
, which leads to the team management page. It can be found in the user drop-down in the top right, under "Team". On this page, a list of all members on the team will be displayed, as well as an "Action" column for changing the team member roles, removing team members, etc. This column, and all team management functions, will only be displayed to or usable by team members with a role that grants the manage_team
permission.
# Database tables
There are two database tables in place to provide team functionality:
# teams
This table contains the team name (defaults to the primary user name when the teams feature is disabled) as well as Stripe subscription information.
# users_teams
This table links teams
to users
by way of a composite primary key (team_id
, user_id
) and a role
field that is the ID of the role this user has been given. By default, the first user on a team will be granted the owner
role.
# API: Team and Role in the Request object
In addition to the user
object attached to the req
object of every controller route, there will also be a team
and role
object. These are parsed out from the user's JWT, and then confirmed, to ensure that the user's role hasn't changed or that they haven't been removed from the team. (In these cases, the user's jwt_series
is also updated, which should immediately log out the user, for further security.)
The team
object is an instance of TeamModel
and is only really useful if you want the team name or Stripe information.
The role
object is an instance of RoleModel
and is the primary method of checking if the user has permission to access the route in question. (See checking permissions.)
# UI: Team and Role in Vuex stores
The active user's team and role are stored in the Users
store, under team
and role
, respectively.
The team
object is an instance of TeamModel
and is only really useful if you want the team name or Stripe information.
The role
object is an instance of RoleModel
and is the primary method of checking if the user has permission to access the route in question. (See checking permissions.)
# Checking permissions
Once you have a user's RoleModel
, you can check and see if they have permissions to access a particular resource. The simplest way is to just ask the model itself:
if (! req.role.hasPermissions(['access_secret_area'])) {
throw new Error('NOPE');
}
However, in API controllers, you can create an Express middleware to make it easy to enforce permissions on a per-route basis:
const requireTopSecret = this.requirePermissions(['top_secret']);
this.router.get('/secrets/:id', requireTopSecret, this.showSecrets.bind(this));
# TeamModel
The TeamModel
represents a single team in your application, and has the following properties:
id
: The internal ID of the team. This isn't ever sent directly to the application. Instead, Hashids (opens new window) are used to obfuscate this value before it is sent via the API.name
: The name of the team. This defaults to "UserName's Team" if the user declines to enter a anme.currency
: The currency code this team's subscription is charged in. Defaults to USD.stripeCustomerId
: If the team has a subscription, this is the Stripe customer ID for that subscription.createdAt
: A timestamp indicated when the team was created.updatedAt
: A timestamp indicating when the team was last updated.flags
: A JSONB object that can store any value. Should be used to store values that a team may only occasionally have set. More-permanent values should have their own columns defined. These values can be queried either from the database (opens new window) or directly off the model with thehasFlag()
function.secureFlags
: A JSONB object that can store any value, similar toflags
. However, these values are not serialized intoJSON()
and thus will never be sent over the API. These values can be queried either from the database (opens new window) or directly off the model with thehasFlag()
function.subscription
: ASubscriptionModel
object, if the team has a subscription.