├── client
├── containers
│ ├── DevTools.production.jsx
│ ├── index.js
│ ├── DevTools.jsx
│ ├── DevTools.development.jsx
│ ├── RequireAuthentication.jsx
│ ├── App.jsx
│ └── LogsContainer.jsx
├── components
│ ├── LogsDialog.css
│ ├── Header.css
│ ├── LogsDialog.jsx
│ ├── Header.jsx
│ └── LogsTable.jsx
├── actions
│ ├── index.js
│ ├── config.js
│ ├── log.js
│ └── auth.js
├── reducers
│ ├── index.js
│ ├── filter.js
│ ├── logs.js
│ ├── auth.js
│ └── config.js
├── showDevTools.jsx
├── utils
│ ├── createReducer.js
│ └── auth.js
├── app.jsx
├── middlewares
│ └── normalizeErrorMiddleware.js
├── store
│ └── configureStore.js
└── constants.js
├── server
├── lib
│ ├── config.js
│ ├── logger.js
│ └── processLogs.js
├── routes
│ ├── meta.js
│ ├── hooks.js
│ ├── index.js
│ └── html.js
├── config.sample.json
└── index.js
├── tests
├── mocha.js
└── lib
│ └── processLogs.tests.js
├── .babelrc
├── .editorconfig
├── README.md
├── webtask.js
├── index.js
├── .gitignore
├── LICENSE
├── .eslintrc
├── .github
└── PULL_REQUEST_TEMPLATE.md
├── webtask.json
└── package.json
/client/containers/DevTools.production.jsx:
--------------------------------------------------------------------------------
1 | export default () =>
;
2 |
--------------------------------------------------------------------------------
/server/lib/config.js:
--------------------------------------------------------------------------------
1 | module.exports = require('auth0-extension-tools').config();
2 |
--------------------------------------------------------------------------------
/tests/mocha.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | process.env.NODE_ENV = 'test';
3 |
--------------------------------------------------------------------------------
/client/components/LogsDialog.css:
--------------------------------------------------------------------------------
1 | .modal div.logs-dialog {
2 | width: 90%;
3 | max-width: 1024px;
4 | }
5 |
--------------------------------------------------------------------------------
/client/actions/index.js:
--------------------------------------------------------------------------------
1 | export * as authActions from './auth';
2 | export * as configActions from './config';
3 | export * as logActions from './log';
4 |
--------------------------------------------------------------------------------
/client/containers/index.js:
--------------------------------------------------------------------------------
1 | export App from './App.jsx';
2 | export LogsContainer from './LogsContainer.jsx';
3 | export RequireAuthentication from './RequireAuthentication.jsx';
4 |
--------------------------------------------------------------------------------
/client/containers/DevTools.jsx:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./DevTools.production');
3 | } else {
4 | module.exports = require('./DevTools.development');
5 | }
6 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "stage-0"
6 | ],
7 | "env": {
8 | "development": {
9 | "presets": ["react-hmre"]
10 | }
11 | },
12 | "plugins": [
13 | "transform-runtime"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/client/components/Header.css:
--------------------------------------------------------------------------------
1 | header.dashboard-header nav .navbar-brand {
2 | font-weight: normal;
3 | padding: 15px 15px;
4 | width: 100%;
5 | min-width: 400px;
6 | }
7 |
8 | .btn-nav span {
9 | font-size: 16px;
10 | top: 2px;
11 | position: relative;
12 | }
13 |
--------------------------------------------------------------------------------
/server/routes/meta.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const metadata = require('../../webtask.json');
3 |
4 | module.exports = () => {
5 | const api = express.Router();
6 | api.get('/', (req, res) => {
7 | res.status(200).send(metadata);
8 | });
9 |
10 | return api;
11 | };
12 |
--------------------------------------------------------------------------------
/client/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import { auth } from './auth';
4 | import { config } from './config';
5 | import { logs } from './logs';
6 | import { filter } from './filter';
7 |
8 | export default combineReducers({
9 | auth,
10 | config,
11 | logs,
12 | filter
13 | });
14 |
--------------------------------------------------------------------------------
/client/containers/DevTools.development.jsx:
--------------------------------------------------------------------------------
1 | import { createDevTools } from 'redux-devtools';
2 | import LogMonitor from 'redux-devtools-log-monitor';
3 | import DockMonitor from 'redux-devtools-dock-monitor';
4 |
5 | export default createDevTools(
6 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/client/reducers/filter.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 |
3 | import * as constants from '../constants';
4 | import createReducer from '../utils/createReducer';
5 |
6 | const initialState = {
7 | status: false
8 | };
9 |
10 | export const filter = createReducer(fromJS(initialState), { // eslint-disable-line import/prefer-default-export
11 | [constants.SET_FILTER]: (state, action) =>
12 | state.merge({
13 | status: action.payload.status
14 | })
15 | });
16 |
--------------------------------------------------------------------------------
/server/config.sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "EXTENSION_SECRET": "any string",
3 | "WT_URL": "http://localhost:3000",
4 | "PUBLIC_WT_URL": "http://localhost:3000",
5 | "AUTH0_DOMAIN": "tenant-name.eu.auth0.com",
6 | "AUTH0_RTA": "auth0.auth0.com",
7 | "AUTH0_CLIENT_ID": "AUTH0_CLIENT_ID",
8 | "AUTH0_CLIENT_SECRET": "AUTH0_CLIENT_SECRET",
9 | "BATCH_SIZE": 100,
10 | "WEBHOOK_URL": "http://localhost:8000",
11 | "SEND_AS_BATCH": true,
12 | "AUTH0_API_ENDPOINTS": "users,connections,whatever"
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 2
13 |
14 | # We recommend you to keep these unchanged
15 | end_of_line = lf
16 | charset = utf-8
17 | trim_trailing_whitespace = true
18 | insert_final_newline = true
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Auth0 Management API Webhooks
2 |
3 | ## Move Notice
4 | Beginning with the `2.2` release of this extension, we have moved from separate repositories for each of the logging extensions to building and deploying via a single `auth0-logs-to-provider` monorepo. This approach will make maintenance and issue tracking across all logging extensions much easier for Auth0 and more timely for our customers.
5 |
6 | The new monorepo can be found here: [auth0-logs-to-provider](https://github.com/auth0-extensions/auth0-logs-to-provider)
7 |
--------------------------------------------------------------------------------
/server/lib/logger.js:
--------------------------------------------------------------------------------
1 | const winston = require('winston');
2 |
3 | winston.emitErrs = true;
4 |
5 | const logger = new winston.Logger({
6 | transports: [
7 | new winston.transports.Console({
8 | timestamp: true,
9 | level: 'debug',
10 | handleExceptions: true,
11 | json: false,
12 | colorize: true
13 | })
14 | ],
15 | exitOnError: false
16 | });
17 |
18 | module.exports = logger;
19 | module.exports.stream = {
20 | write: (message) => {
21 | logger.info(message.replace(/\n$/, ''));
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/client/showDevTools.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import DevTools from './containers/DevTools';
4 |
5 | module.exports = (store) => {
6 | const popup = window.open(null, 'Redux DevTools', 'menubar=no,location=no,resizable=yes,scrollbars=no,status=no');
7 | popup.location.reload();
8 |
9 | setTimeout(() => {
10 | popup.document.write('
');
11 | render(
12 | ,
13 | popup.document.getElementById('react-devtools-root')
14 | );
15 | }, 10);
16 | };
17 |
--------------------------------------------------------------------------------
/webtask.js:
--------------------------------------------------------------------------------
1 | const tools = require('auth0-extension-express-tools');
2 |
3 | const expressApp = require('./server');
4 | const config = require('./server/lib/config');
5 | const logger = require('./server/lib/logger');
6 |
7 | const createServer = tools.createServer((cfg, storage) => {
8 | logger.info('Starting Management API Webhooks extension - Version:', process.env.CLIENT_VERSION);
9 | return expressApp(cfg, storage);
10 | });
11 |
12 | module.exports = (context, req, res) => {
13 | config.setValue('PUBLIC_WT_URL', tools.urlHelpers.getWebtaskUrl(req));
14 | createServer(context, req, res);
15 | };
16 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const nconf = require('nconf');
3 | const logger = require('./server/lib/logger');
4 |
5 | // Initialize configuration.
6 | nconf
7 | .argv()
8 | .env()
9 | .file(path.join(__dirname, './server/config.json'))
10 | .defaults({
11 | NODE_ENV: 'development',
12 | HOSTING_ENV: 'default',
13 | PORT: 3001,
14 | WT_URL: 'http://localhost:3000'
15 | });
16 |
17 | // Start the server.
18 | const app = require('./server')(key => nconf.get(key), null);
19 |
20 | const port = nconf.get('PORT');
21 | app.listen(port, (error) => {
22 | if (error) {
23 | logger.error(error);
24 | } else {
25 | logger.info(`Listening on http://localhost:${port}.`);
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/client/utils/createReducer.js:
--------------------------------------------------------------------------------
1 | import Immutable, { Map, List } from 'immutable';
2 |
3 | export default function createReducer(initialState, actionHandlers) {
4 | return (state = initialState, action) => {
5 | if (!Map.isMap(state) && !List.isList(state)) {
6 | state = Immutable.fromJS(state); // eslint-disable-line no-param-reassign
7 | }
8 |
9 | const handler = actionHandlers[action.type];
10 | if (!handler) {
11 | return state;
12 | }
13 |
14 | state = handler(state, action); // eslint-disable-line no-param-reassign
15 | if (!Map.isMap(state) && !List.isList(state)) {
16 | throw new TypeError('Reducers must return Immutable objects.');
17 | }
18 |
19 | return state;
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
35 | .DS_Store
36 | .idea
37 | dist
38 | server/data.json
39 | server/config.json
40 |
--------------------------------------------------------------------------------
/client/app.jsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import { Provider } from 'react-redux';
5 |
6 | import { App } from './containers';
7 | import { loadCredentials } from './actions/auth';
8 | import configureStore from './store/configureStore';
9 |
10 | const showDevTools = (process.env.NODE_ENV !== 'production') ? require('./showDevTools') : null;
11 |
12 | // Make axios aware of the base path.
13 | axios.defaults.baseURL = window.config.BASE_URL;
14 |
15 | // Initialize the store.
16 | const store = configureStore();
17 | store.dispatch(loadCredentials());
18 |
19 | // Render application.
20 | ReactDOM.render(
21 |
22 |
23 | ,
24 | document.getElementById('app')
25 | );
26 |
27 | // Show the developer tools.
28 | if (showDevTools) {
29 | showDevTools(store);
30 | }
31 |
--------------------------------------------------------------------------------
/client/containers/RequireAuthentication.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | export default function RequireAuthentication(InnerComponent) {
5 | class RequireAuthenticationContainer extends React.Component {
6 | componentWillMount() {
7 | this.requireAuthentication();
8 | }
9 |
10 | componentWillReceiveProps() {
11 | this.requireAuthentication();
12 | }
13 |
14 | requireAuthentication() {
15 | if (!this.props.auth.isAuthenticated && !this.props.auth.isAuthenticating) {
16 | window.location = window.config.BASE_URL + '/login';
17 | }
18 | }
19 |
20 | render() {
21 | if (this.props.auth.isAuthenticated) {
22 | return ;
23 | }
24 |
25 | return
;
26 | }
27 | }
28 |
29 | return connect((state) => ({ auth: state.auth.toJS() }), { })(RequireAuthenticationContainer);
30 | }
31 |
--------------------------------------------------------------------------------
/client/actions/config.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import * as constants from '../constants';
3 |
4 | /*
5 | * Load the configuration settings.
6 | */
7 | export function fetchConfiguration() {
8 | return {
9 | type: constants.FETCH_CONFIGURATION,
10 | payload: {
11 | promise: axios.get('/api/config', {
12 | timeout: 5000,
13 | responseType: 'json'
14 | })
15 | }
16 | };
17 | }
18 |
19 | /*
20 | * Close notification.
21 | */
22 | export function closeNotification() {
23 | return {
24 | type: constants.CLOSE_NOTIFICATION,
25 | payload: {
26 | promise: axios.post('/api/notified', {
27 | responseType: 'json'
28 | })
29 | }
30 | };
31 | }
32 |
33 | export function confirmNotification() {
34 | return {
35 | type: constants.CONFIRM_NOTIFICATION,
36 | payload: {
37 | promise: axios.post('/api/notified', {
38 | responseType: 'json'
39 | })
40 | }
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/client/middlewares/normalizeErrorMiddleware.js:
--------------------------------------------------------------------------------
1 | export default function normalizeErrorMiddleware() {
2 | return () => next => (action) => {
3 | if (action && action.type.endsWith('_REJECTED') && action.payload) {
4 | let error = 'Unknown Server Error';
5 | if (action.payload.code === 'ECONNABORTED') {
6 | error = 'The connectioned timed out.';
7 | } else if (action.payload.data && action.payload.data.error) {
8 | error = action.payload.data.error;
9 | } else if (action.payload.error) {
10 | error = action.payload.error;
11 | } else if (action.payload.response && action.payload.response.data) {
12 | error = action.payload.response.data;
13 | }
14 |
15 | if (error && error.message) {
16 | error = error.message;
17 | }
18 |
19 | action.errorMessage = error || action.payload.statusText || action.payload.status || 'Unknown Server Error'; // eslint-disable-line no-param-reassign
20 | }
21 |
22 | next(action);
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/client/actions/log.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import * as constants from '../constants';
3 |
4 | /*
5 | * Load the logs history.
6 | */
7 | export function fetchLogs(page = 1, errors) {
8 | const params = { page };
9 | if (errors) {
10 | params.filter = 'errors';
11 | }
12 |
13 | return {
14 | type: constants.FETCH_LOGS,
15 | payload: {
16 | promise: axios.get('/api/report', {
17 | params,
18 | timeout: 5000,
19 | responseType: 'json'
20 | })
21 | },
22 | meta: {
23 | page
24 | }
25 | };
26 | }
27 |
28 | /*
29 | * Open a log.
30 | */
31 | export function openLog(log) {
32 | return {
33 | type: constants.OPEN_LOG,
34 | payload: {
35 | log
36 | }
37 | };
38 | }
39 |
40 | /*
41 | * Clear the current logs.
42 | */
43 | export function clearLog() {
44 | return {
45 | type: constants.CLEAR_LOG
46 | };
47 | }
48 |
49 | /*
50 | * Set log filtering.
51 | */
52 | export function setFilter(status) {
53 | return {
54 | type: constants.SET_FILTER,
55 | payload: {
56 | status
57 | }
58 | };
59 | }
60 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Auth0
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/client/components/LogsDialog.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Button, ButtonToolbar, Modal } from 'react-bootstrap';
3 |
4 | import './LogsDialog.css';
5 |
6 | export default class LogsDialog extends Component {
7 | static propTypes = {
8 | log: React.PropTypes.object,
9 | onClose: React.PropTypes.func.isRequired
10 | };
11 |
12 | render() {
13 | if (!this.props.log) {
14 | return
;
15 | }
16 |
17 | const log = this.props.log;
18 |
19 | let message = JSON.stringify(log, null, ' ');
20 |
21 | return (
22 |
23 |
24 | {log.checkpoint}
25 |
26 |
27 | {message}
28 |
29 |
30 |
31 |
32 | Close
33 |
34 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/client/utils/auth.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 |
3 | import qs from 'qs';
4 | import jwtDecode from 'jwt-decode';
5 |
6 | export function parseHash(hash) {
7 | let hashFragement = hash || window.location.hash;
8 |
9 | let parsedHash;
10 | if (hashFragement.match(/error/)) {
11 | hashFragement = hashFragement.substr(1).replace(/^\//, '');
12 | parsedHash = qs.parse(hashFragement);
13 |
14 | return {
15 | error: parsedHash.error,
16 | errorDescription: parsedHash.error_description
17 | };
18 | }
19 |
20 | hashFragement = hashFragement.substr(1).replace(/^\//, '');
21 | parsedHash = qs.parse(hashFragement);
22 |
23 | return {
24 | accessToken: parsedHash.access_token,
25 | idToken: parsedHash.id_token,
26 | refreshToken: parsedHash.refresh_token,
27 | state: parsedHash.state
28 | };
29 | }
30 |
31 | export function isTokenExpired(decodedToken) {
32 | if (typeof decodedToken.exp === 'undefined') {
33 | return true;
34 | }
35 |
36 | const d = new Date(0);
37 | d.setUTCSeconds(decodedToken.exp);
38 |
39 | return !(d.valueOf() > (new Date().valueOf() + (1000)));
40 | }
41 |
42 | export function decodeToken(token) {
43 | return jwtDecode(token);
44 | }
45 |
--------------------------------------------------------------------------------
/client/reducers/logs.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 |
3 | import * as constants from '../constants';
4 | import createReducer from '../utils/createReducer';
5 |
6 | const initialState = {
7 | loading: false,
8 | error: null,
9 | records: [],
10 | total: 0,
11 | activeRecord: null
12 | };
13 |
14 | export const logs = createReducer(fromJS(initialState), { // eslint-disable-line import/prefer-default-export
15 | [constants.OPEN_LOG]: (state, action) =>
16 | state.merge({
17 | activeRecord: action.payload.log
18 | }),
19 | [constants.CLEAR_LOG]: state =>
20 | state.merge({
21 | activeRecord: null
22 | }),
23 | [constants.FETCH_LOGS_PENDING]: state =>
24 | state.merge({
25 | loading: true,
26 | records: []
27 | }),
28 | [constants.FETCH_LOGS_REJECTED]: (state, action) =>
29 | state.merge({
30 | loading: false,
31 | error: `An error occurred while loading the logs: ${action.errorMessage}`
32 | }),
33 | [constants.FETCH_LOGS_FULFILLED]: (state, action) => {
34 | const { data } = action.payload;
35 | return state.merge({
36 | loading: false,
37 | records: fromJS(data.logs),
38 | nextPage: action.meta.page + 1,
39 | total: data.total
40 | });
41 | }
42 | });
43 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "auth0-base",
4 | "plugin:import/errors",
5 | "plugin:import/warnings"
6 | ],
7 | "parser": "babel-eslint",
8 | "parserOptions": {
9 | "ecmaFeatures": {
10 | "jsx": true,
11 | "modules": true
12 | },
13 | "ecmaVersion": 4,
14 | "sourceType": "module"
15 | },
16 | "env": {
17 | "node": true,
18 | "mocha": true
19 | },
20 | "rules": {
21 | "max-len": 0,
22 | "react/display-name": 0,
23 | "array-bracket-spacing": [2, "always"],
24 | "comma-dangle": [2, "never"],
25 | "eol-last": 2,
26 | "indent": [2, 2, {
27 | "SwitchCase": 1
28 | }],
29 | "import/no-extraneous-dependencies": [error, { devDependencies: true }],
30 | "prefer-arrow-callback": 0,
31 | "object-shorthand": 0,
32 | "prefer-template": 0,
33 | "func-names": 0,
34 | "new-cap": 0,
35 | "no-param-reassign": 0,
36 | "no-multiple-empty-lines": 2,
37 | "no-unused-vars": 2,
38 | "no-var": 0,
39 | "object-curly-spacing": [2, "always"],
40 | "quotes": [2, "single", "avoid-escape"],
41 | "semi": [2, "always"],
42 | "strict": 0,
43 | "space-before-blocks": [2, "always"],
44 | "space-before-function-paren": [2, {
45 | "anonymous": "never",
46 | "named": "never"
47 | }]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/client/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import createLogger from 'redux-logger';
2 | import thunkMiddleware from 'redux-thunk';
3 | import promiseMiddleware from 'redux-promise-middleware';
4 | import { compose, createStore, applyMiddleware } from 'redux';
5 |
6 | import rootReducer from '../reducers';
7 | import normalizeErrorMiddleware from '../middlewares/normalizeErrorMiddleware';
8 | import DevTools from '../containers/DevTools.jsx';
9 |
10 | const nextRootReducer = ((process.env.NODE_ENV !== 'production' && module.hot)) ? require('../reducers') : null;
11 |
12 | export default function configureStore() {
13 | const pipeline = [
14 | applyMiddleware(
15 | promiseMiddleware(),
16 | thunkMiddleware,
17 | normalizeErrorMiddleware(),
18 | createLogger({
19 | predicate: () => process.env.NODE_ENV !== 'production'
20 | })
21 | )
22 | ];
23 |
24 | if (process.env.NODE_ENV !== 'production') {
25 | pipeline.push(DevTools.instrument());
26 | }
27 |
28 | const finalCreateStore = compose(...pipeline)(createStore);
29 | const store = finalCreateStore(rootReducer, { });
30 |
31 | // Enable Webpack hot module replacement for reducers.
32 | if (nextRootReducer) {
33 | module.hot.accept('../reducers', () => {
34 | store.replaceReducer(nextRootReducer);
35 | });
36 | }
37 |
38 | return store;
39 | }
40 |
--------------------------------------------------------------------------------
/server/routes/hooks.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router;
2 | const tools = require('auth0-extension-tools');
3 | const middlewares = require('auth0-extension-express-tools').middlewares;
4 |
5 | const config = require('../lib/config');
6 | const logger = require('../lib/logger');
7 |
8 | module.exports = () => {
9 | const hooks = router();
10 | const hookValidator = middlewares
11 | .validateHookToken(config('AUTH0_DOMAIN'), config('WT_URL'), config('EXTENSION_SECRET'));
12 |
13 | hooks.use('/on-uninstall', hookValidator('/.extensions/on-uninstall'));
14 |
15 | hooks.delete('/on-uninstall', (req, res) => {
16 | const clientId = config('AUTH0_CLIENT_ID');
17 | const options = {
18 | domain: config('AUTH0_DOMAIN'),
19 | clientSecret: config('AUTH0_CLIENT_SECRET'),
20 | clientId
21 | };
22 | tools.managementApi.getClient(options)
23 | .then(auth0 => auth0.clients.delete({ client_id: clientId }))
24 | .then(() => {
25 | logger.debug(`Deleted client ${clientId}`);
26 | res.sendStatus(204);
27 | })
28 | .catch((err) => {
29 | logger.debug(`Error deleting client: ${clientId}`);
30 | logger.error(err);
31 |
32 | // Even if deleting fails, we need to be able to uninstall the extension.
33 | res.sendStatus(204);
34 | });
35 | });
36 | return hooks;
37 | };
38 |
--------------------------------------------------------------------------------
/client/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import url from 'url';
2 | import { fromJS } from 'immutable';
3 |
4 | import * as constants from '../constants';
5 | import createReducer from '../utils/createReducer';
6 |
7 | const initialState = {
8 | error: null,
9 | isAuthenticated: false,
10 | isAuthenticating: false,
11 | issuer: null,
12 | token: null,
13 | decodedToken: null,
14 | user: null
15 | };
16 |
17 | export const auth = createReducer(fromJS(initialState), { // eslint-disable-line import/prefer-default-export
18 | [constants.LOGIN_PENDING]: state =>
19 | state.merge({
20 | ...initialState,
21 | isAuthenticating: true
22 | }),
23 | [constants.LOGIN_FAILED]: (state, action) =>
24 | state.merge({
25 | isAuthenticating: false,
26 | error: (action.payload && action.payload.error) || 'Unknown Error'
27 | }),
28 | [constants.LOGIN_SUCCESS]: (state, action) =>
29 | state.merge({
30 | isAuthenticated: true,
31 | isAuthenticating: false,
32 | user: action.payload.user,
33 | token: action.payload.token,
34 | decodedToken: action.payload.decodedToken,
35 | issuer: url.parse(action.payload.decodedToken.iss).hostname
36 | }),
37 | [constants.LOGOUT_SUCCESS]: state =>
38 | state.merge({
39 | user: null,
40 | token: null,
41 | decodedToken: null,
42 | isAuthenticated: false
43 | })
44 | });
45 |
--------------------------------------------------------------------------------
/server/routes/index.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const router = require('express').Router;
3 | const middlewares = require('auth0-extension-express-tools').middlewares;
4 |
5 | const config = require('../lib/config');
6 | const htmlRoute = require('./html');
7 |
8 | module.exports = (storage) => {
9 | const app = router();
10 |
11 | const authenticateAdmins = middlewares.authenticateAdmins({
12 | credentialsRequired: true,
13 | secret: config('EXTENSION_SECRET'),
14 | audience: 'urn:management-api-webhooks',
15 | baseUrl: config('PUBLIC_WT_URL') || config('WT_URL'),
16 | onLoginSuccess: (req, res, next) => next()
17 | });
18 |
19 | app.get('/', htmlRoute());
20 |
21 | app.get('/api/report', authenticateAdmins, (req, res, next) =>
22 | storage.read()
23 | .then((data) => {
24 | const allLogs = (data && data.logs) ? _.sortByOrder(data.logs, 'start', 'desc') : [];
25 | const logs = (req.query.filter && req.query.filter === 'errors') ? _.filter(allLogs, log => !!log.error) : allLogs;
26 | const page = (req.query.page && parseInt(req.query.page, 10)) ? parseInt(req.query.page, 10) - 1 : 0;
27 | const perPage = (req.query.per_page && parseInt(req.query.per_page, 10)) || 10;
28 | const offset = perPage * page;
29 |
30 | return res.json({ logs: logs.slice(offset, offset + perPage), total: logs.length });
31 | })
32 | .catch(next));
33 |
34 | return app;
35 | };
36 |
--------------------------------------------------------------------------------
/client/actions/auth.js:
--------------------------------------------------------------------------------
1 | /* global window, localStorage, sessionStorage */
2 |
3 | import axios from 'axios';
4 | import { isTokenExpired, decodeToken } from '../utils/auth';
5 |
6 | import * as constants from '../constants';
7 |
8 | export function logout() {
9 | return (dispatch) => {
10 | localStorage.removeItem('management-api-webhooks:apiToken');
11 | sessionStorage.removeItem('management-api-webhooks:apiToken');
12 |
13 | window.location = window.config.AUTH0_MANAGE_URL;
14 |
15 | dispatch({
16 | type: constants.LOGOUT_SUCCESS
17 | });
18 | };
19 | }
20 |
21 | export function loadCredentials() {
22 | return (dispatch) => {
23 | const apiToken = sessionStorage.getItem('management-api-webhooks:apiToken');
24 | if (apiToken) {
25 | const decodedToken = decodeToken(apiToken);
26 |
27 | if (isTokenExpired(decodedToken)) {
28 | return;
29 | }
30 |
31 | axios.defaults.headers.common.Authorization = `Bearer ${apiToken}`;
32 | sessionStorage.setItem('management-api-webhooks:apiToken', apiToken);
33 |
34 | dispatch({
35 | type: constants.RECIEVED_TOKEN,
36 | payload: {
37 | token: apiToken
38 | }
39 | });
40 |
41 | dispatch({
42 | type: constants.LOGIN_SUCCESS,
43 | payload: {
44 | token: apiToken,
45 | decodedToken,
46 | user: decodedToken
47 | }
48 | });
49 | }
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/client/containers/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { logout } from '../actions/auth';
5 | import Header from '../components/Header';
6 |
7 | import RequireAuthentication from './RequireAuthentication';
8 | import { LogsContainer } from './';
9 |
10 | class App extends Component {
11 | render() {
12 | return (
13 |
37 | );
38 | }
39 | }
40 |
41 | function select(state) {
42 | return {
43 | user: state.auth.get('user'),
44 | issuer: state.auth.get('issuer')
45 | };
46 | }
47 |
48 | export default RequireAuthentication(connect(select, { logout })(App));
49 |
--------------------------------------------------------------------------------
/client/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Configuration.
3 | */
4 |
5 | // Fetch.
6 | export const FETCH_CONFIGURATION = 'FETCH_CONFIGURATION';
7 | export const FETCH_CONFIGURATION_PENDING = 'FETCH_CONFIGURATION_PENDING';
8 | export const FETCH_CONFIGURATION_REJECTED = 'FETCH_CONFIGURATION_REJECTED';
9 | export const FETCH_CONFIGURATION_FULFILLED = 'FETCH_CONFIGURATION_FULFILLED';
10 |
11 | export const CLOSE_NOTIFICATION = 'CLOSE_NOTIFICATION';
12 | export const CLOSE_NOTIFICATION_PENDING = 'CLOSE_NOTIFICATION_PENDING';
13 | export const CLOSE_NOTIFICATION_FULFILLED = 'CLOSE_NOTIFICATION_FULFILLED';
14 | export const CLOSE_NOTIFICATION_REJECTED = 'CLOSE_NOTIFICATION_REJECTED';
15 | export const CONFIRM_NOTIFICATION = 'CONFIRM_NOTIFICATION';
16 | export const CONFIRM_NOTIFICATION_PENDING = 'CONFIRM_NOTIFICATION_PENDING';
17 | export const CONFIRM_NOTIFICATION_FULFILLED = 'CONFIRM_NOTIFICATION_FULFILLED';
18 | export const CONFIRM_NOTIFICATION_REJECTED = 'CONFIRM_NOTIFICATION_REJECTED';
19 |
20 | /*
21 | * Logs.
22 | */
23 | export const OPEN_LOG = 'OPEN_LOG';
24 | export const CLEAR_LOG = 'CLEAR_LOG';
25 |
26 | export const SET_FILTER = 'SET_FILTER';
27 |
28 | // Fetch.
29 | export const FETCH_LOGS = 'FETCH_LOGS';
30 | export const FETCH_LOGS_PENDING = 'FETCH_LOGS_PENDING';
31 | export const FETCH_LOGS_REJECTED = 'FETCH_LOGS_REJECTED';
32 | export const FETCH_LOGS_FULFILLED = 'FETCH_LOGS_FULFILLED';
33 |
34 | /*
35 | * Auth.
36 | */
37 |
38 | // Token.
39 | export const LOADED_TOKEN = 'LOADED_TOKEN';
40 | export const RECIEVED_TOKEN = 'RECIEVED_TOKEN';
41 |
42 | // Login.
43 | export const SHOW_LOGIN = 'SHOW_LOGIN';
44 | export const REDIRECT_LOGIN = 'REDIRECT_LOGIN';
45 | export const LOGIN_PENDING = 'LOGIN_PENDING';
46 | export const LOGIN_FAILED = 'LOGIN_FAILED';
47 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
48 |
49 | // Logout.
50 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
51 |
52 |
--------------------------------------------------------------------------------
/client/reducers/config.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 |
3 | import * as constants from '../constants';
4 | import createReducer from '../utils/createReducer';
5 |
6 | const initialState = {
7 | loading: false,
8 | error: null,
9 | record: { },
10 | showNotification: false,
11 | activeTab: 'config'
12 | };
13 |
14 | export const config = createReducer(fromJS(initialState), { // eslint-disable-line import/prefer-default-export
15 | [constants.FETCH_CONFIGURATION_PENDING]: state =>
16 | state.merge({
17 | loading: true,
18 | record: { },
19 | showNotification: false
20 | }),
21 | [constants.FETCH_CONFIGURATION_REJECTED]: (state, action) =>
22 | state.merge({
23 | loading: false,
24 | error: `An error occured while loading the configuration: ${action.errorMessage}`
25 | }),
26 | [constants.FETCH_CONFIGURATION_FULFILLED]: (state, action) => {
27 | const { data } = action.payload;
28 | return state.merge({
29 | loading: false,
30 | record: fromJS(data),
31 | showNotification: data.showNotification
32 | });
33 | },
34 | [constants.CLOSE_NOTIFICATION_PENDING]: state =>
35 | state.merge({
36 | loading: true,
37 | showNotification: false
38 | }),
39 | [constants.CLOSE_NOTIFICATION_REJECTED]: state =>
40 | state.merge({
41 | loading: false
42 | }),
43 | [constants.CLOSE_NOTIFICATION_FULFILLED]: state =>
44 | state.merge({
45 | loading: false
46 | }),
47 | [constants.CONFIRM_NOTIFICATION_PENDING]: state =>
48 | state.merge({
49 | loading: true,
50 | showNotification: false
51 | }),
52 | [constants.CONFIRM_NOTIFICATION_REJECTED]: state =>
53 | state.merge({
54 | loading: false
55 | }),
56 | [constants.CONFIRM_NOTIFICATION_FULFILLED]: state =>
57 | state.merge({
58 | loading: false,
59 | showNotification: false,
60 | activeTab: 'rules'
61 | })
62 | });
63 |
--------------------------------------------------------------------------------
/client/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import './Header.css';
2 | import React, { Component } from 'react';
3 |
4 | export default class Header extends Component {
5 | static propTypes = {
6 | tenant: React.PropTypes.string,
7 | onLogout: React.PropTypes.func.isRequired
8 | }
9 |
10 | getPicture(tenant) {
11 | return `https://cdn.auth0.com/avatars/${tenant.slice(0, 2).toLowerCase()}.png`;
12 | }
13 |
14 | render() {
15 | const { tenant, onLogout } = this.props;
16 |
17 | return (
18 |
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const morgan = require('morgan');
3 | const Express = require('express');
4 | const bodyParser = require('body-parser');
5 | const tools = require('auth0-extension-tools');
6 | const expressTools = require('auth0-extension-express-tools');
7 |
8 | const routes = require('./routes/index');
9 | const meta = require('./routes/meta');
10 | const hooks = require('./routes/hooks');
11 | const logger = require('./lib/logger');
12 | const config = require('./lib/config');
13 | const processLogs = require('./lib/processLogs');
14 |
15 | module.exports = (configProvider, storageProvider) => {
16 | config.setProvider(configProvider);
17 |
18 | const storage = storageProvider
19 | ? new tools.WebtaskStorageContext(storageProvider, { force: 1 })
20 | : new tools.FileStorageContext(path.join(__dirname, './data.json'), { mergeWrites: true });
21 |
22 | const app = new Express();
23 | app.use(morgan(':method :url :status :response-time ms - :res[content-length]', {
24 | stream: logger.stream
25 | }));
26 |
27 | const prepareBody = middleware =>
28 | (req, res, next) => {
29 | if (req.webtaskContext && req.webtaskContext.body) {
30 | req.body = req.webtaskContext.body;
31 | return next();
32 | }
33 |
34 | return middleware(req, res, next);
35 | };
36 |
37 | app.use(prepareBody(bodyParser.json()));
38 | app.use(prepareBody(bodyParser.urlencoded({ extended: false })));
39 |
40 | // Configure routes.
41 |
42 | app.use(expressTools.routes.dashboardAdmins({
43 | secret: config('EXTENSION_SECRET'),
44 | audience: 'urn:management-api-webhooks',
45 | domain: config('AUTH0_DOMAIN'),
46 | rta: config('AUTH0_RTA').replace('https://', ''),
47 | baseUrl: config('PUBLIC_WT_URL') || config('WT_URL'),
48 | clientName: 'Management Api Webhooks',
49 | urlPrefix: '',
50 | sessionStorageKey: 'management-api-webhooks:apiToken'
51 | }));
52 | app.use('/meta', meta());
53 | app.use('/.extensions', hooks());
54 |
55 | app.use('/app', Express.static(path.join(__dirname, '../dist')));
56 |
57 | app.use(processLogs(storage));
58 | app.use('/', routes(storage));
59 |
60 | // Generic error handler.
61 | app.use(expressTools.middlewares.errorHandler(logger.error.bind(logger)));
62 | return app;
63 | };
64 |
--------------------------------------------------------------------------------
/client/components/LogsTable.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { ButtonToolbar } from 'react-bootstrap';
3 | import { Table, TableAction, TableCell, TableBody, TableIconCell, TableTextCell, TableHeader, TableColumn, TableRow } from 'auth0-extension-ui';
4 |
5 | export default class LogsTable extends Component {
6 | static propTypes = {
7 | showLogs: React.PropTypes.func.isRequired,
8 | error: React.PropTypes.string,
9 | records: React.PropTypes.array.isRequired
10 | };
11 |
12 | render() {
13 | const { error, records } = this.props;
14 | if (!error && records.size === 0) {
15 | return There are no logs available.
;
16 | }
17 |
18 | return (
19 |
20 |
21 |
22 |
23 | Status
24 | Start
25 | End
26 | Logs Processed
27 | Checkpoint
28 |
29 |
30 |
31 | {records.map((record, index) => {
32 | const success = !record.error;
33 | const color = success ? 'green' : '#A93F3F';
34 | const status = success ? 'Success' : 'Failed';
35 | return (
36 |
37 |
38 | {status}
39 | {record.start}
40 | {record.end}
41 | {record.logsProcessed}
42 | {record.checkpoint}
43 |
44 |
45 | this.props.showLogs(record)}
48 | />
49 |
50 |
51 |
52 | );
53 | })}
54 |
55 |
56 |
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## ✏️ Changes
2 |
3 | > DESCRIPTION GOES HERE. Try to describe both what is changing and why this is important
4 | > - Make sure you run when adding / updating a package
5 | > - What did you change from a design standpoint?
6 | > - What did you change in the code itself?
7 | > - If you are updating a dependency, explain why this is needed.
8 |
9 | ## 📷 Screenshots
10 |
11 | If there were visual changes to the application with this change, please include before and after screenshots here. If it has animation, please use screen capture software like to make a gif.
12 |
13 | ## 🔗 References
14 |
15 | > Include at at least one link to an explanation + requirements for this change, and more if at all possible. Typically this is a Jira/GitHub Issue, but could also be links to Zendesk tickets, RFCs, rollout plan or Slack conversations (for Slack conversations, make sure you provide a summary of the conversation under “Changes”).
16 |
17 | ## 🎯 Testing
18 |
19 | > Describe how this can be tested by reviewers. Please be specific about anything not tested and reasons why.
20 | > - Make sure you add unit and integration tests.
21 | > - If this is on a hot path, add load or performance tests
22 | > - Especially for dependency updates we also need to make sure that there is no impact on performance.
23 |
24 | ✅🚫 This change has been tested in a Webtask
25 |
26 | ✅🚫 This change has unit test coverage
27 |
28 | ✅🚫 This change has integration test coverage
29 |
30 | ✅🚫 This change has been tested for performance
31 |
32 | ## 🚀 Deployment
33 |
34 | > Can this change be merged at any time? What will the deployment of the change look like? Does this need to be released in lockstep with something else?
35 |
36 | ✅🚫 This can be deployed any time
37 |
38 | > or
39 | > ⚠️ This should not be merged until:
40 | > - Other PR is merged because REASON
41 | > - After date because REASON
42 | > - Other condition: REASON
43 |
44 | ## 🎡 Rollout
45 |
46 | > Explain how the change will be verified once released. Manual testing? Functional testing?
47 |
48 | In order to verify that the deployment was successful we will …
49 |
50 | ## 🔥 Rollback
51 |
52 | > Explain when and why we will rollback the change.
53 |
54 | We will rollback if …
55 |
56 | ### 📄 Procedure
57 |
58 | > Explain how the rollback for this change will look like, how we can recover fast.
59 |
60 | ## 🖥 Appliance
61 |
62 | **Note to reviewers:** ensure that this change is compatible with the Appliance.
63 |
--------------------------------------------------------------------------------
/webtask.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Auth0 Management API Webhooks",
3 | "name": "auth0-webhooks",
4 | "version": "2.1.0",
5 | "author": "auth0",
6 | "description": "Allows you to define webhooks for Auth0's Management API. It will go through the audit logs and call a webhook for specific events.",
7 | "type": "cron",
8 | "initialUrlPath": "/login",
9 | "category": "webhook",
10 | "repository": "https://github.com/auth0/auth0-management-api-webhooks",
11 | "docsUrl": "https://auth0.com/docs/extensions/management-api-webhooks",
12 | "logoUrl": "https://cdn.auth0.com/extensions/auth0-webhooks/assets/logo.svg",
13 | "keywords": [
14 | "auth0",
15 | "extension"
16 | ],
17 | "schedule": "0 */5 * * * *",
18 | "auth0": {
19 | "createClient": true,
20 | "onUninstallPath": "/.extensions/on-uninstall",
21 | "scopes": "read:logs delete:clients"
22 | },
23 | "secrets": {
24 | "BATCH_SIZE": {
25 | "description": "The amount of logs to be read on each execution. Maximum is 100.",
26 | "default": 100
27 | },
28 | "AUTH0_API_ENDPOINTS": {
29 | "description": "Allows you to filter specific API endpoints, comma separated.",
30 | "example": "e.g.: users, connections, rules, emails, stats, clients, tenants"
31 | },
32 | "WEBHOOK_URL": {
33 | "required": true,
34 | "type": "text"
35 | },
36 | "AUTHORIZATION": {
37 | "description": "Authorization Header (optional).",
38 | "example": "Basic dm9yZGVsOnZvcmRlbA==",
39 | "type": "text"
40 | },
41 | "SEND_AS_BATCH": {
42 | "description": "If enabled, extension will send entire batch in one request. Otherwise, it will send requests one by one.",
43 | "type": "select",
44 | "allowMultiple": false,
45 | "default": "true",
46 | "options": [
47 | {
48 | "value": "true",
49 | "text": "Enabled"
50 | },
51 | {
52 | "value": "false",
53 | "text": "Disabled"
54 | }
55 | ]
56 | },
57 | "WEBHOOK_CONCURRENT_CALLS": {
58 | "description": "The maximum concurrent calls that will be made to your webhook",
59 | "default": 5
60 | },
61 | "START_FROM": {
62 | "description": "Checkpoint ID of log to start from."
63 | },
64 | "SLACK_INCOMING_WEBHOOK_URL": {
65 | "description": "Slack Incoming Webhook URL used to report statistics and possible failures"
66 | },
67 | "SLACK_SEND_SUCCESS": {
68 | "description": "This setting will enable verbose notifications to Slack which are useful for troubleshooting",
69 | "type": "select",
70 | "allowMultiple": false,
71 | "default": "false",
72 | "options": [
73 | {
74 | "value": "false",
75 | "text": "No"
76 | },
77 | {
78 | "value": "true",
79 | "text": "Yes"
80 | }
81 | ]
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/client/containers/LogsContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import { Button, ButtonToolbar } from 'react-bootstrap';
3 | import connectContainer from 'redux-static';
4 | import { Error, LoadingPanel, Pagination, TableTotals } from 'auth0-extension-ui';
5 |
6 | import { logActions } from '../actions';
7 |
8 | import LogsTable from '../components/LogsTable';
9 | import LogsDialog from '../components/LogsDialog';
10 |
11 | export default connectContainer(class extends Component {
12 | static stateToProps = (state) => ({
13 | logs: state.logs,
14 | filter: state.filter.toJS()
15 | });
16 |
17 | static actionsToProps = {
18 | ...logActions
19 | }
20 |
21 | static propTypes = {
22 | logs: PropTypes.object.isRequired,
23 | fetchLogs: PropTypes.func.isRequired,
24 | setFilter: PropTypes.func.isRequired,
25 | openLog: PropTypes.func.isRequired,
26 | clearLog: PropTypes.func.isRequired
27 | }
28 |
29 | componentWillMount() {
30 | this.props.fetchLogs();
31 | }
32 |
33 | updateFilter = (status) => {
34 | this.props.setFilter(status);
35 | this.props.fetchLogs(1, status);
36 | };
37 |
38 | handleReload = () => {
39 | this.props.fetchLogs(1, this.props.filter.status);
40 | };
41 |
42 | handlePageChange = (page) => {
43 | this.props.fetchLogs(page, this.props.filter.status);
44 | };
45 |
46 | render() {
47 | const { error, records, total, loading, activeRecord } = this.props.logs.toJS();
48 |
49 | return (
50 |
51 |
52 |
62 |
63 |
64 | Reload
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | { total > 10 ?
79 |
:
84 |
85 | }
86 |
87 |
88 | );
89 | }
90 | });
91 |
--------------------------------------------------------------------------------
/tests/lib/processLogs.tests.js:
--------------------------------------------------------------------------------
1 | const nock = require('nock');
2 | const expect = require('chai').expect;
3 |
4 | const config = require('../../server/lib/config');
5 | const processLogs = require('../../server/lib/processLogs');
6 |
7 | const storageProvider = (data = { }) => {
8 | const storage = {
9 | data
10 | };
11 | storage.read = () => new Promise(resolve => resolve(storage.data));
12 | storage.write = obj => new Promise((resolve) => {
13 | storage.data = obj;
14 | resolve();
15 | });
16 |
17 | return storage;
18 | };
19 |
20 | describe('processLogs', () => {
21 | before(() => {
22 | const defaultConfig = {
23 | AUTH0_DOMAIN: 'test.auth0.com',
24 | AUTH0_CLIENT_ID: 'someclientid',
25 | AUTH0_CLIENT_SECRET: 'someclientsecret',
26 | WEBHOOK_URL: 'http://test.webhook.example.com',
27 | AUTH0_API_ENDPOINTS: 'user,test',
28 | SEND_AS_BATCH: true,
29 | BATCH_SIZE: 100
30 | };
31 | config.setProvider(key => defaultConfig[key], null);
32 | });
33 |
34 | it('shouldn`t do anything if not run by cron', (done) => {
35 | const route = processLogs(storageProvider());
36 |
37 | route({}, {}, () => {
38 | done();
39 | });
40 | });
41 |
42 | it('should send logs to fake webhook', (done) => {
43 | nock('https://test.auth0.com')
44 | .post('/oauth/token')
45 | .reply(200, { expires_in: 2000, access_token: 'token', id_token: 'id_token', ok: true });
46 |
47 | nock('https://test.auth0.com')
48 | .get('/api/v2/logs')
49 | .query(() => true)
50 | .reply(function() {
51 | const logs = [
52 | { _id: 0, date: new Date(), type: 'sapi', details: { request: { path: '/api/v2/users' } } },
53 | { _id: 1, date: new Date(), type: 'sapi', details: { request: { path: '/api/v2/test' } } },
54 | { _id: 2, date: new Date(), type: 'sapi', details: { request: { path: '/api/v2/users' } } },
55 | { _id: 3, date: new Date(), type: 'sapi', details: { request: { path: '/api/v2/test' } } },
56 | { _id: 4, date: new Date(), type: 'sapi', details: { request: { path: '/api/v2/else' } } }
57 | ];
58 |
59 | return [
60 | 200,
61 | logs,
62 | {
63 | 'x-ratelimit-limit': 50,
64 | 'x-ratelimit-remaining': 49,
65 | 'x-ratelimit-reset': 0
66 | }
67 | ];
68 | });
69 |
70 | nock('https://test.auth0.com')
71 | .get('/api/v2/logs')
72 | .query(() => true)
73 | .reply(200, {});
74 |
75 | let receivedLogs;
76 |
77 | nock('http://test.webhook.example.com')
78 | .post('/')
79 | .reply(function(path, body) {
80 | receivedLogs = body;
81 | return [ 200, {} ];
82 | });
83 | const route = processLogs(storageProvider());
84 | const req = {
85 | body: {
86 | schedule: true,
87 | state: 'active'
88 | }
89 | };
90 |
91 | const res = {
92 | json: (data) => {
93 | expect(receivedLogs.length).to.equal(4);
94 | expect(data.status.logsProcessed).to.equal(5);
95 | expect(data.status.checkpoint).to.equal(4);
96 | done();
97 | }
98 | };
99 |
100 | route(req, res);
101 | });
102 | });
103 |
--------------------------------------------------------------------------------
/server/routes/html.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const ejs = require('ejs');
3 | const path = require('path');
4 | const urlHelpers = require('auth0-extension-express-tools').urlHelpers;
5 |
6 | const config = require('../lib/config');
7 |
8 | module.exports = () => {
9 | const template = `
10 |
11 |
12 |
13 | <%= config.TITLE %>
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | <% if (assets.style) { %> <% } %>
23 | <% if (assets.version) { %> <% } %>
24 | <% if (assets.customCss) { %> <% } %>
25 |
26 |
27 |
28 |
29 |
30 |
31 | <% if (assets.vendors) { %><% } %>
32 | <% if (assets.app) { %><% } %>
33 | <% if (assets.version) { %>
34 |
35 |
36 | <% } %>
37 |
38 |
39 | `;
40 |
41 | return (req, res, next) => {
42 | if (req.url.indexOf('/api') === 0) {
43 | return next();
44 | }
45 |
46 | const settings = {
47 | AUTH0_DOMAIN: config('AUTH0_DOMAIN'),
48 | AUTH0_CLIENT_ID: config('EXTENSION_CLIENT_ID'),
49 | AUTH0_MANAGE_URL: config('AUTH0_MANAGE_URL') || 'https://manage.auth0.com',
50 | BASE_URL: urlHelpers.getBaseUrl(req),
51 | BASE_PATH: urlHelpers.getBasePath(req),
52 | TITLE: config('TITLE')
53 | };
54 |
55 | // Render from CDN.
56 | const clientVersion = process.env.CLIENT_VERSION;
57 | if (clientVersion) {
58 | return res.send(ejs.render(template, {
59 | config: settings,
60 | assets: {
61 | customCss: config('CUSTOM_CSS'),
62 | version: clientVersion
63 | }
64 | }));
65 | }
66 |
67 | // Render locally.
68 | return fs.readFile(path.join(__dirname, '../../dist/manifest.json'), 'utf8', (err, manifest) => {
69 | const locals = {
70 | config: settings,
71 | assets: {
72 | customCss: config('CUSTOM_CSS'),
73 | app: 'http://localhost:3000/app/bundle.js'
74 | }
75 | };
76 |
77 | if (!err && manifest) {
78 | locals.assets = JSON.parse(manifest);
79 | locals.assets.customCss = config('CUSTOM_CSS');
80 | }
81 |
82 | // Render the HTML page.
83 | res.send(ejs.render(template, locals));
84 | });
85 | };
86 | };
87 |
--------------------------------------------------------------------------------
/server/lib/processLogs.js:
--------------------------------------------------------------------------------
1 | const async = require('async');
2 | const moment = require('moment');
3 | const Request = require('request');
4 | const loggingTools = require('auth0-log-extension-tools');
5 |
6 | const config = require('./config');
7 | const logger = require('./logger');
8 |
9 | module.exports = storage =>
10 | (req, res, next) => {
11 | const wtBody = (req.webtaskContext && req.webtaskContext.body) || req.body || {};
12 | const wtHead = (req.webtaskContext && req.webtaskContext.headers) || {};
13 | const isCron = (wtBody.schedule && wtBody.state === 'active') || (wtHead.referer === 'https://manage.auth0.com/' && wtHead['if-none-match']);
14 |
15 | if (!isCron) {
16 | return next();
17 | }
18 |
19 | const url = config('WEBHOOK_URL');
20 | const batchMode = config('SEND_AS_BATCH') === true || config('SEND_AS_BATCH') === 'true';
21 | const concurrentCalls = parseInt(config('WEBHOOK_CONCURRENT_CALLS'), 10) || 5;
22 | const headers = config('AUTHORIZATION') ? { Authorization: config('AUTHORIZATION') } : {};
23 |
24 | const sendRequest = (data, callback) =>
25 | Request({
26 | method: 'POST',
27 | url: url,
28 | json: true,
29 | headers: headers,
30 | body: data
31 | }, (err, response, body) => {
32 | if (err || response.statusCode < 200 || response.statusCode >= 400) {
33 | return callback(err || body || response.statusCode);
34 | }
35 |
36 | return callback();
37 | });
38 |
39 | const callWebhook = (logs, callback) => {
40 | if (batchMode) {
41 | logger.info(`Sending to '${url}'.`);
42 | return sendRequest(logs, callback);
43 | }
44 |
45 | logger.info(`Sending to '${url}' with ${concurrentCalls} concurrent calls.`);
46 | return async.eachLimit(logs, concurrentCalls, sendRequest, callback);
47 | };
48 |
49 | const onLogsReceived = (logs, callback) => {
50 | if (!logs || !logs.length) {
51 | return callback();
52 | }
53 |
54 | let endpointsFilter = config('AUTH0_API_ENDPOINTS').split(',');
55 | endpointsFilter = endpointsFilter.length > 0 && endpointsFilter[0] === '' ? [] : endpointsFilter;
56 |
57 | const requestMatchesFilter = (log) => {
58 | if (!endpointsFilter || !endpointsFilter.length) return true;
59 | const path = log.details.request && log.details.request.path;
60 | return path && endpointsFilter.some(filter => path.indexOf(`/api/v2/${filter}`) >= 0);
61 | };
62 |
63 | const filteredLogs = logs
64 | .filter(requestMatchesFilter)
65 | .map(log => ({
66 | date: log.date,
67 | request: log.details.request,
68 | response: log.details.response
69 | }));
70 |
71 | if (!filteredLogs.length) {
72 | return callback();
73 | }
74 |
75 | logger.info(`${filteredLogs.length} logs found.`);
76 |
77 | return callWebhook(filteredLogs, callback);
78 | };
79 |
80 | const slack = new loggingTools.reporters.SlackReporter({
81 | hook: config('SLACK_INCOMING_WEBHOOK_URL'),
82 | username: 'auth0-management-api-webhooks',
83 | title: 'Management Api Webhooks'
84 | });
85 |
86 | const options = {
87 | domain: config('AUTH0_DOMAIN'),
88 | clientId: config('AUTH0_CLIENT_ID'),
89 | clientSecret: config('AUTH0_CLIENT_SECRET'),
90 | batchSize: parseInt(config('BATCH_SIZE'), 10),
91 | startFrom: config('START_FROM'),
92 | logTypes: [ 'sapi', 'fapi' ]
93 | };
94 |
95 | if (!options.batchSize || options.batchSize > 100) {
96 | options.batchSize = 100;
97 | }
98 |
99 | const auth0logger = new loggingTools.LogsProcessor(storage, options);
100 |
101 | const sendDailyReport = (lastReportDate) => {
102 | const current = new Date();
103 |
104 | const end = current.getTime();
105 | const start = moment(end).subtract(1, 'day');
106 | auth0logger.getReport(start, end)
107 | .then(report => slack.send(report, report.checkpoint))
108 | .then(() => storage.read())
109 | .then((data) => {
110 | data.lastReportDate = lastReportDate;
111 | return storage.write(data);
112 | });
113 | };
114 |
115 | const checkReportTime = () => {
116 | storage.read()
117 | .then((data) => {
118 | const now = moment().format('DD-MM-YYYY');
119 | const reportTime = config('DAILY_REPORT_TIME') || 16;
120 |
121 | if (data.lastReportDate !== now && new Date().getHours() >= reportTime) {
122 | sendDailyReport(now);
123 | }
124 | });
125 | };
126 |
127 | return auth0logger
128 | .run(onLogsReceived)
129 | .then((result) => {
130 | if (result && result.status && result.status.error) {
131 | slack.send(result.status, result.checkpoint);
132 | } else if (config('SLACK_SEND_SUCCESS') === true || config('SLACK_SEND_SUCCESS') === 'true') {
133 | slack.send(result.status, result.checkpoint);
134 | }
135 |
136 | checkReportTime();
137 | res.json(result);
138 | })
139 | .catch((err) => {
140 | slack.send({ error: err, logsProcessed: 0 }, null);
141 | checkReportTime();
142 | next(err);
143 | });
144 | };
145 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "auth0-management-api-webhooks",
3 | "version": "2.1.0",
4 | "description": "A webtask that allows you to define webhooks for Auth0's Management API. It will go through the audit logs and call a webhook for specific events. This can address use cases like:",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "npm run clean && npm run client:build && npm run extension:build",
8 | "clean": "rimraf dist",
9 | "client:build": "npm run clean && cross-env NODE_ENV=production webpack --config ./build/webpack/config.prod.js --colors -p",
10 | "extension:build": "a0-ext build:server ./webtask.js ./dist && cp ./dist/auth0-webhooks.extension.$npm_package_version.js ./build/bundle.js",
11 | "lint:js": "eslint --ignore-path .gitignore --ignore-pattern build --ignore-pattern webpack .",
12 | "serve:dev": "cross-env NODE_ENV=development gulp run --gulpfile ./build/gulpfile.js",
13 | "serve:prod": "cross-env NODE_ENV=production node index.js",
14 | "tag": "git tag $npm_package_version && git push --tags",
15 | "test": "npm run test:pre && cross-env NODE_ENV=test mocha ./tests/mocha.js ./tests/**/*.tests.js",
16 | "test:watch": "npm run test:pre && cross-env NODE_ENV=test mocha ./tests/mocha.js ./tests/**/*.tests.js --watch",
17 | "test:pre": "npm run test:clean",
18 | "test:clean": "rimraf ./coverage && rimraf ./.nyc_output"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/auth0/auth0-management-api-webhooks.git"
23 | },
24 | "keywords": [
25 | "auth0",
26 | "extension"
27 | ],
28 | "author": "sandrino@auth0.com",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/auth0/auth0-management-api-webhooks/issues"
32 | },
33 | "homepage": "https://github.com/auth0/auth0-management-api-webhooks#readme",
34 | "auth0-extension": {
35 | "externals": [
36 | "async@2.1.2",
37 | "auth0-extension-tools@1.3.1",
38 | "bluebird@3.4.6",
39 | "compression@1.4.4",
40 | "delegates@0.1.0",
41 | "depd@1.0.1",
42 | "destroy@1.0.3",
43 | "ejs@2.3.1",
44 | "express@4.12.4",
45 | "express-jwt@3.1.0",
46 | "iconv-lite@0.4.10",
47 | "lodash@3.10.1",
48 | "lru-cache@2.6.4",
49 | "mime-db@1.10.0",
50 | "moment@2.10.3",
51 | "morgan@1.5.3",
52 | "ms@0.7.1",
53 | "qs@3.1.0",
54 | "raw-body@2.1.0",
55 | "read-all-stream@2.1.2",
56 | "request@2.56.0",
57 | "superagent@1.2.0",
58 | "type-check@0.3.1",
59 | "winston@1.0.0",
60 | "xml2js@0.4.8",
61 | "auth0@2.1.0",
62 | "lru-memoizer@1.10.0",
63 | "node-uuid@1.4.3",
64 | "jade@1.10.0",
65 | "jsonwebtoken@7.1.9",
66 | "debug@2.2.0",
67 | "body-parser@1.12.4",
68 | "mime-types@2.0.12",
69 | "webtask-tools"
70 | ]
71 | },
72 | "dependencies": {
73 | "async": "2.1.2",
74 | "auth0": "^2.4.0",
75 | "auth0-extension-express-tools": "^1.1.7",
76 | "auth0-extension-tools": "1.3.1",
77 | "auth0-extension-ui": "^1.0.1",
78 | "auth0-oauth2-express": "^1.1.8",
79 | "auth0-log-extension-tools": "^1.3.1",
80 | "axios": "^0.15.0",
81 | "babel": "^6.5.2",
82 | "babel-core": "^6.9.1",
83 | "babel-plugin-transform-runtime": "^6.9.0",
84 | "babel-polyfill": "^6.9.1",
85 | "babel-preset-es2015": "^6.9.0",
86 | "babel-preset-es2015-loose": "^8.0.0",
87 | "babel-preset-stage-0": "^6.5.0",
88 | "babel-register": "^6.9.0",
89 | "babel-runtime": "^6.9.2",
90 | "bluebird": "^3.4.1",
91 | "body-parser": "^1.15.2",
92 | "ejs": "^2.4.2",
93 | "expect": "^1.20.2",
94 | "express": "^4.14.0",
95 | "express-jwt": "^3.4.0",
96 | "glob": "^7.0.5",
97 | "immutable": "^3.8.1",
98 | "jsonwebtoken": "^7.1.9",
99 | "json-stringify-safe": "^5.0.1",
100 | "jwt-decode": "^2.1.0",
101 | "lodash": "3.10.1",
102 | "lru-memoizer": "^1.6.0",
103 | "moment": "^2.13.0",
104 | "morgan": "^1.7.0",
105 | "nconf": "^0.8.4",
106 | "qs": "^6.4.0",
107 | "react": "^15.3.2",
108 | "react-dom": "^15.3.2",
109 | "react-redux": "^4.4.5",
110 | "react-router": "^4.0.0",
111 | "redux": "^3.5.2",
112 | "redux-logger": "^2.6.1",
113 | "redux-promise-middleware": "^4.1.0",
114 | "redux-thunk": "^2.1.0",
115 | "request": "^2.72.0",
116 | "request-promise": "^3.0.0",
117 | "semver": "^5.1.0",
118 | "snyk": "^1.19.1",
119 | "superagent": "^1.2.0",
120 | "useragent": "2.1.6",
121 | "webtask-tools": "^3.0.0",
122 | "winston": "^2.2.0"
123 | },
124 | "devDependencies": {
125 | "auth0-extensions-cli": "^1.0.8",
126 | "autoprefixer": "^6.3.6",
127 | "babel-eslint": "^7.0.0",
128 | "babel-loader": "^6.2.4",
129 | "babel-preset-react": "^6.5.0",
130 | "babel-preset-react-hmre": "^1.1.1",
131 | "chai": "^3.5.0",
132 | "classnames": "^2.2.5",
133 | "cross-env": "^3.1.2",
134 | "css-loader": "^0.25.0",
135 | "eslint": "^3.16.1",
136 | "eslint-config-auth0": "^6.0.1",
137 | "eslint-config-auth0-base": "^12.0.0",
138 | "eslint-plugin-babel": "^3.3.0",
139 | "eslint-plugin-import": "^2.2.0",
140 | "eslint-plugin-jsx-a11y": "^2.1.0",
141 | "eslint-plugin-react": "^6.1.2",
142 | "exports-loader": "^0.6.3",
143 | "extract-text-webpack-plugin": "^1.0.1",
144 | "file-loader": "^0.9.0",
145 | "gulp": "^3.9.1",
146 | "gulp-nodemon": "^2.2.1",
147 | "gulp-util": "^3.0.7",
148 | "imports-loader": "^0.6.5",
149 | "json-loader": "^0.5.4",
150 | "mocha": "^3.1.2",
151 | "nock": "^9.0.11",
152 | "ngrok": "^2.2.9",
153 | "nodemon": "^1.9.2",
154 | "nyc": "^8.3.1",
155 | "postcss-focus": "^1.0.0",
156 | "postcss-loader": "^0.13.0",
157 | "postcss-reporter": "^1.3.3",
158 | "postcss-simple-vars": "^3.0.0",
159 | "raw-loader": "^0.5.1",
160 | "react-bootstrap": "^0.30.5",
161 | "react-loader-advanced": "^1.4.0",
162 | "react-s-alert": "^1.1.4",
163 | "react-transform-hmr": "^1.0.4",
164 | "redux-devtools": "^3.3.1",
165 | "redux-devtools-dock-monitor": "^1.1.1",
166 | "redux-devtools-log-monitor": "^1.0.11",
167 | "redux-form": "^5.2.5",
168 | "redux-simple-router": "^2.0.4",
169 | "redux-static": "^1.0.0",
170 | "rimraf": "^2.5.2",
171 | "style-loader": "^0.13.1",
172 | "url-loader": "^0.5.7",
173 | "webpack": "^1.13.1",
174 | "webpack-dev-middleware": "^1.6.1",
175 | "webpack-dev-server": "^1.14.1",
176 | "webpack-hot-middleware": "^2.10.0",
177 | "webpack-sources": "^0.1.2",
178 | "webpack-stats-plugin": "^0.1.1",
179 | "webtask-bundle": "^2.1.1"
180 | }
181 | }
182 |
--------------------------------------------------------------------------------