Meteor-rest equivalent for meteor v3

In meteor v2 Ive previously used meteor-rest to create a json api for my meteor app. meteor-rest is no longer supported, and in meteor v3 I understand that I can use Express and its ecosystem. Does anyone have any good pointers (beyond the migration guide) for doing this and:

  1. creating endpoints to login (returning bearer token) and logout
  2. creating middleware to use that bearer token in my custom rest routes

or perhaps some hints to an alternative strategy?

bw

Matt

1 Like

Can you post the code you are actually using for those endpoints so people can help you with a better example of how to migrate them?

Per the Meteor v3 docs AI:

To set up a REST endpoint in Meteor 3, you can leverage the new Express integration. Here’s how you can do it:

  1. First, make sure you’re using Meteor 3, which includes Express under the hood.

  2. In your server/main.js file, import the WebApp module:

import { WebApp } from "meteor/webapp";
  1. Then, you can use WebApp.handlers as your Express instance to define your REST endpoints. Here’s an example of how to create a GET endpoint:
import { LinksCollection } from "/imports/api/links";
import { WebApp } from "meteor/webapp";

WebApp.handlers.get("/all-links", async (req, res) => {
  const links = await LinksCollection.find().fetchAsync();
  res.json(links);
});

This code sets up a GET endpoint at /all-links that returns all links from the LinksCollection as JSON Meteor v3 uses express under the hood – How to use and deploy it..

You can define other HTTP methods (POST, PUT, DELETE, etc.) in a similar manner using WebApp.handlers.

This approach allows you to easily create RESTful APIs or serve static files using the familiar Express syntax within your Meteor 3 application Embracing the Future: Meteor 3 with Node v20 and Express.

Remember that this is a new feature in Meteor 3, so make sure you’re using the latest version to take advantage of this Express integration.

This is the current code that Im using …

import { Accounts } from 'meteor/accounts-base'
import { check, Match } from 'meteor/check'
import { WebApp } from 'meteor/webapp'
import bearerToken from 'express-bearer-token'
import bodyParser from 'body-parser'

// provides middleware and rest end points to be used with the rest api
// inspired by [simple:rest](https://atmospherejs.com/simple/rest)

// express middleware for authenticating a user resume token
export const MeteorRestAuthentication = (opts) => {
  const bearerTokenMiddleware = bearerToken(opts)

  const authMiddleware = async (req, res, next) => {
    if (req.token) {
      const hashedToken = Accounts._hashLoginToken(req.token)
      const user = await Meteor.users.findOneAsync({
        'services.resume.loginTokens.hashedToken': hashedToken,
      })
      if (user) {
        req.userId = user._id
      }
    }
    next()
  }

  return [bearerTokenMiddleware, authMiddleware]
}

// code to add login and logout routes, run from server
export const addRestAuthRoutes = () => {
  const app = WebApp.express()
  const router = WebApp.express.Router()

  // login route - POST username or email address and password
  // returns bearer token if successful
  router.post('/users/login', bodyParser.urlencoded({ extended: true }), async (req, res) => {
    // note that bodyParser middleware enables req.body to yield js object of form content in following line
    const options = req.body

    let user
    if (options.email) {
      check(options, {
        email: String,
        password: String,
      })
      user = await Meteor.users.findOneAsync({ 'emails.address': options.email })
    } else {
      check(options, {
        username: String,
        password: String,
      })
      user = await Meteor.users.findOneAsync({ username: options.username })
    }

    if (!user) {
      res.status(400).send({ message: 'Bad Request' })
    } else {
      const result = await Accounts._checkPasswordAsync(user, options.password)
      check(result, {
        userId: String,
        error: Match.Optional(Meteor.Error),
      })

      if (result.error) {
        res.status(400).send({ message: 'Bad Request' })
      } else {
        const stampedLoginToken = Accounts._generateStampedLoginToken()
        check(stampedLoginToken, {
          token: String,
          when: Date,
        })

        await Accounts._insertLoginToken(result.userId, stampedLoginToken)

        var tokenExpiration = Accounts._tokenExpiration(stampedLoginToken.when)
        check(tokenExpiration, Date)

        res.send({
          id: result.userId,
          token: stampedLoginToken.token,
          tokenExpires: tokenExpiration,
        })
      }
    }
  })

  router.get('/users/logout', MeteorRestAuthentication(), async (req, res) => {
    if (req.userId) {
      // the existence of userId on the request signals an existing login token
      // (see middleware above) so no need to confirm
      const hashedToken = Accounts._hashLoginToken(req.token)
      await Meteor.users.updateAsync(
        { _id: req.userId },
        { $pull: { 'services.resume.loginTokens': { hashedToken: hashedToken } } },
      )
      res.send({ message: 'OK' })
    } else {
      res.status(400).send({ message: 'Bad Request' })
    }
  })

  app.use('/', router)
  WebApp.handlers.use(app)
}
2 Likes

