diff --git a/README.md b/README.md index 3f7286d..4df0b9d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Metabase Node.js interactive embedding sample -Sample code for the Metabase Node.js Interactive Embedding Quickstart LINK. +This repo includes sample code referenced in the [quick start guide](https://www.metabase.com/learn/customer-facing-analytics/interactive-embedding-quick-start) for setting up interactive embedding with JWT. -You'll need a paid version of Metabase up and running. If you're not sure where to start, sign up for Metabase Cloud: Pro. LINK. +You'll need a Pro or Enterprise version of Metabase up and running. If you're not sure where to start, sign up for [Pro Cloud](https://www.metabase.com/pricing). ## Set up your Metabase @@ -88,20 +88,9 @@ user: rene@example.com password: foobar ``` -## Set up groups +## Set up groups and data sandboxing -TODO - -Create groups `Customer Acme` and `Customer Fake` and configure permissions so they can access the collection in which the dashboard is located. Also, setup data sandboxing on the Invoices table filtering on `accountId`. - -Under SSO then activate group membership syncing and map `Customer-Acme` and `Customer-Fake` to the groups you've created. -8. You should be able to sign in with the two users and see the dashboard. If not, check the collection permissions for their respective groups. - -## Set up sandboxing - -TODO - -Both users should be able to see the same dashboard but with different data, beacuse of sandboxing. +Check out our [quick start guide](https://www.metabase.com/learn/customer-facing-analytics/interactive-embedding-quick-start) to set up interactive embedding with JWT and data sandboxing. ## Reporting issues diff --git a/index.js b/index.js index 0d33656..bee0f8f 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,13 @@ "use strict"; const METABASE_SITE_URL = - process.env.METABASE_SITE_URL || "http://localhost:3000"; + process.env.METABASE_SITE_URL || "http://localhost:3000"; const METABASE_JWT_SHARED_SECRET = - process.env.METABASE_JWT_SHARED_SECRET || - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; -const mods = 'logo=false&top_nav=false&search=false&new_button=false&side_nav=false&header=false&additional_info=false&breadcrumbs=false&action_buttons=false' + process.env.METABASE_JWT_SHARED_SECRET || + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + +const mods = "logo=false"; + /** * Module dependencies. */ @@ -28,173 +30,173 @@ app.set("views", path.join(__dirname, "views")); app.use(express.urlencoded({ extended: false })); app.use( - session({ - resave: false, // don't save session if unmodified - saveUninitialized: false, // don't create session until something stored - secret: "shhhh, very secret", - }) + session({ + resave: false, // don't save session if unmodified + saveUninitialized: false, // don't create session until something stored + secret: "shhhh, very secret", + }) ); // Session-persisted message middleware app.use(function (req, res, next) { - var err = req.session.error; - var msg = req.session.success; - delete req.session.error; - delete req.session.success; - res.locals.message = ""; - if (err) res.locals.message = '

' + err + "

"; - if (msg) res.locals.message = '

' + msg + "

"; - next(); + var err = req.session.error; + var msg = req.session.success; + delete req.session.error; + delete req.session.success; + res.locals.message = ""; + if (err) res.locals.message = '

' + err + "

"; + if (msg) res.locals.message = '

' + msg + "

"; + next(); }); // dummy database var users = [ - { - firstName: "Rene", - lastName: "Mueller", - email: "rene@example.com", - accountId: 28, - accountName: "Customer-Acme", - }, - { - firstName: "Cecilia", - lastName: "Stark", - email: "cecilia@example.com", - accountId: 132, - accountName: "Customer-Fake", - }, + { + firstName: "Rene", + lastName: "Mueller", + email: "rene@example.com", + accountId: 28, + accountName: "Customer-Acme", + }, + { + firstName: "Cecilia", + lastName: "Stark", + email: "cecilia@example.com", + accountId: 132, + accountName: "Customer-Fake", + }, ]; // when you create a user, generate a salt // and hash the password ('foobar' is the pass here) hash({ password: "foobar" }, function (err, pass, salt, hash) { - if (err) throw err; - // store the salt & hash in the "db" - users.forEach((element) => { - element.salt = salt; - element.hash = hash; - }); + if (err) throw err; + // store the salt & hash in the "db" + users.forEach((element) => { + element.salt = salt; + element.hash = hash; + }); }); function findUserbyEmail(email) { - var u = users.find((u) => u.email === email); - return u; + var u = users.find((u) => u.email === email); + return u; } // Authenticate using our plain-object database of doom! function authenticate(email, pass, fn) { - if (!module.parent) console.log("authenticating %s:%s", email, pass); - var user = findUserbyEmail(email); - // query the db for the given email - if (!user) return fn(null, null); - // apply the same algorithm to the POSTed password, applying - // the hash against the pass / salt, if there is a match we - // found the user - hash({ password: pass, salt: user.salt }, function (err, pass, salt, hash) { - if (err) return fn(err); - if (hash === user.hash) return fn(null, user); - fn(null, null); - }); + if (!module.parent) console.log("authenticating %s:%s", email, pass); + var user = findUserbyEmail(email); + // query the db for the given email + if (!user) return fn(null, null); + // apply the same algorithm to the POSTed password, applying + // the hash against the pass / salt, if there is a match we + // found the user + hash({ password: pass, salt: user.salt }, function (err, pass, salt, hash) { + if (err) return fn(err); + if (hash === user.hash) return fn(null, user); + fn(null, null); + }); } function restrict(req, res, next) { - if (req.session.user) { - next(); - } else { - req.session.returnTo = req.originalUrl; - req.session.error = "Access denied!"; - res.redirect("/login"); - } + if (req.session.user) { + next(); + } else { + req.session.returnTo = req.originalUrl; + req.session.error = "Access denied!"; + res.redirect("/login"); + } } const signUserToken = (user) => - jwt.sign( - { - email: user.email, - first_name: user.firstName, - last_name: user.lastName, - account_id: user.accountId, - groups: [user.accountName], - exp: Math.round(Date.now() / 1000) + 60 * 10, // 10 minute expiration - }, - METABASE_JWT_SHARED_SECRET - ); + jwt.sign( + { + email: user.email, + first_name: user.firstName, + last_name: user.lastName, + account_id: user.accountId, + groups: [user.accountName], + exp: Math.round(Date.now() / 1000) + 60 * 10, // 10 minute expiration + }, + METABASE_JWT_SHARED_SECRET + ); app.get("/", function (req, res) { - res.redirect("/analytics"); + res.redirect("/analytics"); }); app.get("/analytics", restrict, function (req, res) { - // replace ID "1" with the ID number in the path of your dashboard in Metabase. - const METABASE_DASHBOARD_PATH = "/dashboard/1"; - var iframeUrl = `/sso/metabase?return_to=${METABASE_DASHBOARD_PATH}`; - res.send( - `` - ); + // replace ID "1" with the ID number in the path of your dashboard in Metabase. + const METABASE_DASHBOARD_PATH = "/dashboard/1"; + var iframeUrl = `/sso/metabase?return_to=${METABASE_DASHBOARD_PATH}`; + res.send( + `` + ); }); app.get("/logout", function (req, res) { - // destroy the user's session to log them out - // will be re-created next request - req.session.destroy(function () { - res.redirect("/"); - }); + // destroy the user's session to log them out + // will be re-created next request + req.session.destroy(function () { + res.redirect("/"); + }); }); app.get("/login", function (req, res) { - res.render("login"); + res.render("login"); }); app.post("/login", function (req, res, next) { - authenticate(req.body.email, req.body.password, function (err, user) { - if (err) return next(err); - if (user) { - // Regenerate session when signing in - // to prevent fixation - var returnTo = req.session.returnTo; - req.session.regenerate(function () { - // Store the user's primary key - // in the session store to be retrieved, - // or in this case the entire user object - req.session.user = user; - req.session.success = - "Authenticated as " + - user.firstName + - "" + - user.lastName + - ' click to logout. ' + - ' click to access analytics'; - res.redirect(returnTo || "/"); - delete req.session.returnTo; - }); - } else { - req.session.error = - "Authentication failed, please check your " + - " email and password." + - ' (use "rene@example.com" or "cecilia@example.com" and password "foobar")'; - res.redirect("/login"); - } - }); + authenticate(req.body.email, req.body.password, function (err, user) { + if (err) return next(err); + if (user) { + // Regenerate session when signing in + // to prevent fixation + var returnTo = req.session.returnTo; + req.session.regenerate(function () { + // Store the user's primary key + // in the session store to be retrieved, + // or in this case the entire user object + req.session.user = user; + req.session.success = + "Authenticated as " + + user.firstName + + "" + + user.lastName + + ' click to logout. ' + + ' click to access analytics'; + res.redirect(returnTo || "/"); + delete req.session.returnTo; + }); + } else { + req.session.error = + "Authentication failed, please check your " + + " email and password." + + ' (use "rene@example.com" or "cecilia@example.com" and password "foobar")'; + res.redirect("/login"); + } + }); }); app.get("/sso/metabase", restrict, (req, res) => { - res.redirect( - url.format({ - pathname: `${METABASE_SITE_URL}/auth/sso`, - query: { - jwt: signUserToken(req.session.user), - return_to: `${req.query.return_to || "/"}?${mods}`, - }, - }) - ); + res.redirect( + url.format({ + pathname: `${METABASE_SITE_URL}/auth/sso`, + query: { + jwt: signUserToken(req.session.user), + return_to: `${req.query.return_to || "/"}?${mods}`, + }, + }) + ); }); const PORT = 8080; if (!module.parent) { - app.listen(PORT); - console.log(`Express started serving on port ${PORT}`); + app.listen(PORT); + console.log(`Express started serving on port ${PORT}`); }