-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ebd143d
commit 0bca5e5
Showing
2 changed files
with
128 additions
and
137 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: [email protected] | |
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 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = '<p class="msg error">' + err + "</p>"; | ||
if (msg) res.locals.message = '<p class="msg success">' + msg + "</p>"; | ||
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 = '<p class="msg error">' + err + "</p>"; | ||
if (msg) res.locals.message = '<p class="msg success">' + msg + "</p>"; | ||
next(); | ||
}); | ||
|
||
// dummy database | ||
|
||
var users = [ | ||
{ | ||
firstName: "Rene", | ||
lastName: "Mueller", | ||
email: "[email protected]", | ||
accountId: 28, | ||
accountName: "Customer-Acme", | ||
}, | ||
{ | ||
firstName: "Cecilia", | ||
lastName: "Stark", | ||
email: "[email protected]", | ||
accountId: 132, | ||
accountName: "Customer-Fake", | ||
}, | ||
{ | ||
firstName: "Rene", | ||
lastName: "Mueller", | ||
email: "[email protected]", | ||
accountId: 28, | ||
accountName: "Customer-Acme", | ||
}, | ||
{ | ||
firstName: "Cecilia", | ||
lastName: "Stark", | ||
email: "[email protected]", | ||
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( | ||
`<iframe src="${iframeUrl}" frameborder="0" width="1280" height="1000" allowtransparency></iframe>` | ||
); | ||
// 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( | ||
`<iframe src="${iframeUrl}" frameborder="0" width="1280" height="1000" allowtransparency></iframe>` | ||
); | ||
}); | ||
|
||
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 <a href="/logout">logout</a>. ' + | ||
' click to access <a href="/analytics">analytics</a>'; | ||
res.redirect(returnTo || "/"); | ||
delete req.session.returnTo; | ||
}); | ||
} else { | ||
req.session.error = | ||
"Authentication failed, please check your " + | ||
" email and password." + | ||
' (use "[email protected]" or "[email protected]" 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 <a href="/logout">logout</a>. ' + | ||
' click to access <a href="/analytics">analytics</a>'; | ||
res.redirect(returnTo || "/"); | ||
delete req.session.returnTo; | ||
}); | ||
} else { | ||
req.session.error = | ||
"Authentication failed, please check your " + | ||
" email and password." + | ||
' (use "[email protected]" or "[email protected]" 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}`); | ||
} |