Just wanted to give a shoutout this example code, I don’t know how it came to be, but searching around for examples on how to bolt web api auth onto a Meteor app (especially post v3 with express), this example is very detailed and appreciated (edit: though I guess you’re still seeking advice - regardless, for lazier people like me it’s good to see what others are doing).

I might be missing something but I think login token lifecycle management might need to be addressed - that is, if it’s omission wasn’t by design.

Otherwise I can see a situation where you end up with people permanently authenticated until they access the Meteor DDP world and their token gets invalidated, then their API stops working?

(Or do stale tokens only get removed when a DDP client attempts to use that one specifically? So the web API tokens just hang around forever?)

Ahh, yes you’re quite right - many thanks for pointing this out. The MeteorRestAuthentication middleware needs to honour the tokenExpires mechanism. In my tests the following seems to do this:

// express middleware for authenticating a user resume token
export const MeteorRestAuthentication = (opts) => {
  const bearerTokenMiddleware = bearerToken(opts)

  const authMiddleware = async (req, res, next) => {
    if (req.token) {
      const hashedToken = Accounts._hashLoginToken(req.token)
      const user = await Meteor.users.findOneAsync(
        {
          'services.resume.loginTokens.hashedToken': hashedToken,
        },
        { fields: { 'services.resume.loginTokens': 1 } },
      )
      if (user) {
        let resume = user.services.resume.loginTokens.find(
          (token) => token.hashedToken == hashedToken,
        )
        if (resume) {
          const tokenExpires = Accounts._tokenExpiration(resume.when)
          if (new Date() <= tokenExpires) {
            // update resume when
            Meteor.users.updateAsync(
              { _id: user._id, 'services.resume.loginTokens': resume },
              {
                $set: {
                  'services.resume.loginTokens.$': {
                    hashedToken: resume.hashedToken,
                    when: new Date(),
                  },
                },
              },
            )
            req.userId = user._id
          } else {
            // token has expired so remove it
            Meteor.users.updateAsync(
              { _id: user._id },
              { $pull: { 'services.resume.loginTokens': resume } },
            )
          }
        }
      }
    }
    next()
  }

  return [bearerTokenMiddleware, authMiddleware]
}

This approach will remove expired tokens but you might want to combine this with a periodic calling of Accounts._expireTokens() to remove those that are created and then forgotten. Im a little unsure of the consequences of asynchronously updating the user document like this but abstractly it seems sensible.

I found this later thread Simple:rest Meteor 3.x useful for more tips of what other people had done.

1 Like

That update looks great! Thanks @mattsouth !

Im a little unsure of the consequences of asynchronously updating the user document like this but abstractly it seems sensible

I reckon the only issue is that if you get an unhandled rejected promise (maybe if the DB connection had a momentary glitch?) you could accidentally risk crashing the whole server now that we’re on a new version of Node for Meteor 3?

Haven’t seen it in practice before but in that case you might just want to log an error to start with.

Meteor.users.updateAsync({
   ...
}).catch(e => console.error('Error removing expired token from user:`, e)

My only other thought is that if you wanted long-lived API Tokens as a separate auth mechanism (e.g. stored in a different collection etc), then that might require a bit of refactoring because someone might want to use bearer tokens for that.

But that’d basically be duplicating what Meteor does for login tokens or passwords auth, so maybe a separate topic. Might be fun to explore in a new package.

1 Like