Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

onboarding.md 13KB

User Sign Up

  1. User arrives at home page
  2. User fills out bare minimum information to create profile
  3. User is emailed a verification email
  4. User clicks on transactional email
  5. User is redirected to application
  6. User is logged in and presented with home screen

User Arrives at Application

  1. Upon first arrival at the Siimee application, the Vue Router’s guards are called in the router.beforeEach() method. A simple conditional checks to see if the application is runnning in development mode. If DEV == false, then the checkLoginStatus() method is called, imported from src/router/guards.js.
// frontend/main.js
router.beforeEach((to, from, next) => {
  if (DEV) {
    next();
  } else {
    checkLoginStatus(to, next);
  }
});
  1. In the guards.js file, the checkLoginStatus method itself first calls the loginIfToken() method. Within this loginIfToken() method, a sessionData variable is created which is the asyncronous returned results of the authenticator.verifySessionCookie() method:
// src/router/guards.js
const loginIfToken = async () => {
  const sessionData = await authenticator.verifySessionCookie();
  if (
    sessionData?.profileId &&
    sessionData?.sessionToken &&
    !currentProfile.isLoggedIn
  ) {
    await currentProfile.login(
      sessionData.profileId,
      WaveUI.instance.notify,
      sessionData.sessionToken,
    );
  }
};
  1. Following to the auth.service.js file from services directory, we find the invoked verifySessionCookie() method, which itself calls the validateSession() method:
// src/services/auth.service.js
    async verifySessionCookie() {
        const validatedToken = await this.validateSession()
        if (validatedToken.error)
            return console.error('ERROR :=>', validatedToken.error)
        return validatedToken
    }
  1. Which itself calls the grabStoredSessionToken()
// src/services/auth.service.js
    async validateSession() {
        const hashedSessionToken = this.grabStoredSessionToken()
        let validation
        try {
            validation = await db.post(
                '/user/validate-session',
                hashedSessionToken,
                true,
            )
        } catch (error) {
            console.error(error)
        }
        console.log('validatedSession :>> ', validation)
        return validation
    }
  1. Finally, the cookie, ‘siimee_session’ is grabbed from the browser’s window.document object.
// src/services/auth.service.js
    grabStoredSessionToken(cookieKey = 'siimee_session') {
        const cookies = document.cookie.split('; ').reduce((prev, current) => {
            const [name, ...value] = current.split('=')
            prev[name] = value.join('=')
            return prev
        }, {})
        if (!cookies[cookieKey])
            return console.warn(
                'WARNING :=> accessToken is not defined; There was problem with session cookie you are not logged in.',
            )
        return cookies[cookieKey]
    }
  1. Returning to the validateSession() method, we now should have a sessionToken if there is one (otherwise we receive a warning, indicating that the user is not logged in). This hashedSessionToken is now posted to a route on the backend: ‘/user/validate-session’, along with the boolean value of true.
// src/services/auth.service.js
    async validateSession() {
        const hashedSessionToken = this.grabStoredSessionToken()
        let validation
        try {
            validation = await db.post(
                '/user/validate-session',
                hashedSessionToken,
                true,
            )
        } catch (error) {
            console.error(error)
        }
        console.log('validatedSession :>> ', validation)
        return validation
    }
  1. On the backend the /validate-session route is hit. First a variable is instantiated called validatedSessionToken which holds the return value of the userService.validateSession(hashedSessionToken) method:
// backend/lib/routes/user/validate-session.js
const validatedSessionToken = userService.validateSession(hashedSessionToken);
  1. Tracking down the validateSession() method in the services/user.js file, we find that the UserService class’s activeSessions object is checked to see if it has a key that matches the hashedSessionToken parameter passed to it (our sessiontoken passed from the document’s cookies). Two conditionals are checked against whether or not the userSession even exists, as well as if the emailWasRespondedTo was ever defined (emailWasRespondedTo originally set to false when signing up). The sessionToken is then grabbed from the userSession.sessionToken (destructuring could have been used here). The validateToken(sessionToken) method is then called.
// backend/lib/services/user.js, line 255
   validateSession(hashedSessionToken) {
       const userSession = this.activeSessions[hashedSessionToken]
       if (!userSession) {
           throw new Error(
               'hashedSessionToken not in activeSessions registry!',
           )
       }
       if (!userSession.emailWasRespondedTo) {
           throw new Error('email was never responded to!')
       }
       const sessionToken = userSession.sessionToken
       const sessionTokenIsValid = this.validateToken(sessionToken)
       return {
           ...sessionTokenIsValid.payload,
           sessionToken: this.activeSessions[hashedSessionToken].sessionToken,
       }
   }
  1. The validateToken method. Firstly the key is extracted from the server.registrations[‘main-app-plugin’].options.jwtKey object, established in the server/manifest.js file:
// backend/lib/services/user.js, line 241
    validateToken(token) {
        const key = this.server.registrations['main-app-plugin'].options.jwtKey
        try {
            return JWT.verify(token, key)
        } catch (err) {
            return { payload: null, message: err.message }
        }
    }
// backend/server/manifest.js, line 72
options: {
  jwtKey: {
    $filter: 'NODE_ENV',
    $default: {
      $param: 'APP_SECRET',
      $default: 'app-secret',
    },
    // Use .env file in production
    production: {
      $param: 'APP_SECRET',
    },
  },
}
  1. The JWT, from the ‘jsonwebtoken’ package is used to verify the token passed as well as the key. Should it fail, the return value of { payload: null, message: err.message } is returned instead.

  2. Returning back to the validateSession(hashedSessionToken) in the services/user.js, we then proceed to destructure the return value of this sessionToken’s payload, as well as the sessionToken from this.activeSession[hashedSessionToken] object.

