add vuejs-auth0-graphql sample app (#1898)

This is a sample Vue.js app with Auth0 integration. Has a simple schema with users and article tables with permissions setup. Once the user logs in to the app, the articles written by the user would be shown.
This commit is contained in:
Praveen Durairaj 2019-03-29 12:22:59 +05:30 committed by Shahidh K Muhammed
parent ca7d8b3df5
commit 8e78e27707
47 changed files with 13123 additions and 0 deletions

View File

@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not ie <= 8

View File

@ -0,0 +1,2 @@
node_modules/
.git

View File

@ -0,0 +1,36 @@
FROM node:10-alpine as build
RUN apk update && apk upgrade && \
apk add --no-cache bash git openssh
RUN mkdir /app
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
# ---------------
FROM node:10-alpine
RUN mkdir -p /app/dist
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json .
COPY --from=build /app/server.js .
ENV NODE_ENV production
RUN npm install --production
EXPOSE 3000
CMD ["node", "server.js"]

View File

@ -0,0 +1,114 @@
# vuejs-auth0-graphql
This sample Vue.js app demonstrates:
- Logging in to Auth0 using Redirect Mode
- Making an authenticated graphql query fetching articles written by the logged in user
- Accessing profile information that has been provided in the ID token
- Gated content. The `/profile` route is not accessible without having first logged in
## Integrating Vue App with Auth0 and JWT authorization with Hasura GraphQL Engine
In this example, we use Hasura GraphQL engine's JWT authorization mode. We use
Auth0 as our authentication and JWT token provider.
## Create an application in Auth0
1. Create an application in Auth0 dashboard
2. In the settings of the application, add `http://localhost:3000/callback` as
"Allowed Callback URLs" and `http://localhost:3000` as "Allowed Web Origins"
## Add rules for custom JWT claims
In the Auth0 dashboard, navigate to "Rules". Add the following rules to add our custom JWT claims:
```javascript
function (user, context, callback) {
const namespace = "https://hasura.io/jwt/claims";
context.idToken[namespace] =
{
'x-hasura-default-role': 'user',
// do some custom logic to decide allowed roles
'x-hasura-allowed-roles': user.email === 'admin@foobar.com' ? ['user', 'admin'] : ['user'],
'x-hasura-user-id': user.user_id
};
callback(null, user, context);
}
```
## Get your JWT signing certificate
Head to [https://hasura.io/jwt-config](https://hasura.io/jwt-config) and generate the config for your auth0 domain.
## Deploy Hasura GraphQL Engine
[![Deploy HGE on heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hasura/graphql-engine-heroku)
After deploying, add the following environment variables to configure JWT mode:
```
HASURA_GRAPHQL_ADMIN_SECRET: youradminsecretkey
```
```
HASURA_GRAPHQL_JWT_SECRET: {"type":"RS256", "key": "<the-certificate-data-in-one-line>"}
```
For example, (copy the certificate from above step or use generated config from https://hasura.io/jwt-config):
```
HASURA_GRAPHQL_JWT_SECRET: {"type":"RS256", "key": "-----BEGIN CERTIFICATE-----\nMIIDDTCCAfWgAwIBAgIJPhNlZ11IDrxbMA0GCSqGSIb3DQEBCQxIjAgNV\nBAMTGXRlc3QtaGdlLWp3dC5ldS5hdXRoMC5jb20wHhcNMTgwNzMwMTM1MjM1WhcN\nMzIwNDA3MTM1MjM1WjAkMSIwIAYDVQQDExl0ZXN0LWhnZS1qd3QuZXUuYXV0aDAu\nY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA13CivdSkNzRnOnR5iReDb+AgbL7BWjRiw3tRwjxRp5PYzvAGuj94y+R6LRh3QybYtsMFbSg5J7fNq6\nLd6yMpRMrUu8CBOnYY45D6b/2jlf+Vp8vEQuKvPMOOw8Ev6x7X3blcuXCELSwyL3\nAGHq9OpP2RV6V6CIE863IzzuYH5HDLzU35oMZqozgJVRJM0+6besH6TnSTNiA7xi\nBAqFaiQRNQRVi1CAUa0bLkN1XRp4AFy7d63VldO9sM+8QnCNHySdDr1XevVuq6DK\nLQyGexFFy4niALgHV0Q7QA+xP1c2G6rJomZmn4jl1avnlBpU87E58JMrRHOCj+5m\nXj22AQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBT6FvNkuUgu\YQ/i4lo5aOgwazAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEB\nADCLj+/L22pEKyqaIUlhHUJh7DAiDSLafy0fw56UCntzPhqiZVVRlhxeAKidkCLVIEbRLuxUoXiQSezPqMp//9xHegMp0f2VauVCFbg7EpUanYwvqFqjy9LWgH+SBz\n4uroLSYZ5g1EPsHtlArLRChA90caTX4e7Z7Xlu8vG2kHRJB5nC7ycdbMUvEWBMeI\ntn/pcb4mZ3/vlgj4UTEnCURe2UPmSJpxmPwXqBctvwdKHRMgFXhZxojWCi0z4ftf\nf8t8UJSIcbEblnkYe7wzRYy8tOXoMMHqGSisCdkWp/866029rJsKbwd8rVIyKNC5\nfrGYawv+0cxO6/Sir0meA=\n-----END CERTIFICATE-----"}
```
Save changes.
## Configure the Auth0 Application
The project needs to be configured with your Auth0 domain and client ID in order for the authentication flow to work.
To do this, open `auth_config.json`, and replace the values within with your own Auth0 application credentials:
```json
{
"domain": "<YOUR AUTH0 DOMAIN>",
"clientId": "<YOUR AUTH0 CLIENT ID>"
}
```
## Create the initial tables
1. Add your database URL and admin secret in `hasura/config.yaml`
```yaml
endpoint: https://<hge-heroku-url>
admin_secret: <your-admin-secret>
```
2. Run `hasura migrate apply` inside `hasura` directory to create the required tables and permissions for the app
## Create Auth0 Rule
Everytime user signups on Auth0, we need to sync that user into our postgres database. This is done using Auth0 rules. Create a Rule and insert the following code:
```
function (user, context, callback) {
const userId = user.user_id;
const nickname = user.nickname;
request.post({
headers: {'content-type' : 'application/json', 'x-hasura-admin-secret': '<your-admin-secret>'},
url: 'http://myapp.herokuapp.com/v1alpha1/graphql',
body: `{\"query\":\"mutation($userId: String!, $nickname: String) {\\n insert_users(\\n objects: [{ auth0_id: $userId, name: $nickname }]\\n on_conflict: {\\n constraint: users_pkey\\n update_columns: [last_seen, name]\\n }\\n ) {\\n affected_rows\\n }\\n }\",\"variables\":{\"userId\":\"${userId}\",\"nickname\":\"${nickname}\"}}`
}, function(error, response, body){
console.log(body);
callback(null, user, context);
});
}
```
## Run the application
`npm install && npm run serve`
> The app runs on port 3000 by default. You can change the port number, but you will also have to reconfigure the callback

View File

@ -0,0 +1,4 @@
{
"domain": "",
"clientId": ""
}

View File

@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/app"]
};

View File

@ -0,0 +1,2 @@
docker build --rm -t auth0-vue-01-login .
docker run -p 3000:3000 --pid=host auth0-vue-01-login

View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
docker build --rm -t auth0-vue-01-login .
docker run -p 3000:3000 --pid=host auth0-vue-01-login

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg width='120px' height='120px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-ring"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><defs><filter id="uil-ring-shadow" x="-100%" y="-100%" width="300%" height="300%"><feOffset result="offOut" in="SourceGraphic" dx="0" dy="0"></feOffset><feGaussianBlur result="blurOut" in="offOut" stdDeviation="0"></feGaussianBlur><feBlend in="SourceGraphic" in2="blurOut" mode="normal"></feBlend></filter></defs><path d="M10,50c0,0,0,0.5,0.1,1.4c0,0.5,0.1,1,0.2,1.7c0,0.3,0.1,0.7,0.1,1.1c0.1,0.4,0.1,0.8,0.2,1.2c0.2,0.8,0.3,1.8,0.5,2.8 c0.3,1,0.6,2.1,0.9,3.2c0.3,1.1,0.9,2.3,1.4,3.5c0.5,1.2,1.2,2.4,1.8,3.7c0.3,0.6,0.8,1.2,1.2,1.9c0.4,0.6,0.8,1.3,1.3,1.9 c1,1.2,1.9,2.6,3.1,3.7c2.2,2.5,5,4.7,7.9,6.7c3,2,6.5,3.4,10.1,4.6c3.6,1.1,7.5,1.5,11.2,1.6c4-0.1,7.7-0.6,11.3-1.6 c3.6-1.2,7-2.6,10-4.6c3-2,5.8-4.2,7.9-6.7c1.2-1.2,2.1-2.5,3.1-3.7c0.5-0.6,0.9-1.3,1.3-1.9c0.4-0.6,0.8-1.3,1.2-1.9 c0.6-1.3,1.3-2.5,1.8-3.7c0.5-1.2,1-2.4,1.4-3.5c0.3-1.1,0.6-2.2,0.9-3.2c0.2-1,0.4-1.9,0.5-2.8c0.1-0.4,0.1-0.8,0.2-1.2 c0-0.4,0.1-0.7,0.1-1.1c0.1-0.7,0.1-1.2,0.2-1.7C90,50.5,90,50,90,50s0,0.5,0,1.4c0,0.5,0,1,0,1.7c0,0.3,0,0.7,0,1.1 c0,0.4-0.1,0.8-0.1,1.2c-0.1,0.9-0.2,1.8-0.4,2.8c-0.2,1-0.5,2.1-0.7,3.3c-0.3,1.2-0.8,2.4-1.2,3.7c-0.2,0.7-0.5,1.3-0.8,1.9 c-0.3,0.7-0.6,1.3-0.9,2c-0.3,0.7-0.7,1.3-1.1,2c-0.4,0.7-0.7,1.4-1.2,2c-1,1.3-1.9,2.7-3.1,4c-2.2,2.7-5,5-8.1,7.1 c-0.8,0.5-1.6,1-2.4,1.5c-0.8,0.5-1.7,0.9-2.6,1.3L66,87.7l-1.4,0.5c-0.9,0.3-1.8,0.7-2.8,1c-3.8,1.1-7.9,1.7-11.8,1.8L47,90.8 c-1,0-2-0.2-3-0.3l-1.5-0.2l-0.7-0.1L41.1,90c-1-0.3-1.9-0.5-2.9-0.7c-0.9-0.3-1.9-0.7-2.8-1L34,87.7l-1.3-0.6 c-0.9-0.4-1.8-0.8-2.6-1.3c-0.8-0.5-1.6-1-2.4-1.5c-3.1-2.1-5.9-4.5-8.1-7.1c-1.2-1.2-2.1-2.7-3.1-4c-0.5-0.6-0.8-1.4-1.2-2 c-0.4-0.7-0.8-1.3-1.1-2c-0.3-0.7-0.6-1.3-0.9-2c-0.3-0.7-0.6-1.3-0.8-1.9c-0.4-1.3-0.9-2.5-1.2-3.7c-0.3-1.2-0.5-2.3-0.7-3.3 c-0.2-1-0.3-2-0.4-2.8c-0.1-0.4-0.1-0.8-0.1-1.2c0-0.4,0-0.7,0-1.1c0-0.7,0-1.2,0-1.7C10,50.5,10,50,10,50z" fill="#337ab7" filter="url(#uil-ring-shadow)"><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" repeatCount="indefinite" dur="1s"></animateTransform></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
{
"name": "vuejs-auth0-graphql",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"express": "^4.16.4",
"morgan": "^1.9.1",
"vue-apollo": "^3.0.0-beta.11"
},
"devDependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.14",
"@fortawesome/free-solid-svg-icons": "^5.7.1",
"@fortawesome/vue-fontawesome": "^0.1.5",
"@vue/cli-plugin-babel": "^3.4.0",
"@vue/cli-plugin-eslint": "^3.4.0",
"@vue/cli-service": "^3.4.0",
"@vue/eslint-config-prettier": "^3.0.5",
"auth0-js": "^9.10.0",
"babel-eslint": "^10.0.1",
"bootstrap": "^4.2.1",
"graphql-tag": "^2.9.0",
"highlight.js": "^9.14.2",
"jquery": "^3.3.1",
"node-sass": "^4.11.0",
"npm-run-all": "^4.1.5",
"popper.js": "^1.14.7",
"samples-bootstrap-theme": "github:auth0-quickstarts/samples-bootstrap-theme",
"sass-loader": "^7.1.0",
"vue": "^2.6.2",
"vue-cli-plugin-apollo": "^0.19.2",
"vue-router": "^3.0.2",
"vue-template-compiler": "^2.6.2"
},
"eslintConfig": {
"root": true,
"env": {
"node": true,
"browser": true
},
"plugins": [
"vue"
],
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {
"no-console": [
"error",
{
"allow": [
"error",
"warn"
]
}
]
},
"parserOptions": {
"parser": "babel-eslint"
}
},
"prettier": {
"singleQuote": false,
"semi": true
}
}

View File

@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Vue.js Auth0 Authentication with Hasura GraphQL Engine</title>
</head>
<body>
<noscript>
<strong>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,14 @@
/* eslint-disable no-console */
const express = require("express");
const { join } = require("path");
const morgan = require("morgan");
const app = express();
app.use(morgan("dev"));
app.use(express.static(join(__dirname, "dist")));
app.use((_, res) => {
res.sendFile(join(__dirname, "dist", "index.html"));
});
app.listen(3000, () => console.log("Listening on port 3000"));

View File

@ -0,0 +1,30 @@
<template>
<div id="app">
<nav-bar/>
<div class="container mt-5">
<router-view/>
</div>
</div>
</template>
<script>
import "jquery";
import "samples-bootstrap-theme";
import "samples-bootstrap-theme/dist/css/auth0-theme.css";
import NavBar from "./components/NavBar";
export default {
components: {
NavBar
},
async created() {
try {
await this.$auth.renewTokens();
} catch {
// Supress the 'not logged in' error as we can illegitimately get that
// when processing the callback url
}
}
};
</script>

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg width='120px' height='120px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-ring"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><defs><filter id="uil-ring-shadow" x="-100%" y="-100%" width="300%" height="300%"><feOffset result="offOut" in="SourceGraphic" dx="0" dy="0"></feOffset><feGaussianBlur result="blurOut" in="offOut" stdDeviation="0"></feGaussianBlur><feBlend in="SourceGraphic" in2="blurOut" mode="normal"></feBlend></filter></defs><path d="M10,50c0,0,0,0.5,0.1,1.4c0,0.5,0.1,1,0.2,1.7c0,0.3,0.1,0.7,0.1,1.1c0.1,0.4,0.1,0.8,0.2,1.2c0.2,0.8,0.3,1.8,0.5,2.8 c0.3,1,0.6,2.1,0.9,3.2c0.3,1.1,0.9,2.3,1.4,3.5c0.5,1.2,1.2,2.4,1.8,3.7c0.3,0.6,0.8,1.2,1.2,1.9c0.4,0.6,0.8,1.3,1.3,1.9 c1,1.2,1.9,2.6,3.1,3.7c2.2,2.5,5,4.7,7.9,6.7c3,2,6.5,3.4,10.1,4.6c3.6,1.1,7.5,1.5,11.2,1.6c4-0.1,7.7-0.6,11.3-1.6 c3.6-1.2,7-2.6,10-4.6c3-2,5.8-4.2,7.9-6.7c1.2-1.2,2.1-2.5,3.1-3.7c0.5-0.6,0.9-1.3,1.3-1.9c0.4-0.6,0.8-1.3,1.2-1.9 c0.6-1.3,1.3-2.5,1.8-3.7c0.5-1.2,1-2.4,1.4-3.5c0.3-1.1,0.6-2.2,0.9-3.2c0.2-1,0.4-1.9,0.5-2.8c0.1-0.4,0.1-0.8,0.2-1.2 c0-0.4,0.1-0.7,0.1-1.1c0.1-0.7,0.1-1.2,0.2-1.7C90,50.5,90,50,90,50s0,0.5,0,1.4c0,0.5,0,1,0,1.7c0,0.3,0,0.7,0,1.1 c0,0.4-0.1,0.8-0.1,1.2c-0.1,0.9-0.2,1.8-0.4,2.8c-0.2,1-0.5,2.1-0.7,3.3c-0.3,1.2-0.8,2.4-1.2,3.7c-0.2,0.7-0.5,1.3-0.8,1.9 c-0.3,0.7-0.6,1.3-0.9,2c-0.3,0.7-0.7,1.3-1.1,2c-0.4,0.7-0.7,1.4-1.2,2c-1,1.3-1.9,2.7-3.1,4c-2.2,2.7-5,5-8.1,7.1 c-0.8,0.5-1.6,1-2.4,1.5c-0.8,0.5-1.7,0.9-2.6,1.3L66,87.7l-1.4,0.5c-0.9,0.3-1.8,0.7-2.8,1c-3.8,1.1-7.9,1.7-11.8,1.8L47,90.8 c-1,0-2-0.2-3-0.3l-1.5-0.2l-0.7-0.1L41.1,90c-1-0.3-1.9-0.5-2.9-0.7c-0.9-0.3-1.9-0.7-2.8-1L34,87.7l-1.3-0.6 c-0.9-0.4-1.8-0.8-2.6-1.3c-0.8-0.5-1.6-1-2.4-1.5c-3.1-2.1-5.9-4.5-8.1-7.1c-1.2-1.2-2.1-2.7-3.1-4c-0.5-0.6-0.8-1.4-1.2-2 c-0.4-0.7-0.8-1.3-1.1-2c-0.3-0.7-0.6-1.3-0.9-2c-0.3-0.7-0.6-1.3-0.8-1.9c-0.4-1.3-0.9-2.5-1.2-3.7c-0.3-1.2-0.5-2.3-0.7-3.3 c-0.2-1-0.3-2-0.4-2.8c-0.1-0.4-0.1-0.8-0.1-1.2c0-0.4,0-0.7,0-1.1c0-0.7,0-1.2,0-1.7C10,50.5,10,50,10,50z" fill="#337ab7" filter="url(#uil-ring-shadow)"><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" repeatCount="indefinite" dur="1s"></animateTransform></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,128 @@
/* eslint-disable */
import auth0 from "auth0-js";
import { EventEmitter } from "events";
import authConfig from "../../auth_config.json";
const webAuth = new auth0.WebAuth({
domain: authConfig.domain,
redirectUri: `${window.location.origin}/callback`,
clientID: authConfig.clientId,
responseType: "token id_token",
scope: "openid profile"
});
const localStorageKey = "loggedIn";
const loginEvent = "loginEvent";
class AuthService extends EventEmitter {
idToken = null;
profile = null;
tokenExpiry = null;
login(customState) {
webAuth.authorize();
}
logOut() {
localStorage.removeItem(localStorageKey);
this.idToken = null;
this.tokenExpiry = null;
this.profile = null;
webAuth.logout({
returnTo: `${window.location.origin}`
});
this.emit(loginEvent, { loggedIn: false });
}
handleAuthentication() {
return new Promise((resolve, reject) => {
webAuth.parseHash((err, authResult) => {
if (err) {
reject(err);
} else {
this.localLogin(authResult);
resolve(authResult.idToken);
}
});
});
}
isAuthenticated() {
return (
Date.now() < this.tokenExpiry &&
localStorage.getItem(localStorageKey) === "true"
);
}
isIdTokenValid() {
return (
this.idToken &&
this.tokenExpiry &&
Date.now() < this.tokenExpiry
);
}
getIdToken() {
return new Promise((resolve, reject) => {
if (this.isIdTokenValid()) {
resolve(this.idToken);
} else if (this.isAuthenticated()) {
this.renewTokens().then(authResult => {
resolve(authResult.idToken);
}, reject);
} else {
resolve();
}
});
}
localLogin(authResult) {
console.log(authResult);
this.idToken = authResult.idToken;
this.profile = authResult.idTokenPayload;
// Convert the expiry time from seconds to milliseconds,
// required by the Date constructor
this.tokenExpiry = new Date(this.profile.exp * 1000);
localStorage.setItem(localStorageKey, "true");
localStorage.setItem("apollo-token", authResult.idToken);
this.emit(loginEvent, {
loggedIn: true,
profile: authResult.idTokenPayload,
state: authResult.appState || {}
});
}
renewTokens() {
return new Promise((resolve, reject) => {
console.log(localStorage.getItem(localStorageKey));
if (localStorage.getItem(localStorageKey) !== "true") {
return reject("Not logged in");
}
webAuth.checkSession({}, (err, authResult) => {
if (err) {
console.log('inside');
console.log(err);
localStorage.setItem(localStorageKey, "false");
localStorage.removeItem("apollo-token");
reject(err);
} else {
this.localLogin(authResult);
resolve(authResult);
}
});
});
}
}
const service = new AuthService();
service.setMaxListeners(5);
export default service;

View File

@ -0,0 +1,34 @@
<template>
<div class="spinner">
<img src="../assets/loading.svg" alt="Loading">
</div>
</template>
<script>
export default {
methods: {
handleLoginEvent(data) {
// this.$router.push(data.state.target || "/");
window.location.href = data.state.target || "/";
}
},
created() {
this.$auth.handleAuthentication();
}
};
</script>
<style scoped>
.spinner {
position: absolute;
display: flex;
justify-content: center;
height: 100vh;
width: 100vw;
background-color: white;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<div class="nav-container">
<nav class="navbar navbar-expand-md navbar-light bg-light">
<div class="container">
<div class="navbar-brand"></div>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<router-link to="/" class="nav-link">Home</router-link>
</li>
</ul>
<ul class="navbar-nav d-none d-md-block">
<li v-if="!isAuthenticated" class="nav-item">
<button
id="qsLoginBtn"
class="btn btn-primary btn-margin"
@click.prevent="login"
>Login</button>
</li>
<li class="nav-item dropdown" v-if="isAuthenticated">
<a
class="nav-link dropdown-toggle"
href="#"
id="profileDropDown"
data-toggle="dropdown"
>
<img :src="profile.picture" alt="User's profile picture" class="nav-user-profile">
</a>
<div class="dropdown-menu dropdown-menu-right">
<div class="dropdown-header">{{ profile.name }}</div>
<router-link to="/profile" class="dropdown-item dropdown-profile">
<span class="icon icon-profile"></span> Profile
</router-link>
<a id="qsLogoutBtn" href="#" class="dropdown-item" @click.prevent="logout">
<span class="icon icon-power"></span> Log out
</a>
</div>
</li>
</ul>
<ul class="navbar-nav d-md-none" v-if="!isAuthenticated">
<button class="btn btn-primary btn-block" @click="login">Log in</button>
</ul>
<ul class="navbar-nav d-md-none" v-if="isAuthenticated">
<li class="nav-item">
<span class="user-info">
<img :src="profile.picture" alt="User's profile picture" class="nav-user-profile d-inline-block">
<h6 class="d-inline-block">{{ profile.name }}</h6>
</span>
</li>
<li>
<span class="icon icon-profile"></span>
<router-link to="/profile">Profile</router-link>
</li>
<li>
<span class="icon icon-power"></span>
<a id="qsLogoutBtn" href="#" class @click.prevent="logout">Log out</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
</template>
<script>
export default {
name: "NavBar",
beforeCreate() {
this.$auth.renewTokens();
},
methods: {
login() {
this.$auth.login();
},
logout() {
this.$auth.logOut();
this.$router.push({ path: "/" });
},
handleLoginEvent(data) {
this.isAuthenticated = data.loggedIn;
this.profile = data.profile;
}
},
data() {
return {
isAuthenticated: false,
profile: {}
};
}
};
</script>

View File

@ -0,0 +1,31 @@
import hljs from "highlight.js/lib/highlight";
import json from "highlight.js/lib/languages/json";
import "highlight.js/styles/monokai-sublime.css";
hljs.registerLanguage("json", json);
export default {
deep: true,
bind: function(el, binding) {
// on first bind, highlight all targets
let targets = el.querySelectorAll("code");
targets.forEach(target => {
// if a value is directly assigned to the directive, use this
// instead of the element content.
if (binding.value) {
target.textContent = binding.value;
}
hljs.highlightBlock(target);
});
},
componentUpdated: function(el, binding) {
// after an update, re-fill the content and then highlight
let targets = el.querySelectorAll("code");
targets.forEach(target => {
if (binding.value) {
target.textContent = binding.value;
hljs.highlightBlock(target);
}
});
}
};

View File

@ -0,0 +1,25 @@
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import AuthPlugin from "./plugins/auth";
import HighlightJs from "./directives/highlight";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faLink } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { createProvider } from './vue-apollo'
Vue.use(AuthPlugin);
Vue.directive("highlightjs", HighlightJs);
Vue.config.productionTip = false;
library.add(faLink);
Vue.component("font-awesome-icon", FontAwesomeIcon);
new Vue({
router,
apolloProvider: createProvider(),
render: h => h(App)
}).$mount("#app");

View File

@ -0,0 +1,21 @@
import authService from "../auth/authService";
export default {
install(Vue) {
Vue.prototype.$auth = authService;
Vue.mixin({
created() {
if (this.handleLoginEvent) {
authService.addListener("loginEvent", this.handleLoginEvent);
}
},
destroyed() {
if (this.handleLoginEvent) {
authService.removeListener("loginEvent", this.handleLoginEvent);
}
}
});
}
};

View File

@ -0,0 +1,40 @@
import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import Profile from "./views/Profile.vue";
import Callback from "./components/Callback.vue";
import auth from "./auth/authService";
Vue.use(Router);
const router = new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/profile",
name: "profile",
component: Profile
},
{
path: "/callback",
name: "callback",
component: Callback
}
]
});
router.beforeEach((to, from, next) => {
if (to.path === "/" || to.path === "/callback" || auth.isAuthenticated()) {
return next();
}
auth.login({ target: to.path });
});
export default router;

View File

@ -0,0 +1,86 @@
<template>
<div>
<div class="spinner" v-if="isLoading">
<img src="../assets/loading.svg" alt="Loading">
</div>
<div class="text-center hero" v-if="!isLoading">
<img class="mb-3 app-logo" src="/logo.png" alt="Vue.js logo">
<div v-if="!isAuthenticated">
<h1 class="mb-4">
Login to view articles
</h1>
<p class="lead">
This is a sample application that demonstrates an authentication flow for an SPA, using
<a
href="https://vuejs.org"
>Vue.js</a> and making a authenticated GraphQL query to <a href="https://github.com/hasura/graphql-engine">Hasura GraphQL Engine</a>
</p>
</div>
<div v-if="isAuthenticated">
<h1 class="mb-4">
Articles written by me
</h1>
<div v-for="a in article" :key="a.id">
{{a.id}}. {{ a.title }}
</div>
</div>
</div>
</div>
</template>
<script>
import gql from 'graphql-tag'
export default {
name: "home",
methods: {
handleLoginEvent(data) {
this.isAuthenticated = data.loggedIn;
this.isLoading = false;
}
},
beforeCreate() {
this.isLoading = true;
},
mounted() {
this.isLoading = false;
},
data() {
return {
isAuthenticated: false,
isLoading: true
};
},
apollo: {
// Simple query that will update the 'article' vue property
article: gql`query {
article {
id
title
}
}`,
},
};
</script>
<style lang="scss" scoped>
.next-steps {
.fa-link {
margin-right: 5px;
}
}
.spinner {
position: absolute;
display: flex;
justify-content: center;
height: 100vh;
width: 100vw;
background-color: white;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<div class="container">
<div class="row align-items-center profile-header">
<div class="col-md-2">
<img :src="profile.picture" alt="User's profile picture" class="rounded-circle img-fluid profile-picture">
</div>
<div class="col-md">
<h2>{{ profile.name }}</h2>
<p class="lead text-muted">{{ profile.email }}</p>
</div>
</div>
<div class="row">
<pre v-highlightjs class="rounded"><code class="json">{{ JSON.stringify(profile, null, 2) }}</code></pre>
</div>
</div>
</template>
<script>
export default {
data() {
return {
profile: this.$auth.profile
};
},
methods: {
handleLoginEvent(data) {
this.profile = data.profile;
}
}
};
</script>

View File

@ -0,0 +1,117 @@
/* eslint-disable */
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'
import authService from './auth/authService'
// Install the vue plugin
Vue.use(VueApollo)
// Name of the localStorage item
const AUTH_TOKEN = 'apollo-token'
// Http endpoint
const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:8080/v1alpha1/graphql'
// Files URL root
export const filesRoot = process.env.VUE_APP_FILES_ROOT || httpEndpoint.substr(0, httpEndpoint.indexOf('/graphql'))
Vue.prototype.$filesRoot = filesRoot
// Config
const defaultOptions = {
// You can use `https` for secure connection (recommended in production)
httpEndpoint,
// You can use `wss` for secure connection (recommended in production)
// Use `null` to disable subscriptions
wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:8080/v1alpha1/graphql',
// LocalStorage token
tokenName: AUTH_TOKEN,
// Enable Automatic Query persisting with Apollo Engine
persisting: false,
// Use websockets for everything (no HTTP)
// You need to pass a `wsEndpoint` for this to work
websocketsOnly: false,
// Is being rendered on the server?
ssr: false,
// Override default apollo link
// note: don't override httpLink here, specify httpLink options in the
// httpLinkOptions property of defaultOptions.
// link: myLink
// Override default cache
// cache: myCache
// Override the way the Authorization header is set
// getAuth: (tokenName) => ...
// Additional ApolloClient options
// apollo: { ... }
// Client local data (see apollo-link-state)
// clientState: { resolvers: { ... }, defaults: { ... } }
getAuth: tokenName => {
// get the authentication token from local storage if it exists
// return the headers to the context so httpLink can read them
const token = localStorage.getItem('apollo-token')
if (token) {
return 'Bearer ' + token
} else {
return ''
}
},
}
// Call this in the Vue app file
export function createProvider (options = {}) {
// Create apollo client
const { apolloClient, wsClient } = createApolloClient({
...defaultOptions,
...options,
})
apolloClient.wsClient = wsClient
// Create vue apollo provider
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
defaultOptions: {
$query: {
// fetchPolicy: 'cache-and-network',
},
},
errorHandler (error) {
// eslint-disable-next-line no-console
console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
},
})
return apolloProvider
}
// Manually call this when user log in
export async function onLogin (apolloClient, token) {
if (typeof localStorage !== 'undefined' && token) {
localStorage.setItem(AUTH_TOKEN, token)
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
try {
await apolloClient.resetStore()
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (login)', 'color: orange;', e.message)
}
}
// Manually call this when user log out
export async function onLogout (apolloClient) {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(AUTH_TOKEN)
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
try {
await apolloClient.resetStore()
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (logout)', 'color: orange;', e.message)
}
}

View File

@ -0,0 +1,17 @@
const webpack = require("webpack");
module.exports = {
devServer: {
port: 3000
},
configureWebpack: {
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jquery: "jquery",
"window.jQuery": "jquery",
jQuery: "jquery"
})
]
}
};

View File

@ -0,0 +1 @@
endpoint: http://localhost:8070

View File

@ -0,0 +1,3 @@
- args:
sql: DROP TABLE "public"."users"
type: run_sql

View File

@ -0,0 +1,8 @@
- args:
sql: CREATE TABLE "public"."users"("auth0_id" text NOT NULL, "name" text NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("auth0_id") );
type: run_sql
- args:
name: users
schema: public
type: add_existing_table_or_view

View File

@ -0,0 +1,3 @@
- args:
sql: DROP TABLE "public"."article"
type: run_sql

View File

@ -0,0 +1,8 @@
- args:
sql: CREATE TABLE "public"."article"("id" serial NOT NULL, "title" text NOT NULL,
"user_id" text NOT NULL, PRIMARY KEY ("id") );
type: run_sql
- args:
name: article
schema: public
type: add_existing_table_or_view

View File

@ -0,0 +1,6 @@
- args:
role: user
table:
name: users
schema: public
type: drop_select_permission

View File

@ -0,0 +1,16 @@
- args:
permission:
allow_aggregations: false
columns:
- auth0_id
- name
- created_at
filter:
auth0_id:
_eq: X-Hasura-User-Id
limit: null
role: user
table:
name: users
schema: public
type: create_select_permission

View File

@ -0,0 +1,6 @@
- args:
role: user
table:
name: article
schema: public
type: drop_select_permission

View File

@ -0,0 +1,16 @@
- args:
permission:
allow_aggregations: false
columns:
- id
- title
- user_id
filter:
user_id:
_eq: X-Hasura-User-Id
limit: null
role: user
table:
name: article
schema: public
type: create_select_permission

View File

@ -0,0 +1,6 @@
- args:
role: user
table:
name: article
schema: public
type: drop_insert_permission

View File

@ -0,0 +1,16 @@
- args:
permission:
allow_upsert: true
check:
user_id:
_eq: X-Hasura-User-Id
columns:
- id
- title
- user_id
set: {}
role: user
table:
name: article
schema: public
type: create_insert_permission

View File

@ -0,0 +1,6 @@
- args:
role: user
table:
name: article
schema: public
type: drop_update_permission

View File

@ -0,0 +1,15 @@
- args:
permission:
columns:
- id
- title
- user_id
filter:
user_id:
_eq: X-Hasura-User-Id
set: {}
role: user
table:
name: article
schema: public
type: create_update_permission

View File

@ -0,0 +1,6 @@
- args:
role: user
table:
name: article
schema: public
type: drop_delete_permission

View File

@ -0,0 +1,10 @@
- args:
permission:
filter:
user_id:
_eq: X-Hasura-User-Id
role: user
table:
name: article
schema: public
type: create_delete_permission

View File

@ -0,0 +1,3 @@
- args:
sql: ALTER TABLE "public"."article" DROP CONSTRAINT "article_user_id_fkey"
type: run_sql

View File

@ -0,0 +1,4 @@
- args:
sql: ALTER TABLE "public"."article" ADD FOREIGN KEY ("user_id") REFERENCES "public"."users"
("auth0_id")
type: run_sql