├── 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 | 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 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
    22 |
  1. 23 | Auth0 Dashboard 24 |
  2. 25 |
  3. 26 | Extensions 27 |
  4. 28 |
29 |

Management Api Webhooks

30 |
31 | 32 |
33 |
34 |
35 |
36 |
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 |
19 | 52 |
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 | 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 | --------------------------------------------------------------------------------