// backend/lib/services/user.js, line 265
const sessionToken = userSession.sessionToken;
const sessionTokenIsValid = this.validateToken(sessionToken);
return {
  ...sessionTokenIsValid.payload,
  sessionToken: this.activeSessions[hashedSessionToken].sessionToken,
};
  1. Assumming the user is within the userService.activeSession object, this should return the extracted data within the JWT, via the destructured …sessionTokenIsValid.payload object. This should contain the email. Returning back to our /validate-session route, we then pass this email utilizing the userService.findByUserEmail method:
// backend/lib/routes/user/validate-session.js, line 36
const user = await userService.findByUserEmail(validatedSessionToken.email);
  1. This leads us back to our services/user.js file, where we simply query our users table to find the first user where that email matches:
// backend/lib/services/user.js, line 104
async findByUserEmail(userEmail, txn) {
  const { User } = this.server.models()
  const user = await User.query(txn)
    .throwIfNotFound()
    .first()
    .where({ user_email: userEmail })
  return user
}
  1. Returning back to our /validate-session route, a type variable is then determined based off of the is_poster field returned from this query, where it is assigned either the string ‘poster’ or ‘seeker’ based off of the returned value of this is_poster field:
// backend/lib/routes/user/validate-session.js, line 39
const type = user.is_poster === 1 ? "poster" : "seeker";
  1. Thie user.user_id as well as the type variables are then passed to the profileService.getCompleteProfilesFor() method:
// backend/lib/routes/user/validate-session.js, line 40
const profiles = await profileService.getCompleteProfilesFor(
  user.user_id,
  type,
);
  1. This method is declared in the /services/profile.index.js file. It brings in the Profile model to query the profiles table. The _setTagLookup() method is also invoked to set all tags upon application load. The userId argument is then passed to grab the profileId of the user and assigned to the dedupedProfileIds variable. These ids are then passsed to the Profile model, which not only grabs the profile fields from the Profile table, but also grabs the corresponding tags, responses, and user data from their respective tables. Finally a series of helper functions from profiler.js are invoked under the overarching method makeCompleteFromProfileEntries() method.
// backend/lib/services/profile/index.js, line 98
async getCompleteProfilesFor(userId, type) {
  const { Profile } = this.server.models()
  await this._setTagLookup()

  const dedupedProfileIds = await this._getProfileIdsForUserId(userId)

  const profilesEntries = await Profile.query()
    .whereIn('profile_id', dedupedProfileIds)
    .withGraphFetched('tags')
    .withGraphFetched('responses')
    .withGraphFetched('user')

  return profiler.makeCompleteFromProfileEntries(
    profilesEntries,
    type,
    this.tagLookup,
  )
}
  1. Once again returning to the /validate-session route, the profile_id field is then grabbed from the first entry of the return array from getCompleteProfilesFor() method and assigned to the profileId variable.
// backend/lib/routes/user/validate-session.js, line 45
const profileId = profiles[0].profile_id;
  1. Finally, should all methods succeed, the route returns the destructured validatedSessionToken data, as well as the profileId back to the frontend.
// backend/lib/routes/user/validate-session.js, line 46
return {
  ok: true,
  handler: pluginConfig.handlerType,
  data: {
    ...validatedSessionToken,
    profileId: profileId,
  },
};
  1. At our frontend, the validateSession() method then returns this object as the variable validation within the auth.service.js file.
// src/services/auth.service.js, line 20
async validateSession() {
  const hashedSessionToken = this.grabStoredSessionToken()
  let validation
  try {
    validation = await db.post(
    '/user/validate-session',
    hashedSessionToken,
    true,
    )
  } catch (error) {
    console.error(error)
  }
  console.log('validatedSession :>> ', validation)
  return validation
}
  1. This then returns us back to the router/guards.js file where this returned validation variable is assigned to the sessionData variable. It is then checked alongside the currentProfile object to see if the sessionData.profileId, the sessionData.sessionToken, and the currentProfile.isLoggedIn variables are defined/truthy or not. Should all the conditionals pass, the currentProfile.login() method is invoked, passing the sessionData.profileId, as well as the sessionData.sessionToken variables to instantiate an authenticated session:
// src/router/guards.js, line 17
const loginIfToken = async () => {
  const sessionData = await authenticator.verifySessionCookie();
  if (
    sessionData?.profileId &&
    sessionData?.sessionToken &&
    !currentProfile.isLoggedIn
  ) {
    await currentProfile.login(
      sessionData.profileId,
      WaveUI.instance.notify,
      sessionData.sessionToken,
    );
  }
};
  1. This loginIfToken method is called to establish a logged in, authenticated session should there be a session-token cookie in the user’s browser’s window.document.cookie object. The console.info() method is called within the log() method to inform any developers monitoring the console of the authenticated status of the user, and also where the user is then be rerouted to by the Vue Router.$A
// src/router/guards.js, line 34
log(destination);
// src/router/guards.js, line 6
async function log(to) {
  if (!currentProfile.isLoggedIn || !currentProfile.isComplete) {
    console.info(
      `[Guard Status debug]: Profile: ${currentProfile.id.value} | Login: ${currentProfile.isLoggedIn} | Complete: ${currentProfile.isComplete}`,
    );
  }
  console.info("[Guard Status debug]: being routed to:", to.fullPath);
}