Permissions

Permissions are handled by 2 main classes: Authenticator and Authorizer. The Authenticator is responsible for checking if someone is logged in, and the Authorizer is responsible for checking if a logged in person has the right permissions to do something.

Important Classes

Authenticator - See if someone is logged in, who it is, and persist a session token to keep a person logged in.
MethodReturnsDescription
getPerson()Person | nullGet the logged in person, if someone is logged in
setSessionToken(token: string, person: Person)voidLog a person in by setting their token and Person record
getSessionToken()string | nullGet the session token of a logged in person
isLoggedIn()boolCheck if someone is logged in
clearSession()voidClear the session, logging the person out
addEventListener<N extends 'did-login'‘did-logout’>(name: N, cb: Payloads[N])`void
removeEventListener<N extends 'did-login' | 'did-logout'>(name: N, cb?: Payloads[N])voidRemove an event listener, passing no cb will remove all listeners for that event
Authorizer - Check if a person has the right permissions to do something. Works if someone is not logged in.
MethodReturnsDescription
can<ContractId, Ids>(options: AuthorizerCanOptions<ContractId, Ids>)Promise<Record<Ids, boolean>>Check if the current person has a permission
savePermissions<ContractId, Ids>(options: SavePermissionsOptions<ContractId, Ids>)Promise<void>Save permissions for a person. Note: the person must have the permission to save permissions

Checking in the backend

Coming soon…

Checking in Skill Views

Redirecting if someone is not logged in

Test 1: Check for redirect on load

We’re going to write this test with the person not logged in and redirect, but it’ll take another test to get the Authenticator into the production code.

import { fake, AbstractSpruceFixtureTest } from '@sprucelabs/spruce-test-fixtures'

//@fake.login() will ensure a fake person is logged in for each test
@fake.login()
export default class RootSkillViewTest extends AbstractSpruceFixtureTest {

    @test()
    protected static async redirectsToOnboardingIfNotLoggedIn() {
        const vc = this.views.Controller('eightbitstories.root', {})

        //first thing we do is log the person back out =)
        this.permissions.getAuthenticator().clearSession()

        //Assert that loading the skill view redirects
        await vcAssert.assertActionRedirects({
            action: () => this.views.load(vc),
            router: this.views.getRouter(),
            destination: {
                id: 'eightbitstories.onboarding',
            },
        })
    }
}
MethodReturnsDescription
views.Controller(viewId: string, options: object)ViewCreates and returns a view controller for the specified view ID with the given options.
permissions.getAuthenticator().clearSession()voidClears the current session, logging the person out.
views.load(view: View)PromiseLoads the specified view and returns a promise that resolves when the view is loaded.
views.getRouter()RouterGets the router instance used for navigating between views.
vcAssert.assertActionRedirects(options: object)PromiseAsserts that a specified action redirects to the expected destination. Options include the action, router, and destination.
Production 1: Redirect no matter what

We actually don’t need to check if the person is logged in yet! Go TDD!

import { AbstractSkillViewController, SkillViewControllerLoadOptions } from '@sprucelabs/heartwood-view-controllers'

export default class RootSkillViewController extends AbstractSkillViewController {

    public async load(
        options: SkillViewControllerLoadOptions
    ): Promise<void> {
        const { router } = options

        await this.router.redirect('eightbitstories.onboarding')
        
    }
}

MethodReturnsDescription
router.redirect(destination: string)PromiseRedirects to the specified destination.
load(options: SkillViewControllerLoadOptions)PromiseLoads the view controller with the given options and redirects to the ‘eightbitstories.onboarding’ destination.
Test 2: Should not redirect if logged in

Now we’ll test that it does NOT redirect if someone is logged in, which will force us to do the authenticator.isLoggedIn() check. Something to note: If a redirect is triggered without an assert, it will throw an error and fail the test. So, you don’t actually need to assert anything in this test.

...

@test()
protected static async shouldNotRedirectIfLoggedIn() {
    const vc = this.views.Controller('eightbitstories.root', {})

    //Because we use the @fake.login() decorator, the person is already logged in
    await this.views.load(vc)
}

...

MethodReturnsDescription
views.Controller(viewId: string, options: object)ViewCreates and returns a view controller for the specified view ID with the given options.
views.load(view: View)PromiseLoads the specified view and returns a promise that resolves when the view is loaded.
test()voidMarks a method as a test method to be executed by the test runner.
shouldNotRedirectIfLoggedIn()PromiseEnsures that the view does not redirect if someone is already logged in.
Production 2: Check if person is logged inNow, inside our `RootSkillViewController`, we'll check if the person is logged in before redirecting. If you run logic after this check, you'll need to write tests to ensure that logic is not run after the redirect (not covered in this example).
import { AbstractSkillViewController, SkillViewControllerLoadOptions } from '@sprucelabs/heartwood-view-controllers'

export default class RootSkillViewController extends AbstractSkillViewController {

    public async load(
        options: SkillViewControllerLoadOptions
    ): Promise<void> {
        const { router, authenticator } = options

        if (!authenticator.isLoggedIn()) {
            await this.router.redirect('eightbitstories.onboarding')
        }
    }
}

MethodReturnsDescription
router.redirect(destination: string)PromiseRedirects to the specified destination.
authenticator.isLoggedIn()booleanChecks if the person is logged in and returns true if logged in, false otherwise.
load(options: SkillViewControllerLoadOptions)PromiseLoads the view controller with the given options, and if the person is not logged in, redirects to the ‘eightbitstories.onboarding’ destination.
Test 3: Refactor tests

Here is how you could refactor your tests to make them more readable and maintainable.

import { fake, AbstractSpruceFixtureTest } from '@sprucelabs/spruce-test-fixtures'
import RootSkillViewController from '../../skillViewControllers/Root.svc'

@fake.login()
export default class RootSkillViewTest extends AbstractSpruceFixtureTest {
    protected static vc: RootSkillViewController

    protected static async beforeEach() {
        await super.beforeEach()

        //Construct the RootSkillViewController once here to work on in every test
        this.vc = this.views.Controller('eightbitstories.root', {})
    }

    @test()
    protected static async redirectsToOnboardingIfNotLoggedIn() {
        //This could be exctracted too, but I'll wait until the second time we need to call it
        this.permissions.getAuthenticator().clearSession()

        await vcAssert.assertActionRedirects({
            action: () => this.load(),
            router: this.views.getRouter(),
            destination: {
                id: 'eightbitstories.onboarding',
            },
        })
    }

    @test()
    protected static async shouldNotRedirectIfLoggedIn() {
        await this.load()
    }

    //I'll extract `this.views.load(this.vc)` to `this.load()` because it's been called twice now
    protected static async load() {
        await this.views.load(this.vc)
    }
}
MethodReturnsDescription
views.Controller(viewId: string, options: object)ViewCreates and returns a view controller for the specified view ID with the given options.
permissions.getAuthenticator().clearSession()voidClears the current session, logging the person out.
vcAssert.assertActionRedirects(options: object)PromiseAsserts that a specified action redirects to the expected destination. Options include the action, router, and destination.
test()voidMarks a method as a test method to be executed by the test runner.
beforeEach()PromiseSets up the necessary preconditions before each test, including constructing the RootSkillViewController.
redirectsToOnboardingIfNotLoggedIn()PromiseTests that the controller redirects to onboarding if the person is not logged in.
shouldNotRedirectIfLoggedIn()PromiseTests that the controller does not redirect if the person is logged in.
load()PromiseLoads the RootSkillViewController for use in tests.

In event contracts

Coming soon…

Something Missing?

Request Documentation Enhancement
It looks like you are using Internet Explorer. While the basic content is available, this is no longer a supported browser by the manufacturer, and no attention is being given to having IE work well here.