commit c92f737237df90476cac5a0b0fc8f1b60c77d226 Author: Mumshad Mannambeth Date: Wed Jun 7 13:36:44 2017 -0400 Initial Commit diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..33f4a0d --- /dev/null +++ b/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": ["es2015"], + "plugins": [ + "transform-class-properties" + ] +} diff --git a/.buildignore b/.buildignore new file mode 100644 index 0000000..e69de29 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c2cdfb8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..28a8da4 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,254 @@ +{ + "parser": "babel-eslint", + "env": { + "es6": true + }, + "globals": {}, + "plugins": [], + "rules": { + //Possible Errors + "comma-dangle": 0, //disallow or enforce trailing commas + "no-cond-assign": 2, //disallow assignment in conditional expressions + "no-console": 0, //disallow use of console in the node environment + "no-constant-condition": 1, //disallow use of constant expressions in conditions + "no-control-regex": 2, //disallow control characters in regular expressions + "no-debugger": 2, //disallow use of debugger + "no-dupe-args": 2, //disallow duplicate arguments in functions + "no-dupe-keys": 2, //disallow duplicate keys when creating object literals + "no-duplicate-case": 0, //disallow a duplicate case label. + "no-empty-character-class": 2, //disallow the use of empty character classes in regular expressions + "no-empty": 2, //disallow empty statements + "no-ex-assign": 2, //disallow assigning to the exception in a catch block + "no-extra-boolean-cast": 2, //disallow double-negation boolean casts in a boolean context + "no-extra-parens": 1, //disallow unnecessary parentheses + "no-extra-semi": 2, //disallow unnecessary semicolons + "no-func-assign": 2, //disallow overwriting functions written as function declarations + "no-inner-declarations": 1, //disallow function or variable declarations in nested blocks + "no-invalid-regexp": 2, //disallow invalid regular expression strings in the RegExp constructor + "no-irregular-whitespace": 2, //disallow irregular whitespace outside of strings and comments + "no-negated-in-lhs": 2, //disallow negation of the left operand of an in expression + "no-obj-calls": 2, //disallow the use of object properties of the global object (Math and JSON) as functions + "no-prototype-builtins": 0, //Disallow use of Object.prototypes builtins directly + "no-regex-spaces": 2, //disallow multiple spaces in a regular expression literal + "no-sparse-arrays": 1, //disallow sparse arrays + "no-unexpected-multiline": 2, //Avoid code that looks like two expressions but is actually one + "no-unreachable": 2, //disallow unreachable statements after a return, throw, continue, or break statement + "no-unsafe-finally": 2, //disallow control flow statements in finally blocks + "use-isnan": 2, //disallow comparisons with the value NaN + "valid-jsdoc": 0, //Ensure JSDoc comments are valid + "valid-typeof": 2, //Ensure that the results of typeof are compared against a valid string + + //Best Practices + "accessor-pairs": 0, //Enforces getter/setter pairs in objects + "array-callback-return": 2, //Enforces return statements in callbacks of array's methods + "block-scoped-var": 0, //treat var statements as if they were block scoped + "complexity": 0, //specify the maximum cyclomatic complexity allowed in a program + "consistent-return": 0, //require return statements to either always or never specify values + "curly": [2, "multi-line"], //specify curly brace conventions for all control statements + "default-case": 0, //require default case in switch statements + "dot-location": [2, "property"], //enforces consistent newlines before or after dots + "dot-notation": 2, //encourages use of dot notation whenever possible + "eqeqeq": 0, //require the use of === and !== + "guard-for-in": 0, //make sure for-in loops have an if statement + "no-alert": 2, //disallow the use of alert, confirm, and prompt + "no-caller": 0, //disallow use of arguments.caller or arguments.callee + "no-case-declarations": 0, //disallow lexical declarations in case clauses + "no-div-regex": 2, //disallow division operators explicitly at beginning of regular expression + "no-else-return": 0, //disallow else after a return in an if + "no-empty-function": 0, //disallow use of empty functions + "no-empty-pattern": 2, //disallow use of empty destructuring patterns + "no-eq-null": 2, //disallow comparisons to null without a type-checking operator + "no-eval": 2, //disallow use of eval() + "no-extend-native": 0, //disallow adding to native types + "no-extra-bind": 1, //disallow unnecessary function binding + "no-extra-label": 2, //disallow unnecessary labels + "no-fallthrough": 0, //disallow fallthrough of case statements + "no-floating-decimal": 0, //disallow the use of leading or trailing decimal points in numeric literals + "no-implicit-coercion": 0, //disallow the type conversions with shorter notations + "no-implicit-globals": 0, //disallow var and named functions in global scope + "no-implied-eval": 2, //disallow use of eval()-like methods + "no-invalid-this": 1, //disallow this keywords outside of classes or class-like objects + "no-iterator": 2, //disallow usage of __iterator__ property + "no-labels": 2, //disallow use of labeled statements + "no-lone-blocks": 2, //disallow unnecessary nested blocks + "no-loop-func": 2, //disallow creation of functions within loops + "no-magic-numbers": 0, //disallow the use of magic numbers + "no-multi-spaces": 2, //disallow use of multiple spaces + "no-multi-str": 0, //disallow use of multiline strings + "no-native-reassign": 2, //disallow reassignments of native objects + "no-new-func": 1, //disallow use of new operator for Function object + "no-new-wrappers": 2, //disallows creating new instances of String,Number, and Boolean + "no-new": 2, //disallow use of the new operator when not part of an assignment or comparison + "no-octal-escape": 0, //disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251"; + "no-octal": 0, //disallow use of octal literals + "no-param-reassign": 0, //disallow reassignment of function parameters + "no-process-env": 0, //disallow use of process.env + "no-proto": 2, //disallow usage of __proto__ property + "no-redeclare": 2, //disallow declaring the same variable more than once + "no-return-assign": 2, //disallow use of assignment in return statement + "no-script-url": 2, //disallow use of javascript: urls. + "no-self-assign": 2, //disallow assignments where both sides are exactly the same + "no-self-compare": 2, //disallow comparisons where both sides are exactly the same + "no-sequences": 2, //disallow use of the comma operator + "no-throw-literal": 2, //restrict what can be thrown as an exception + "no-unmodified-loop-condition": 0, //disallow unmodified conditions of loops + "no-unused-expressions": 0, //disallow usage of expressions in statement position + "no-unused-labels": 2, //disallow unused labels + "no-useless-call": 2, //disallow unnecessary .call() and .apply() + "no-useless-concat": 2, //disallow unnecessary concatenation of literals or template literals + "no-useless-escape": 2, //disallow unnecessary escape characters + "no-void": 0, //disallow use of the void operator + "no-warning-comments": 1, //disallow usage of configurable warning terms in comments (e.g. TODO or FIXME) + "no-with": 2, //disallow use of the with statement + "radix": 2, //require use of the second argument for parseInt() + "vars-on-top": 0, //require declaration of all vars at the top of their containing scope + "wrap-iife": 2, //require immediate function invocation to be wrapped in parentheses + "yoda": 2, //require or disallow Yoda conditions + + //Strict Mode + "strict": 0, //controls location of Use Strict Directives + + //Variables + "init-declarations": 0, //enforce or disallow variable initializations at definition + "no-catch-shadow": 2, //disallow the catch clause parameter name being the same as a variable in the outer scope + "no-delete-var": 2, //disallow deletion of variables + "no-label-var": 2, //disallow labels that share a name with a variable + "no-restricted-globals": 0, //restrict usage of specified global variables + "no-shadow-restricted-names": 2, //disallow shadowing of names such as arguments + "no-shadow": [2, {"allow": ["err"]}], //disallow declaration of variables already declared in the outer scope + "no-undef-init": 2, //disallow use of undefined when initializing variables + "no-undef": 2, //disallow use of undeclared variables unless mentioned in a /*global */ block + "no-undefined": 0, //disallow use of undefined variable + "no-unused-vars": 2, //disallow declaration of variables that are not used in the code + "no-use-before-define": 0, //disallow use of variables before they are defined + + //Node.js and CommonJS + "callback-return": 2, //enforce return after a callback + "global-require": 0, //enforce require() on top-level module scope + "handle-callback-err": 2, //enforce error handling in callbacks + "no-mixed-requires": 2, //disallow mixing regular variable and require declarations + "no-new-require": 2, //disallow use of new operator with the require function + "no-path-concat": 2, //disallow string concatenation with __dirname and __filename + "no-process-exit": 2, //disallow process.exit() + "no-restricted-imports": 0, //restrict usage of specified node imports + "no-restricted-modules": 0, //restrict usage of specified node modules + "no-sync": 1, //disallow use of synchronous methods + + //Stylistic Issues + "array-bracket-spacing": [2, "never"], //enforce spacing inside array brackets + "block-spacing": 0, //disallow or enforce spaces inside of single line blocks + "brace-style": 2, //enforce one true brace style + "camelcase": 1, //require camel case names + "comma-spacing": [2, {"before": false, "after": true}], //enforce spacing before and after comma + "comma-style": 2, //enforce one true comma style + "computed-property-spacing": 2, //require or disallow padding inside computed properties + "consistent-this": 2, //enforce consistent naming when capturing the current execution context + "eol-last": 2, //enforce newline at the end of file, with no multiple empty lines + "func-names": 0, //require function expressions to have a name + "func-style": 0, //enforce use of function declarations or expressions + "id-blacklist": 0, //blacklist certain identifiers to prevent them being used + "id-length": 0, //this option enforces minimum and maximum identifier lengths (variable names, property names etc.) + "id-match": 0, //require identifiers to match the provided regular expression + "indent": ["error", 2], //specify tab or space width for your code + "jsx-quotes": 0, //specify whether double or single quotes should be used in JSX attributes + "key-spacing": 2, //enforce spacing between keys and values in object literal properties + "keyword-spacing": [2, { + "before": true, + "after": true, + "overrides": { + "if": {"after": false}, + "for": {"after": false}, + "while": {"after": false}, + "catch": {"after": false} + } + }], //enforce spacing before and after keywords + "linebreak-style": 2, //disallow mixed 'LF' and 'CRLF' as linebreaks + "lines-around-comment": 0, //enforce empty lines around comments + "max-depth": 1, //specify the maximum depth that blocks can be nested + "max-len": [1, 200], //specify the maximum length of a line in your program + "max-lines": 0, //enforce a maximum file length + "max-nested-callbacks": 2, //specify the maximum depth callbacks can be nested + "max-params": 0, //limits the number of parameters that can be used in the function declaration. + "max-statements": 0, //specify the maximum number of statement allowed in a function + "max-statements-per-line": 0, //enforce a maximum number of statements allowed per line + "new-cap": 0, //require a capital letter for constructors + "new-parens": 2, //disallow the omission of parentheses when invoking a constructor with no arguments + "newline-after-var": 0, //require or disallow an empty newline after variable declarations + "newline-before-return": 0, //require newline before return statement + "newline-per-chained-call": [ + "error", + {"ignoreChainWithDepth": 2} + ], //enforce newline after each call when chaining the calls + "no-array-constructor": 2, //disallow use of the Array constructor + "no-bitwise": 0, //disallow use of bitwise operators + "no-continue": 0, //disallow use of the continue statement + "no-inline-comments": 0, //disallow comments inline after code + "no-lonely-if": 2, //disallow if as the only statement in an else block + "no-mixed-operators": 0, //disallow mixes of different operators + "no-mixed-spaces-and-tabs": 2, //disallow mixed spaces and tabs for indentation + "no-multiple-empty-lines": 2, //disallow multiple empty lines + "no-negated-condition": 0, //disallow negated conditions + "no-nested-ternary": 0, //disallow nested ternary expressions + "no-new-object": 2, //disallow the use of the Object constructor + "no-plusplus": 0, //disallow use of unary operators, ++ and -- + "no-restricted-syntax": 0, //disallow use of certain syntax in code + "no-spaced-func": 2, //disallow space between function identifier and application + "no-ternary": 0, //disallow the use of ternary operators + "no-trailing-spaces": 2, //disallow trailing whitespace at the end of lines + "no-underscore-dangle": 0, //disallow dangling underscores in identifiers + "no-unneeded-ternary": 2, //disallow the use of ternary operators when a simpler alternative exists + "no-whitespace-before-property": 2, //disallow whitespace before properties + "object-curly-newline": 0, //enforce consistent line breaks inside braces + "object-curly-spacing": 0, //require or disallow padding inside curly braces + "object-property-newline": 0, //enforce placing object properties on separate lines + "one-var": [2, "never"], //require or disallow one variable declaration per function + "one-var-declaration-per-line": 2, //require or disallow an newline around variable declarations + "operator-assignment": 0, //require assignment operator shorthand where possible or prohibit it entirely + "operator-linebreak": [1, "before"], //enforce operators to be placed before or after line breaks + "padded-blocks": [2, "never"], //enforce padding within blocks + "quote-props": [2, "as-needed"], //require quotes around object literal property names + "quotes": [2, "single"], //specify whether backticks, double or single quotes should be used + "require-jsdoc": 0, //Require JSDoc comment + "semi-spacing": 2, //enforce spacing before and after semicolons + "sort-imports": 0, //sort import declarations within module + "semi": 2, //require or disallow use of semicolons instead of ASI + "sort-vars": 0, //sort variables within the same declaration block + "space-before-blocks": 2, //require or disallow a space before blocks + "space-before-function-paren": [2, "never"], //require or disallow a space before function opening parenthesis + "space-in-parens": 2, //require or disallow spaces inside parentheses + "space-infix-ops": 2, //require spaces around operators + "space-unary-ops": 2, //require or disallow spaces before/after unary operators + "spaced-comment": 0, //require or disallow a space immediately following the // or /* in a comment + "unicode-bom": 0, //require or disallow the Unicode BOM + "wrap-regex": 0, //require regex literals to be wrapped in parentheses + + //ECMAScript 6 + "arrow-body-style": [2, "as-needed"], //require braces in arrow function body + "arrow-parens": [2, "as-needed"], //require parens in arrow function arguments + "arrow-spacing": 2, //require space before/after arrow function's arrow + "constructor-super": 2, //verify calls of super() in constructors + "generator-star-spacing": 0, //enforce spacing around the * in generator functions + "no-class-assign": 2, //disallow modifying variables of class declarations + "no-confusing-arrow": 2, //disallow arrow functions where they could be confused with comparisons + "no-const-assign": 2, //disallow modifying variables that are declared using const + "no-dupe-class-members": 2, //disallow duplicate name in class members + "no-duplicate-imports": 2, //disallow duplicate module imports + "no-new-symbol": 2, //disallow use of the new operator with the Symbol object + "no-this-before-super": 2, //disallow use of this/super before calling super() in constructors. + "no-useless-computed-key": 2, //disallow unnecessary computed property keys in object literals + "no-useless-constructor": 2, //disallow unnecessary constructor + "no-useless-rename": 2, //disallow renaming import, export, and destructured assignments to the same name + "no-var": 0, //require let or const instead of var + "object-shorthand": 1, //require method and property shorthand syntax for object literals + "prefer-arrow-callback": 0, //suggest using arrow functions as callbacks + "prefer-const": 0, //suggest using const declaration for variables that are never modified after declared + "prefer-reflect": 1, //suggest using Reflect methods where applicable + "prefer-rest-params": 1, //suggest using the rest parameters instead of arguments + "prefer-spread": 1, //suggest using the spread operator instead of .apply(). + "prefer-template": 1, //suggest using template literals instead of strings concatenation + "require-yield": 2, //disallow generator functions that do not have yield + "rest-spread-spacing": ["error", "never"], //enforce spacing between rest and spread operators and their expressions + "template-curly-spacing": 2, //enforce spacing around embedded expressions of template strings + "yield-star-spacing": [2, "after"] //enforce spacing around the * in yield* expressions + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d9a5563 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,43 @@ +* text=auto + +# These files are text and should be normalized (Convert crlf => lf) +*.php text +*.css text +*.js text +*.htm text +*.html text +*.xml text +*.txt text +*.ini text +*.inc text +.htaccess text + +# Denote all files that are truly binary and should not be modified. +# (binary is a macro for -text -diff) +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.mov binary +*.mp4 binary +*.mp3 binary +*.flv binary +*.fla binary +*.swf binary +*.gz binary +*.zip binary +*.7z binary +*.ttf binary + +# Documents +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c79b36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules +public +.tmp +.idea +client/bower_components +client/index.html +dist +/server/config/local.env.js +npm-debug.log +coverage diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..506cd16 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: node_js +node_js: + - 6 +matrix: + fast_finish: true + allow_failures: + - node_js: 5.12.0 +before_script: + - npm install -g gulp-cli node-gyp +services: mongodb +cache: + directories: + - node_modules +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/.yo-rc.json b/.yo-rc.json new file mode 100644 index 0000000..163881f --- /dev/null +++ b/.yo-rc.json @@ -0,0 +1,72 @@ +{ + "generator-angular-fullstack": { + "generatorVersion": "4.2.2", + "endpointDirectory": "server/api/", + "insertRoutes": true, + "registerRoutesFile": "server/routes.js", + "routesNeedle": "// Insert routes below", + "routesBase": "/api/", + "pluralizeRoutes": true, + "insertSockets": true, + "registerSocketsFile": "server/config/socketio.js", + "socketsNeedle": "// Insert sockets below", + "insertModels": true, + "registerModelsFile": "server/sqldb/index.js", + "modelsNeedle": "// Insert models below", + "filters": { + "js": true, + "babel": true, + "flow": false, + "html": true, + "css": true, + "uirouter": true, + "bootstrap": true, + "uibootstrap": true, + "auth": true, + "models": true, + "mongooseModels": true, + "mongoose": true, + "oauth": true, + "googleAuth": true, + "facebookAuth": true, + "mocha": true, + "jasmine": false, + "should": false, + "expect": true + } + }, + "generator-ng-component": { + "routeDirectory": "client/app/", + "directiveDirectory": "client/app/", + "componentDirectory": "client/app/components/", + "filterDirectory": "client/app/", + "serviceDirectory": "client/app/", + "basePath": "client", + "moduleName": "", + "modulePrompt": true, + "filters": [ + "uirouter", + "mocha", + "expect", + "uirouter", + "es6", + "webpack" + ], + "extensions": [ + "babel", + "js", + "html", + "css" + ], + "directiveSimpleTemplates": "", + "directiveComplexTemplates": "", + "filterTemplates": "", + "serviceTemplates": "", + "factoryTemplates": "", + "controllerTemplates": "", + "componentTemplates": "", + "decoratorTemplates": "", + "providerTemplates": "", + "routeTemplates": "" + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..46b0769 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +# Pull base image. +FROM node:6.2.2 + +# Reset Root Password +RUN echo "root:P@ssw0rd@123" | chpasswd + +# Install Ansible +RUN apt-get update && \ + apt-get install python-setuptools python-dev build-essential -y && \ + easy_install pip && \ + pip install ansible + +# TO fix a bug +RUN mkdir -p /root/.config/configstore && chmod g+rwx /root /root/.config /root/.config/configstore +RUN useradd -u 1003 -d /home/app_user -m -s /bin/bash -p $(echo P@ssw0rd@123 | openssl passwd -1 -stdin) app_user + +# Create data directory +RUN mkdir -p /data + +RUN chown -R app_user /usr/local && chown -R app_user /home/app_user && chown -R app_user /data + +# Install VIM and Openssh-Server +RUN apt-get update && apt-get install -y vim openssh-server + +# Permit Root login +RUN sed -i '/PermitRootLogin */cPermitRootLogin yes' /etc/ssh/sshd_config + +# Generate SSH Keys +RUN /usr/bin/ssh-keygen -A + +# Start Open-ssh server +RUN service ssh start + +# Change user to app_user +USER app_user + +RUN mkdir -p /data/web-app +COPY * /data/web-app + +USER root +RUN chown -R app_user /data/web-app + +USER app_user +WORKDIR /data/web-app + +RUN npm install -g yo gulp-cli generator-angular-fullstack +RUN npm install + +ENTRYPOINT gulp serve diff --git a/README.md b/README.md new file mode 100644 index 0000000..b31f6d2 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# app2 + +This project was generated with the [Angular Full-Stack Generator](https://github.com/DaftMonk/generator-angular-fullstack) version 4.2.2. + +## Getting Started + +### Prerequisites + +- [Git](https://git-scm.com/) +- [Node.js and npm](nodejs.org) Node >= 4.x.x, npm >= 2.x.x +- [Gulp](http://gulpjs.com/) (`npm install --global gulp`) +- [MongoDB](https://www.mongodb.org/) - Keep a running daemon with `mongod` + +### Developing + +1. Run `npm install` to install server dependencies. + +2. Run `mongod` in a separate shell to keep an instance of the MongoDB Daemon running + +3. Run `gulp serve` to start the development server. It should automatically open the client in your browser when ready. + +## Build & development + +Run `gulp build` for building and `gulp serve` for preview. + +## Testing + +Running `npm test` will run the unit tests with karma. diff --git a/client/.eslintrc b/client/.eslintrc new file mode 100644 index 0000000..b12322f --- /dev/null +++ b/client/.eslintrc @@ -0,0 +1,7 @@ +{ + "extends": "../.eslintrc", + "env": { + "browser": true, + "commonjs": true + } +} diff --git a/client/.htaccess b/client/.htaccess new file mode 100644 index 0000000..cb84cb9 --- /dev/null +++ b/client/.htaccess @@ -0,0 +1,543 @@ +# Apache Configuration File + +# (!) Using `.htaccess` files slows down Apache, therefore, if you have access +# to the main server config file (usually called `httpd.conf`), you should add +# this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html. + +# ############################################################################## +# # CROSS-ORIGIN RESOURCE SHARING (CORS) # +# ############################################################################## + +# ------------------------------------------------------------------------------ +# | Cross-domain AJAX requests | +# ------------------------------------------------------------------------------ + +# Enable cross-origin AJAX requests. +# http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity +# http://enable-cors.org/ + +# +# Header set Access-Control-Allow-Origin "*" +# + +# ------------------------------------------------------------------------------ +# | CORS-enabled images | +# ------------------------------------------------------------------------------ + +# Send the CORS header for images when browsers request it. +# https://developer.mozilla.org/en/CORS_Enabled_Image +# http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html +# http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/ + + + + + SetEnvIf Origin ":" IS_CORS + Header set Access-Control-Allow-Origin "*" env=IS_CORS + + + + +# ------------------------------------------------------------------------------ +# | Web fonts access | +# ------------------------------------------------------------------------------ + +# Allow access from all domains for web fonts + + + + Header set Access-Control-Allow-Origin "*" + + + + +# ############################################################################## +# # ERRORS # +# ############################################################################## + +# ------------------------------------------------------------------------------ +# | 404 error prevention for non-existing redirected folders | +# ------------------------------------------------------------------------------ + +# Prevent Apache from returning a 404 error for a rewrite if a directory +# with the same name does not exist. +# http://httpd.apache.org/docs/current/content-negotiation.html#multiviews +# http://www.webmasterworld.com/apache/3808792.htm + +Options -MultiViews + +# ------------------------------------------------------------------------------ +# | Custom error messages / pages | +# ------------------------------------------------------------------------------ + +# You can customize what Apache returns to the client in case of an error (see +# http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.: + +ErrorDocument 404 /404.html + + +# ############################################################################## +# # INTERNET EXPLORER # +# ############################################################################## + +# ------------------------------------------------------------------------------ +# | Better website experience | +# ------------------------------------------------------------------------------ + +# Force IE to render pages in the highest available mode in the various +# cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf. + + + Header set X-UA-Compatible "IE=edge" + # `mod_headers` can't match based on the content-type, however, we only + # want to send this header for HTML pages and not for the other resources + + Header unset X-UA-Compatible + + + +# ------------------------------------------------------------------------------ +# | Cookie setting from iframes | +# ------------------------------------------------------------------------------ + +# Allow cookies to be set from iframes in IE. + +# +# Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" +# + +# ------------------------------------------------------------------------------ +# | Screen flicker | +# ------------------------------------------------------------------------------ + +# Stop screen flicker in IE on CSS rollovers (this only works in +# combination with the `ExpiresByType` directives for images from below). + +# BrowserMatch "MSIE" brokenvary=1 +# BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1 +# BrowserMatch "Opera" !brokenvary +# SetEnvIf brokenvary 1 force-no-vary + + +# ############################################################################## +# # MIME TYPES AND ENCODING # +# ############################################################################## + +# ------------------------------------------------------------------------------ +# | Proper MIME types for all files | +# ------------------------------------------------------------------------------ + + + + # Audio + AddType audio/mp4 m4a f4a f4b + AddType audio/ogg oga ogg + + # JavaScript + # Normalize to standard type (it's sniffed in IE anyways): + # http://tools.ietf.org/html/rfc4329#section-7.2 + AddType application/javascript js jsonp + AddType application/json json + + # Video + AddType video/mp4 mp4 m4v f4v f4p + AddType video/ogg ogv + AddType video/webm webm + AddType video/x-flv flv + + # Web fonts + AddType application/font-woff woff + AddType application/vnd.ms-fontobject eot + + # Browsers usually ignore the font MIME types and sniff the content, + # however, Chrome shows a warning if other MIME types are used for the + # following fonts. + AddType application/x-font-ttf ttc ttf + AddType font/opentype otf + + # Make SVGZ fonts work on iPad: + # https://twitter.com/FontSquirrel/status/14855840545 + AddType image/svg+xml svg svgz + AddEncoding gzip svgz + + # Other + AddType application/octet-stream safariextz + AddType application/x-chrome-extension crx + AddType application/x-opera-extension oex + AddType application/x-shockwave-flash swf + AddType application/x-web-app-manifest+json webapp + AddType application/x-xpinstall xpi + AddType application/xml atom rdf rss xml + AddType image/webp webp + AddType image/x-icon ico + AddType text/cache-manifest appcache manifest + AddType text/vtt vtt + AddType text/x-component htc + AddType text/x-vcard vcf + + + +# ------------------------------------------------------------------------------ +# | UTF-8 encoding | +# ------------------------------------------------------------------------------ + +# Use UTF-8 encoding for anything served as `text/html` or `text/plain`. +AddDefaultCharset utf-8 + +# Force UTF-8 for certain file formats. + + AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml + + + +# ############################################################################## +# # URL REWRITES # +# ############################################################################## + +# ------------------------------------------------------------------------------ +# | Rewrite engine | +# ------------------------------------------------------------------------------ + +# Turning on the rewrite engine and enabling the `FollowSymLinks` option is +# necessary for the following directives to work. + +# If your web host doesn't allow the `FollowSymlinks` option, you may need to +# comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the +# performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks + +# Also, some cloud hosting services require `RewriteBase` to be set: +# http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site + + + Options +FollowSymlinks + # Options +SymLinksIfOwnerMatch + RewriteEngine On + # RewriteBase / + + +# ------------------------------------------------------------------------------ +# | Suppressing / Forcing the "www." at the beginning of URLs | +# ------------------------------------------------------------------------------ + +# The same content should never be available under two different URLs especially +# not with and without "www." at the beginning. This can cause SEO problems +# (duplicate content), therefore, you should choose one of the alternatives and +# redirect the other one. + +# By default option 1 (no "www.") is activated: +# http://no-www.org/faq.php?q=class_b + +# If you'd prefer to use option 2, just comment out all the lines from option 1 +# and uncomment the ones from option 2. + +# IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME! + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# Option 1: rewrite www.example.com → example.com + + + RewriteCond %{HTTPS} !=on + RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# Option 2: rewrite example.com → www.example.com + +# Be aware that the following might not be a good idea if you use "real" +# subdomains for certain parts of your website. + +# +# RewriteCond %{HTTPS} !=on +# RewriteCond %{HTTP_HOST} !^www\..+$ [NC] +# RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] +# + + +# ############################################################################## +# # SECURITY # +# ############################################################################## + +# ------------------------------------------------------------------------------ +# | Content Security Policy (CSP) | +# ------------------------------------------------------------------------------ + +# You can mitigate the risk of cross-site scripting and other content-injection +# attacks by setting a Content Security Policy which whitelists trusted sources +# of content for your site. + +# The example header below allows ONLY scripts that are loaded from the current +# site's origin (no inline scripts, no CDN, etc). This almost certainly won't +# work as-is for your site! + +# To get all the details you'll need to craft a reasonable policy for your site, +# read: http://html5rocks.com/en/tutorials/security/content-security-policy (or +# see the specification: http://w3.org/TR/CSP). + +# +# Header set Content-Security-Policy "script-src 'self'; object-src 'self'" +# +# Header unset Content-Security-Policy +# +# + +# ------------------------------------------------------------------------------ +# | File access | +# ------------------------------------------------------------------------------ + +# Block access to directories without a default document. +# Usually you should leave this uncommented because you shouldn't allow anyone +# to surf through every directory on your server (which may includes rather +# private places like the CMS's directories). + + + Options -Indexes + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# Block access to hidden files and directories. +# This includes directories used by version control systems such as Git and SVN. + + + RewriteCond %{SCRIPT_FILENAME} -d [OR] + RewriteCond %{SCRIPT_FILENAME} -f + RewriteRule "(^|/)\." - [F] + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# Block access to backup and source files. +# These files may be left by some text editors and can pose a great security +# danger when anyone has access to them. + + + Order allow,deny + Deny from all + Satisfy All + + +# ------------------------------------------------------------------------------ +# | Secure Sockets Layer (SSL) | +# ------------------------------------------------------------------------------ + +# Rewrite secure requests properly to prevent SSL certificate warnings, e.g.: +# prevent `https://www.example.com` when your certificate only allows +# `https://secure.example.com`. + +# +# RewriteCond %{SERVER_PORT} !^443 +# RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L] +# + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# Force client-side SSL redirection. + +# If a user types "example.com" in his browser, the above rule will redirect him +# to the secure version of the site. That still leaves a window of opportunity +# (the initial HTTP connection) for an attacker to downgrade or redirect the +# request. The following header ensures that browser will ONLY connect to your +# server via HTTPS, regardless of what the users type in the address bar. +# http://www.html5rocks.com/en/tutorials/security/transport-layer-security/ + +# +# Header set Strict-Transport-Security max-age=16070400; +# + +# ------------------------------------------------------------------------------ +# | Server software information | +# ------------------------------------------------------------------------------ + +# Avoid displaying the exact Apache version number, the description of the +# generic OS-type and the information about Apache's compiled-in modules. + +# ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`! + +# ServerTokens Prod + + +# ############################################################################## +# # WEB PERFORMANCE # +# ############################################################################## + +# ------------------------------------------------------------------------------ +# | Compression | +# ------------------------------------------------------------------------------ + + + + # Force compression for mangled headers. + # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping + + + SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding + RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding + + + + # Compress all output labeled with one of the following MIME-types + # (for Apache versions below 2.3.7, you don't need to enable `mod_filter` + # and can remove the `` and `` lines + # as `AddOutputFilterByType` is still in the core directives). + + AddOutputFilterByType DEFLATE application/atom+xml \ + application/javascript \ + application/json \ + application/rss+xml \ + application/vnd.ms-fontobject \ + application/x-font-ttf \ + application/x-web-app-manifest+json \ + application/xhtml+xml \ + application/xml \ + font/opentype \ + image/svg+xml \ + image/x-icon \ + text/css \ + text/html \ + text/plain \ + text/x-component \ + text/xml + + + + +# ------------------------------------------------------------------------------ +# | Content transformations | +# ------------------------------------------------------------------------------ + +# Prevent some of the mobile network providers from modifying the content of +# your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5. + +# +# Header set Cache-Control "no-transform" +# + +# ------------------------------------------------------------------------------ +# | ETag removal | +# ------------------------------------------------------------------------------ + +# Since we're sending far-future expires headers (see below), ETags can +# be removed: http://developer.yahoo.com/performance/rules.html#etags. + +# `FileETag None` is not enough for every server. + + Header unset ETag + + +FileETag None + +# ------------------------------------------------------------------------------ +# | Expires headers (for better cache control) | +# ------------------------------------------------------------------------------ + +# The following expires headers are set pretty far in the future. If you don't +# control versioning with filename-based cache busting, consider lowering the +# cache time for resources like CSS and JS to something like 1 week. + + + + ExpiresActive on + ExpiresDefault "access plus 1 month" + + # CSS + ExpiresByType text/css "access plus 1 year" + + # Data interchange + ExpiresByType application/json "access plus 0 seconds" + ExpiresByType application/xml "access plus 0 seconds" + ExpiresByType text/xml "access plus 0 seconds" + + # Favicon (cannot be renamed!) + ExpiresByType image/x-icon "access plus 1 week" + + # HTML components (HTCs) + ExpiresByType text/x-component "access plus 1 month" + + # HTML + ExpiresByType text/html "access plus 0 seconds" + + # JavaScript + ExpiresByType application/javascript "access plus 1 year" + + # Manifest files + ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" + ExpiresByType text/cache-manifest "access plus 0 seconds" + + # Media + ExpiresByType audio/ogg "access plus 1 month" + ExpiresByType image/gif "access plus 1 month" + ExpiresByType image/jpeg "access plus 1 month" + ExpiresByType image/png "access plus 1 month" + ExpiresByType video/mp4 "access plus 1 month" + ExpiresByType video/ogg "access plus 1 month" + ExpiresByType video/webm "access plus 1 month" + + # Web feeds + ExpiresByType application/atom+xml "access plus 1 hour" + ExpiresByType application/rss+xml "access plus 1 hour" + + # Web fonts + ExpiresByType application/font-woff "access plus 1 month" + ExpiresByType application/vnd.ms-fontobject "access plus 1 month" + ExpiresByType application/x-font-ttf "access plus 1 month" + ExpiresByType font/opentype "access plus 1 month" + ExpiresByType image/svg+xml "access plus 1 month" + + + +# ------------------------------------------------------------------------------ +# | Filename-based cache busting | +# ------------------------------------------------------------------------------ + +# If you're not using a build process to manage your filename version revving, +# you might want to consider enabling the following directives to route all +# requests such as `/css/style.12345.css` to `/css/style.css`. + +# To understand why this is important and a better idea than `*.css?v231`, read: +# http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring + +# +# RewriteCond %{REQUEST_FILENAME} !-f +# RewriteCond %{REQUEST_FILENAME} !-d +# RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L] +# + +# ------------------------------------------------------------------------------ +# | File concatenation | +# ------------------------------------------------------------------------------ + +# Allow concatenation from within specific CSS and JS files, e.g.: +# Inside of `script.combined.js` you could have +# +# +# and they would be included into this single file. + +# +# +# Options +Includes +# AddOutputFilterByType INCLUDES application/javascript application/json +# SetOutputFilter INCLUDES +# +# +# Options +Includes +# AddOutputFilterByType INCLUDES text/css +# SetOutputFilter INCLUDES +# +# + +# ------------------------------------------------------------------------------ +# | Persistent connections | +# ------------------------------------------------------------------------------ + +# Allow multiple requests to be sent over the same TCP connection: +# http://httpd.apache.org/docs/current/en/mod/core.html#keepalive. + +# Enable if you serve a lot of static content but, be aware of the +# possible disadvantages! + +# +# Header set Connection Keep-Alive +# diff --git a/client/_index.html b/client/_index.html new file mode 100644 index 0000000..7dfddd8 --- /dev/null +++ b/client/_index.html @@ -0,0 +1,36 @@ + + + + + + + Angular Full-Stack Generator + + + + + + + + + + + + + + + +
+ + + diff --git a/client/app/account/account.routes.js b/client/app/account/account.routes.js new file mode 100644 index 0000000..0370ed2 --- /dev/null +++ b/client/app/account/account.routes.js @@ -0,0 +1,37 @@ +'use strict'; + +export default function routes($stateProvider) { + 'ngInject'; + + $stateProvider.state('login', { + url: '/login', + template: require('./login/login.html'), + controller: 'LoginController', + controllerAs: 'vm' + }) + .state('logout', { + url: '/logout?referrer', + referrer: 'main', + template: '', + controller($state, Auth) { + 'ngInject'; + + var referrer = $state.params.referrer || $state.current.referrer || 'main'; + Auth.logout(); + $state.go(referrer); + } + }) + .state('signup', { + url: '/signup', + template: require('./signup/signup.html'), + controller: 'SignupController', + controllerAs: 'vm' + }) + .state('settings', { + url: '/settings', + template: require('./settings/settings.html'), + controller: 'SettingsController', + controllerAs: 'vm', + authenticate: true + }); +} diff --git a/client/app/account/index.js b/client/app/account/index.js new file mode 100644 index 0000000..032b1e4 --- /dev/null +++ b/client/app/account/index.js @@ -0,0 +1,24 @@ +'use strict'; + +import angular from 'angular'; + +import uiRouter from 'angular-ui-router'; + +import routing from './account.routes'; +import login from './login'; +import settings from './settings'; +import signup from './signup'; +import oauthButtons from '../../components/oauth-buttons'; + +export default angular.module('app2App.account', [uiRouter, login, settings, signup, oauthButtons]) + .config(routing) + .run(function($rootScope) { + 'ngInject'; + + $rootScope.$on('$stateChangeStart', function(event, next, nextParams, current) { + if(next.name === 'logout' && current && current.name && !current.authenticate) { + next.referrer = current.name; + } + }); + }) + .name; diff --git a/client/app/account/login/index.js b/client/app/account/login/index.js new file mode 100644 index 0000000..ec01fab --- /dev/null +++ b/client/app/account/login/index.js @@ -0,0 +1,8 @@ +'use strict'; + +import angular from 'angular'; +import LoginController from './login.controller'; + +export default angular.module('app2App.login', []) + .controller('LoginController', LoginController) + .name; diff --git a/client/app/account/login/login.controller.js b/client/app/account/login/login.controller.js new file mode 100644 index 0000000..c66a93c --- /dev/null +++ b/client/app/account/login/login.controller.js @@ -0,0 +1,38 @@ +'use strict'; + +export default class LoginController { + user = { + name: '', + email: '', + password: '' + }; + errors = { + login: undefined + }; + submitted = false; + + + /*@ngInject*/ + constructor(Auth, $state) { + this.Auth = Auth; + this.$state = $state; + } + + login(form) { + this.submitted = true; + + if(form.$valid) { + this.Auth.login({ + email: this.user.email, + password: this.user.password + }) + .then(() => { + // Logged in, redirect to home + this.$state.go('main'); + }) + .catch(err => { + this.errors.login = err.message; + }); + } + } +} diff --git a/client/app/account/login/login.html b/client/app/account/login/login.html new file mode 100644 index 0000000..f595f95 --- /dev/null +++ b/client/app/account/login/login.html @@ -0,0 +1,53 @@ +
+
+
+

Login

+

Accounts are reset on server restart from server/config/seed.js. Default account is test@example.com / test

+

Admin account is admin@example.com / admin

+
+
+
+ +
+ + + +
+ +
+ + + +
+ +
+

+ Please enter your email and password. +

+

+ Please enter a valid email. +

+ +

{{ vm.errors.login }}

+
+ +
+ + + Register + +
+ +
+
+
+ +
+
+
+
+
+
+
diff --git a/client/app/account/settings/index.js b/client/app/account/settings/index.js new file mode 100644 index 0000000..42dd6de --- /dev/null +++ b/client/app/account/settings/index.js @@ -0,0 +1,8 @@ +'use strict'; + +import angular from 'angular'; +import SettingsController from './settings.controller'; + +export default angular.module('app2App.settings', []) + .controller('SettingsController', SettingsController) + .name; diff --git a/client/app/account/settings/settings.controller.js b/client/app/account/settings/settings.controller.js new file mode 100644 index 0000000..cada554 --- /dev/null +++ b/client/app/account/settings/settings.controller.js @@ -0,0 +1,36 @@ +'use strict'; + +export default class SettingsController { + user = { + oldPassword: '', + newPassword: '', + confirmPassword: '' + }; + errors = { + other: undefined + }; + message = ''; + submitted = false; + + + /*@ngInject*/ + constructor(Auth) { + this.Auth = Auth; + } + + changePassword(form) { + this.submitted = true; + + if(form.$valid) { + this.Auth.changePassword(this.user.oldPassword, this.user.newPassword) + .then(() => { + this.message = 'Password successfully changed.'; + }) + .catch(() => { + form.password.$setValidity('mongoose', false); + this.errors.other = 'Incorrect password'; + this.message = ''; + }); + } + } +} diff --git a/client/app/account/settings/settings.html b/client/app/account/settings/settings.html new file mode 100644 index 0000000..690b0cf --- /dev/null +++ b/client/app/account/settings/settings.html @@ -0,0 +1,51 @@ +
+
+
+

Change Password

+
+
+
+ +
+ + + +

+ {{ vm.errors.other }} +

+
+ +
+ + + +

+ Password must be at least 3 characters. +

+
+ +
+ + + +

+ Passwords must match. +

+ +
+ +

{{ vm.message }}

+ + +
+
+
+
diff --git a/client/app/account/signup/index.js b/client/app/account/signup/index.js new file mode 100644 index 0000000..b967e18 --- /dev/null +++ b/client/app/account/signup/index.js @@ -0,0 +1,8 @@ +'use strict'; + +import angular from 'angular'; +import SignupController from './signup.controller'; + +export default angular.module('app2App.signup', []) + .controller('SignupController', SignupController) + .name; diff --git a/client/app/account/signup/signup.controller.js b/client/app/account/signup/signup.controller.js new file mode 100644 index 0000000..0ee9f68 --- /dev/null +++ b/client/app/account/signup/signup.controller.js @@ -0,0 +1,45 @@ +'use strict'; + +import angular from 'angular'; + +export default class SignupController { + user = { + name: '', + email: '', + password: '' + }; + errors = {}; + submitted = false; + + + /*@ngInject*/ + constructor(Auth, $state) { + this.Auth = Auth; + this.$state = $state; + } + + register(form) { + this.submitted = true; + + if(form.$valid) { + return this.Auth.createUser({ + name: this.user.name, + email: this.user.email, + password: this.user.password + }) + .then(() => { + // Account created, redirect to home + this.$state.go('main'); + }) + .catch(err => { + err = err.data; + this.errors = {}; + // Update validity of form fields that match the mongoose errors + angular.forEach(err.errors, (error, field) => { + form[field].$setValidity('mongoose', false); + this.errors[field] = error.message; + }); + }); + } + } +} diff --git a/client/app/account/signup/signup.html b/client/app/account/signup/signup.html new file mode 100644 index 0000000..962ea62 --- /dev/null +++ b/client/app/account/signup/signup.html @@ -0,0 +1,86 @@ +
+
+
+

Sign up

+
+
+
+ +
+ + + +

+ A name is required +

+
+ +
+ + + +

+ Doesn't look like a valid email. +

+

+ What's your email address? +

+

+ {{ vm.errors.email }} +

+
+ +
+ + + +

+ Password must be at least 3 characters. +

+

+ {{ vm.errors.password }} +

+
+ +
+ + +

+ Passwords must match. +

+
+ +
+ + +
+ +
+
+
+ +
+
+
+
+
+
+
diff --git a/client/app/admin/admin.controller.js b/client/app/admin/admin.controller.js new file mode 100644 index 0000000..c2b47d9 --- /dev/null +++ b/client/app/admin/admin.controller.js @@ -0,0 +1,14 @@ +'use strict'; + +export default class AdminController { + /*@ngInject*/ + constructor(User) { + // Use the User $resource to fetch all users + this.users = User.query(); + } + + delete(user) { + user.$remove(); + this.users.splice(this.users.indexOf(user), 1); + } +} diff --git a/client/app/admin/admin.css b/client/app/admin/admin.css new file mode 100644 index 0000000..3925cbe --- /dev/null +++ b/client/app/admin/admin.css @@ -0,0 +1,19 @@ +.trash { color:rgb(209, 91, 71); } + +.user-list li { + display: flex; + border: none; + border-bottom: 1px lightgray solid; + margin-bottom: 0; +} +.user-list li:last-child { + border-bottom: none; +} +.user-list li .user-info { + flex-grow: 1; +} +.user-list li .trash { + display: flex; + align-items: center; + text-decoration: none; +} diff --git a/client/app/admin/admin.html b/client/app/admin/admin.html new file mode 100644 index 0000000..cbbe68a --- /dev/null +++ b/client/app/admin/admin.html @@ -0,0 +1,12 @@ +
+

The delete user and user index api routes are restricted to users with the 'admin' role.

+ +
diff --git a/client/app/admin/admin.routes.js b/client/app/admin/admin.routes.js new file mode 100644 index 0000000..5302702 --- /dev/null +++ b/client/app/admin/admin.routes.js @@ -0,0 +1,13 @@ +'use strict'; + +export default function routes($stateProvider) { + 'ngInject'; + + $stateProvider.state('admin', { + url: '/admin', + template: require('./admin.html'), + controller: 'AdminController', + controllerAs: 'admin', + authenticate: 'admin' + }); +} diff --git a/client/app/admin/index.js b/client/app/admin/index.js new file mode 100644 index 0000000..27a3638 --- /dev/null +++ b/client/app/admin/index.js @@ -0,0 +1,10 @@ +'use strict'; + +import angular from 'angular'; +import routes from './admin.routes'; +import AdminController from './admin.controller'; + +export default angular.module('app2App.admin', ['app2App.auth', 'ui.router']) + .config(routes) + .controller('AdminController', AdminController) + .name; diff --git a/client/app/app.config.js b/client/app/app.config.js new file mode 100644 index 0000000..218cf46 --- /dev/null +++ b/client/app/app.config.js @@ -0,0 +1,9 @@ +'use strict'; + +export function routeConfig($urlRouterProvider, $locationProvider) { + 'ngInject'; + + $urlRouterProvider.otherwise('/'); + + $locationProvider.html5Mode(true); +} diff --git a/client/app/app.constants.js b/client/app/app.constants.js new file mode 100644 index 0000000..61b0bd2 --- /dev/null +++ b/client/app/app.constants.js @@ -0,0 +1,7 @@ +'use strict'; + +import angular from 'angular'; + +export default angular.module('app2App.constants', []) + .constant('appConfig', require('../../server/config/environment/shared')) + .name; diff --git a/client/app/app.css b/client/app/app.css new file mode 100644 index 0000000..01c3373 --- /dev/null +++ b/client/app/app.css @@ -0,0 +1,125 @@ +@import '~bootstrap/dist/css/bootstrap.css'; +@import '~bootstrap-social/bootstrap-social.css'; +/** + * Bootstrap Fonts + */ + +@font-face { + font-family: 'Glyphicons Halflings'; + src: url('/assets/fonts/bootstrap/glyphicons-halflings-regular.eot'); + src: url('/assets/fonts/bootstrap/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), + url('/assets/fonts/bootstrap/glyphicons-halflings-regular.woff') format('woff'), + url('/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf') format('truetype'), + url('/assets/fonts/bootstrap/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); +} + +@import '~font-awesome/css/font-awesome.css'; + +/** + *Font Awesome Fonts + */ + +@font-face { + font-family: 'FontAwesome'; + src: url('/assets/fonts/font-awesome/fontawesome-webfont.eot?v=4.1.0'); + src: url('/assets/fonts/font-awesome/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'), + url('/assets/fonts/font-awesome/fontawesome-webfont.woff?v=4.1.0') format('woff'), + url('/assets/fonts/font-awesome/fontawesome-webfont.ttf?v=4.1.0') format('truetype'), + url('/assets/fonts/font-awesome/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} + +/** + * App-wide Styles + */ + +.browserupgrade { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; +} + + +/* Component styles are injected through gulp */ +/* inject:css */ +@import 'admin/admin.css'; +@import 'main/main.css'; +@import '../components/footer/footer.css'; +@import '../components/modal/modal.css'; +@import '../components/oauth-buttons/oauth-buttons.css'; +/* endinject */ + +.navbar { + margin-bottom: 0; + background-color: #2d363a; + border-radius: 0; + border-top: 0; + border-right: 0; + border-left: 0; +} + +.navbar-brand { + padding: 0; + padding-top: 15px; + margin-right: 15px; +} + +.navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { + color: whitesmoke; + background-color: transparent; +} + +.navbar-default .navbar-brand { + color: white; +} + +.navbar-default .navbar-nav > li > a { + color: white; +} + +.navbar-brand-header { + width: 100%; + margin-left: 100px; + font-size: 25px; +} + +.navbar-brand-header a:hover, a:focus { + color: #2d363a; + text-decoration: none; +} + +#header li a.nav-button:hover { + background-color: #333; +} + +#header li.dropdown.open a.dropdown-toggle { + background-color: #333; +} + +#header #logo { + position: absolute; + top: 0; + left: 0; + width: 91px; + height: 70px; + text-align: center; + background-color: #2c95dd; + z-index: 100; +} + +#header #logo img { + margin-top: 30px; +} + +#header a { + color: #fff; +} + +#header .dropdown li a { + color: #333; +} + +.ace_editor { height: 800px; } + diff --git a/client/app/app.js b/client/app/app.js new file mode 100644 index 0000000..c0c45e2 --- /dev/null +++ b/client/app/app.js @@ -0,0 +1,126 @@ +'use strict'; + +import angular from 'angular'; +// import ngAnimate from 'angular-animate'; +import ngCookies from 'angular-cookies'; +import ngResource from 'angular-resource'; +import ngSanitize from 'angular-sanitize'; + +import uiRouter from 'angular-ui-router'; +import uiBootstrap from 'angular-ui-bootstrap'; +import 'angular-validation-match'; + + +import 'angular-ui-ace'; + +import 'yamljs/dist/yaml.min'; + +import 'ansi-to-html'; + +import 'angular-markdown-directive'; + +import treecontrol from 'angular-tree-control'; + +import 'angular-tree-control/css/tree-control-attribute.css'; +import 'angular-tree-control/css/tree-control.css'; + +import { + routeConfig +} from './app.config'; + +import _Auth from '../components/auth/auth.module'; +import account from './account'; +import admin from './admin'; +import navbar from '../components/navbar/navbar.component'; +import footer from '../components/footer/footer.component'; +import main from './main/main.component'; +import DesignerComponent from './designer/designer.component'; +import ProjectComponent from './project/project.component'; +import InventoryComponent from './designer/inventory/inventory.component'; +import PlaybookComponent from './designer/playbook/playbook.component'; +import FileBrowserComponent from './designer/file_browser/file_browser.component'; +import RolesComponent from './designer/roles/roles.component'; +import RunsComponent from './runs/runs.component'; +import CustomModulesComponent from './custom_modules/custom_modules.component'; + +import Projects from './services/projects/projects.service'; +import ansible from './services/ansible/ansible.service'; +import YAML from './providers/yaml/yaml.service'; +import yamlFile from './services/yamlFile/yamlFile.service'; + +import customModules from './custom_modules/custom_modules.service'; + +import ansi2html from './providers/ansi2html/ansi2html.service'; + +import constants from './app.constants'; +import util from '../components/util/util.module'; + +import NewInventoryController from './designer/inventory/new_inventory/new_inventory.controller'; + + +import NewGroupController from './designer/inventory/new_group/new_group.controller'; +import NewHostController from './designer/inventory/new_host/new_host.controller'; +import ComplexVarController from './directives/complexVar/complexVar.controller'; +import NewPlaybookController from './designer/playbook/new_playbook/new_playbook.controller'; +import ExecutionController from './designer/execution/execution.controller'; +import NewPlayController from './designer/playbook/new_play/new_play.controller'; +import NewTaskController from './designer/tasks/new_task/new_task.controller'; + +import NewFileController from './designer/roles/new_file/new_file.controller'; +import NewRoleController from './designer/roles/new_role/new_role.controller'; +import SearchRoleController from './designer/roles/search_role/search_role.controller'; + +import ComplexVarModalController from './modals/complex_var_modal/complex_var_modal.controller'; + +import NewModuleController from './custom_modules/new_module/new_module.controller'; + +import dictToKeyValueArray from './filters/dictToKeyValueArray/dictToKeyValueArray.filter'; +import dictToKeyValueArraySimple from './filters/dictToKeyValueArraySimple/dictToKeyValueArraySimple.filter'; +import keyValueArrayToDict from './filters/keyValueArrayToDict/keyValueArrayToDict.filter'; +import keyValueArrayToArray from './filters/keyValueArrayToArray/keyValueArrayToArray.filter'; +import addDotInKey from './filters/addDotInKey/addDotInKey.filter'; +import removeDotInKey from './filters/removeDotInKey/removeDotInKey.filter'; +import json2yaml from './filters/json2yaml/json2yaml.filter'; + +import complexVar from './directives/complexVar/complexVar.directive'; +import tasks from './designer/tasks/tasks.directive'; + +import editor from './services/editor/editor.service'; + +import './app.css'; + +angular.module('app2App', [ngCookies, ngResource, ngSanitize, uiRouter, uiBootstrap, _Auth, account, + admin, 'validation.match', 'ui.ace', navbar, footer, main, constants, util, ansi2html, + // Components + DesignerComponent, ProjectComponent, InventoryComponent, PlaybookComponent, FileBrowserComponent, RolesComponent, RunsComponent, CustomModulesComponent, + // Services + YAML, yamlFile, Projects, ansible, ansi2html, editor, customModules, + // Controllers + NewInventoryController, NewGroupController, NewHostController, ComplexVarController, NewPlaybookController, ExecutionController, NewPlayController, NewTaskController, ComplexVarModalController, + NewFileController, NewRoleController, SearchRoleController, NewModuleController, + // Filters + dictToKeyValueArray, dictToKeyValueArraySimple, keyValueArrayToDict, keyValueArrayToArray, addDotInKey, removeDotInKey, json2yaml, + // Directives + complexVar, tasks, treecontrol, 'btford.markdown' + +]) + .config(routeConfig) + .run(function($rootScope, $location, Auth) { + 'ngInject'; + // Redirect to login if route requires auth and you're not logged in + + $rootScope.$on('$stateChangeStart', function(event, next) { + Auth.isLoggedIn(function(loggedIn) { + if(next.authenticate && !loggedIn) { + $location.path('/login'); + } + }); + }); + }); + +angular.element(document) + .ready(() => { + angular.bootstrap(document, ['app2App'], { + strictDi: true + }); + }); diff --git a/client/app/custom_modules/custom_modules.component.js b/client/app/custom_modules/custom_modules.component.js new file mode 100644 index 0000000..ad01ebd --- /dev/null +++ b/client/app/custom_modules/custom_modules.component.js @@ -0,0 +1,235 @@ +'use strict'; +const angular = require('angular'); + +const uiRouter = require('angular-ui-router'); + +import routes from './custom_modules.routes'; + +export class CustomModulesComponent { + /*@ngInject*/ + constructor($scope,customModules,$sce,ansi2html,Projects,$uibModal,YAML) { + 'ngInject'; + + $scope.custom_modules = []; + $scope.selectedModule = {module:{},module_code:"",module_unchanged_code:""}; + $scope.selected_module_code = "something"; + + $scope.showNewModuleForm = {value:false}; + + $scope.getProjects = function(){ + $scope.projects = Projects.resource.query(function(){ + if($scope.projects.length){ + $scope.selectedProjectID = localStorage.selectedProjectID || $scope.projects[0]._id; + $scope.projectSelected($scope.selectedProjectID) + } + + }) + }; + + $scope.projectSelected = function(projectID){ + + localStorage.selectedProjectID = projectID; + + $scope.selectedProject = Projects.resource.get({id: projectID},function(){ + Projects.selectedProject = $scope.selectedProject; + $scope.getCustomModules(); + }) + }; + + $scope.getProjects(); + + $scope.getCustomModules = function(){ + customModules.get(function(response){ + console.log(response.data); + var lines = response.data.split("\n"); + if(lines.length) + lines = lines + .filter(function(line){return line.indexOf(".py") > -1}) + .map(function(item){return {name:item}}); + $scope.custom_modules = lines; + + if($scope.selectedModule.module.name){ + $scope.selectedModule.module = $scope.custom_modules.filter(function(item){ + return (item.name == $scope.selectedModule.module.name) + })[0] + } + + + }); + }; + + $scope.loadingModuleCode = false + + $scope.showModuleCode = function(module_name){ + $scope.loadingModuleCode = true; + if(!module_name){ + $scope.selectedModule.module_code = "Select a module"; + return; + } + customModules.show(module_name,function(response) { + $scope.loadingModuleCode = false; + $scope.selectedModule.module_code = response.data.split("Stream :: close")[0]; + $scope.selectedModule.module_unchanged_code = angular.copy($scope.selectedModule.module_code); + }); + }; + + $scope.$watch('selectedModule.module',function(newValue,oldValue){ + if(newValue.name && newValue.name !== oldValue.name){ + $scope.selectedModule.module_code = "Loading Module Code..."; + $scope.showModuleCode(newValue.name) + } + }); + + $scope.code_has_changed = false; + + $scope.codeChanged = function(){ + console.log("Code Changed"); + if($scope.selectedModule.module_unchanged_code !== $scope.selectedModule.module_code){ + $scope.code_has_changed = true + }else{ + $scope.code_has_changed = false + } + }; + + $scope.discardCodeChanges = function(){ + $scope.selectedModule.module_code = angular.copy($scope.selectedModule.module_unchanged_code); + }; + + $scope.saveModule = function(){ + $scope.saving = true; + customModules.save($scope.selectedModule.module.name,$scope.selectedModule.module_code,function(response){ + $scope.saving = false; + $scope.code_has_changed = false; + $scope.selectedModule.module_unchanged_code = angular.copy($scope.selectedModule.module_code); + console.log("Success") + },function(response){ + $scope.saving = false; + console.error(response.data) + }) + }; + + $scope.testModule = function(){ + + var re = /([^]+DOCUMENTATION = '''\s+)([^]+?)(\s+'''[^]+)/; + var module_string = $scope.selectedModule.module_code.replace(re,'$2'); + + $scope.selectedModuleObject = YAML.parse(module_string); + + //var options_copy = angular.copy($scope.selectedModuleObject.options); + var options_copy = {}; + /*options_copy = options_copy.map(function(item){ + var temp_obj = {}; + temp_obj[item.name] = ""; + return temp_obj + });*/ + + var module_name = $scope.selectedModule.module.name; + var module_cached_args = null; + + try{ + module_cached_args = JSON.parse(localStorage['test_args_'+module_name]); + }catch (e){ + console.log("Error getting cached arguments."); + module_cached_args = null; + } + + angular.forEach($scope.selectedModuleObject.options,function(value,key){ + //var temp_obj = {}; + //temp_obj[key] = ""; + options_copy[key] = ""; + + if(module_cached_args && key in module_cached_args){ + options_copy[key] = module_cached_args[key]; + } + + }); + + + var variable = {name:'',complexValue:options_copy}; + $scope.showComplexVariable(variable); + + }; + + $scope.newModule = function(){ + $scope.showNewModuleForm.value = true; + $scope.$broadcast ('newModule'); + }; + + $scope.editModule = function(){ + $scope.showNewModuleForm.value = true; + $scope.$broadcast ('editModule'); + }; + + + + $scope.showComplexVariable = function(variable){ + $scope.result = ""; + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createTaskContent.html',*/ + templateUrl: 'app/modals/complex_var_modal/complexVariable.html', + controller: 'ComplexVarModalController', + size: 'sm', + backdrop: 'static', + keyboard: false, + closeByEscape: false, + closeByDocument: false, + resolve: { + path: function () { + return variable.name + }, + hostvars: function(){ + return null + }, + members: function(){ + return variable.complexValue + } + } + }); + + modalInstance.result.then(function (module_args) { + + var module_name = $scope.selectedModule.module.name; + + /*var args = ""; + angular.forEach(selectedItem,function(value,key){ + + if(value){ + args += " " + key + "=" + value + } + + }); + + if(args){ + module_name += " -a " + args + }*/ + + localStorage['test_args_'+module_name] = JSON.stringify(module_args); + + $scope.testing = true; + + customModules.test(module_name,module_args,function(response) { + $scope.testing = false; + $scope.result = $sce.trustAsHtml(ansi2html.toHtml(response.data.split("Stream :: close")[0]).replace(/\n/g, "
")); + }, + function(response) { + $scope.testing = false; + $scope.result = $sce.trustAsHtml(ansi2html.toHtml(response.data.split("Stream :: close")[0]).replace(/\n/g, "
")); + }); + + }, function () { + + }); + + } + } +} + +export default angular.module('webAppApp.custom_modules', [uiRouter]) + .config(routes) + .component('customModules', { + template: require('./custom_modules.html'), + controller: CustomModulesComponent, + controllerAs: 'customModulesCtrl' + }) + .name; diff --git a/client/app/custom_modules/custom_modules.component.spec.js b/client/app/custom_modules/custom_modules.component.spec.js new file mode 100644 index 0000000..635b941 --- /dev/null +++ b/client/app/custom_modules/custom_modules.component.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Component: CustomModulesComponent', function() { + // load the controller's module + beforeEach(module('webAppApp.custom_modules')); + + var CustomModulesComponent; + + // Initialize the controller and a mock scope + beforeEach(inject(function($componentController) { + CustomModulesComponent = $componentController('custom_modules', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/custom_modules/custom_modules.html b/client/app/custom_modules/custom_modules.html new file mode 100644 index 0000000..7231f0c --- /dev/null +++ b/client/app/custom_modules/custom_modules.html @@ -0,0 +1,55 @@ +
+
+
+
+ + + +
+ + + + + + + + + + + + + + + + +
SelectName
+ {{module.name}}
+
+ + + + +
+

+
+
+ +
+ +
+
+
+ +
+ +
+
+ diff --git a/client/app/custom_modules/custom_modules.routes.js b/client/app/custom_modules/custom_modules.routes.js new file mode 100644 index 0000000..3fe2542 --- /dev/null +++ b/client/app/custom_modules/custom_modules.routes.js @@ -0,0 +1,10 @@ +'use strict'; + +export default function($stateProvider) { + 'ngInject'; + $stateProvider + .state('custom_modules', { + url: '/custom_modules', + template: '' + }); +} diff --git a/client/app/custom_modules/custom_modules.service.js b/client/app/custom_modules/custom_modules.service.js new file mode 100644 index 0000000..55e0dee --- /dev/null +++ b/client/app/custom_modules/custom_modules.service.js @@ -0,0 +1,30 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function customModulesService($http,Projects) { + // AngularJS will instantiate a singleton by calling "new" on this function + + var uri = '/api/custom_modules'; + + this.get = function(successCallback,errorCallback){ + $http.post(uri + '/query',{ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.show = function(customModule,successCallback,errorCallback){ + $http.post(uri+ '/' + customModule+'/get',{ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.test = function(customModule,module_args,successCallback,errorCallback){ + $http.post(uri + '/' + customModule + '/test',{ansibleEngine:Projects.selectedProject.ansibleEngine,moduleArgs:module_args}).then(successCallback,errorCallback) + }; + + this.save = function(customModule,customModuleCode,successCallback,errorCallback){ + $http.post(uri + '/' + customModule,{ansibleEngine:Projects.selectedProject.ansibleEngine,custom_module_code:customModuleCode}).then(successCallback,errorCallback) + } + +} + +export default angular.module('webAppApp.custom_modules_service', []) + .service('customModules', customModulesService) + .name; diff --git a/client/app/custom_modules/custom_modules.service.spec.js b/client/app/custom_modules/custom_modules.service.spec.js new file mode 100644 index 0000000..c6c3204 --- /dev/null +++ b/client/app/custom_modules/custom_modules.service.spec.js @@ -0,0 +1,16 @@ +'use strict'; + +describe('Service: customModules', function() { + // load the service's module + beforeEach(module('webAppApp.custom_modules')); + + // instantiate service + var customModules; + beforeEach(inject(function(_customModules_) { + customModules = _customModules_; + })); + + it('should do something', function() { + expect(!!customModules).to.be.true; + }); +}); diff --git a/client/app/custom_modules/new_module/new_module.controller.js b/client/app/custom_modules/new_module/new_module.controller.js new file mode 100644 index 0000000..c896d17 --- /dev/null +++ b/client/app/custom_modules/new_module/new_module.controller.js @@ -0,0 +1,280 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function newModuleController($scope,$filter,customModules,ansible,YAML) { + + $scope.optionTypes = ['str','list','dict','bool','int','float','path','raw','jsonarg','json','bytes','bits']; + + var defaultModule = { + module:null, + short_description:"", + description:"", + version_added:"", + author:"", + notes: "", + requirements: "", + options:[ + { + name:"parameter1", + description: 'Description of parameter 1', + required: true, + default: null, + choices: '"choice1", "choice2"', + aliases: '"option1", "argument1"', + type: "" + } + ] + }; + + + $scope.loadDefaultTemplate = function(){ + $scope.newModule = angular.copy(defaultModule); + $scope.selectedModule.module_code = "Loading Template.."; + + customModules.show('template.py',function(response) { + $scope.selectedModule.module_code = response.data.split("Stream :: close")[0]; + }); + }; + + $scope.$watch('newModule',function(newValue,oldValue){ + + updateDocumentation(newValue); + updateParameters(newValue); + updateExamples(newValue); + + },true); + + var updateParameters = function(newValue){ + newValue = angular.copy(newValue); + + var parameters_definition_lines = []; + var parameters_retreive_lines = []; + angular.forEach(newValue.options,function(option){ + if(option.name) { + var line = option.name + "=dict("; + + var line_arguments = []; + if (option.required)line_arguments.push("required=True"); + if (!option.required && option.default)line_arguments.push("default='" + option.default + "'"); + if (option.type)line_arguments.push("type='" + option.type + "'"); + if (option.choices)line_arguments.push("choices=[" + option.choices + "]"); + if (option.aliases)line_arguments.push("aliases=[" + option.aliases + "]"); + + line += line_arguments.join(","); + line += ")"; + + parameters_definition_lines.push(line); + parameters_retreive_lines.push(option.name + ' = module.params[\'' + option.name + '\']') + } + }); + + var parameters_definition_string = parameters_definition_lines.join(",\n "); + var parameters_retreive_string = parameters_retreive_lines.join("\n "); + + var re = /(# <--Begin Parameter Definition -->\s+ )([^]+)(\s+ # <--END Parameter Definition -->)/; + $scope.selectedModule.module_code = $scope.selectedModule.module_code.replace(re,"$1" + parameters_definition_string + "$3"); + + var supports_check_mode_string = '\n'; + if(newValue.supports_check_mode){ + supports_check_mode_string = '\n supports_check_mode=True\n' + } + + var re2 = /(# <--Begin Supports Check Mode -->)([^]+)( # <--End Supports Check Mode -->)/; + $scope.selectedModule.module_code = $scope.selectedModule.module_code.replace(re2,"$1" + supports_check_mode_string + "$3"); + + var re3 = /(# <--Begin Retreiving Parameters -->\s+ )([^]+)(\s+ # <--End Retreiving Parameters -->)/; + $scope.selectedModule.module_code = $scope.selectedModule.module_code.replace(re3,"$1" + parameters_retreive_string + "$3"); + + }; + + var updateDocumentation = function(newValue){ + newValue = angular.copy(newValue); + newValue.options = convertOptionsToObject(newValue.options); + + delete newValue['supports_check_mode']; + + if(newValue.description) + newValue.description = newValue.description.split(";"); + + if(newValue.notes) + newValue.notes = newValue.notes.split(";"); + + if(newValue.requirements) + newValue.requirements = newValue.requirements.split(";"); + + $scope.documentation_yaml = '---\n' + $filter('json2yaml')(angular.toJson(newValue)).toString().replace(/__dot__/g,"."); + + //var re = /(.*DOCUMENTATION = '''\n)([^]+?)(\n'''.*)/; + var re = /([^]+DOCUMENTATION = '''\s+)([^]+?)(\s+'''[^]+)/; + $scope.selectedModule.module_code = $scope.selectedModule.module_code.replace(re,'$1' + $scope.documentation_yaml + '$3'); + }; + + var updateExamples = function(newValue){ + newValue = angular.copy(newValue); + + var moduleCopy = { + + }; + + moduleCopy[newValue.module] = convertOptionsToExampleObject(newValue.options); + + $scope.example_yaml = YAML.stringify(moduleCopy,4); + + //var re = /(.*DOCUMENTATION = '''\n)([^]+?)(\n'''.*)/; + var re = /([^]+EXAMPLES = '''[^]+# <-- -->\s+)([^]+?)(\s+# <-- \/ -->\s+'''[^]+)/; + $scope.selectedModule.module_code = $scope.selectedModule.module_code.replace(re,'$1' + $scope.example_yaml + '$3'); + }; + + var convertOptionsToObject = function(options){ + + var result = {}; + + angular.forEach(options,function(option){ + if(option.name){ + result[option.name] = { + description: option.description + }; + + if(option.required) + result[option.name]['required'] = "True"; + else + delete result[option.name]['required']; + + if(!option.required && option.default) + result[option.name]['default'] = option.default; + + if(option.choices){ + result[option.name]['choices'] = "[" + option.choices + "]" + } + + if(option.aliases){ + result[option.name]['aliases'] = "[" + option.aliases + "]" + } + } + + }); + + return result + + }; + + var convertOptionsToExampleObject = function(options){ + + var result = {}; + + angular.forEach(options,function(option){ + if(option.name){ + result[option.name] = "value"; + } + }); + + return result + + }; + + var convertOptionsToArrays = function(options){ + + var result = []; + + angular.forEach(options,function(value,key){ + var option = { + name: key, + description: value.description, + required: value.required, + default:value.default + }; + + if(value.choices && value.choices.length) + option['choices'] = value.choices.map(function(item){return ('"' + item + '"')}).join(",") + + if(value.aliases && value.aliases.length) + option['aliases'] = value.aliases.map(function(item){return ('"' + item + '"')}).join(",") + + result.push(option) + }); + + return result + + }; + + $scope.saveNewModule = function(){ + $scope.saving = true; + customModules.save($scope.newModule.module + '.py',$scope.selectedModule.module_code,function(response){ + $scope.saving = false; + $scope.getCustomModules(); + + ansible.getAnsibleModules(function(response){ + + }, function(response){ + + },null,true); + $scope.cancelNewModule(); + },function(response){ + $scope.saving = false; + console.error(response.data) + }) + }; + + $scope.cancelNewModule = function(){ + $scope.showNewModuleForm.value = false; + $scope.$parent.showModuleCode($scope.selectedModule.module.name) + }; + + var getPropertiesFromCode = function(module_code){ + + //var re = /([^]+DOCUMENTATION = '''\n)([^]+?)(\n'''[^]+)/; + var re = /([^]+DOCUMENTATION = '''\s+)([^]+?)(\s+'''[^]+)/; + var module_string = $scope.selectedModule.module_code.replace(re,'$2'); + + $scope.newModule = YAML.parse(module_string); + $scope.newModule.options = convertOptionsToArrays($scope.newModule.options); + + if($scope.newModule.description && $scope.newModule.description.length) + $scope.newModule.description = $scope.newModule.description.join(";"); + + if($scope.newModule.notes && $scope.newModule.notes.length) + $scope.newModule.notes = $scope.newModule.notes.join(";"); + + if($scope.newModule.requirements && $scope.newModule.requirements.length) + $scope.newModule.requirements = $scope.newModule.requirements.join(";"); + + + re = /([^]+# <--Begin Parameter Definition -->\s+ )([^]+)(\s+ # <--END Parameter Definition -->[^]+)/; + var parameter_string = $scope.selectedModule.module_code.replace(re,"$2"); + + // Read property type form parameter definition + re = /\s+(.*?)=.*type=(.*?)[,\)].*/g; + var m; + + while ((m = re.exec(parameter_string)) !== null) { + if (m.index === re.lastIndex) { + re.lastIndex++; + } + // View your result using the m-variable. + // eg m[0] etc. + if(m[1]){ + angular.forEach($scope.newModule.options,function(option){ + if(option.name === m[1]){ + option.type = m[2].replace(/'/g,'') + } + }) + } + + } + + + }; + + $scope.$on('editModule', function(e) { + getPropertiesFromCode($scope.selected_module_code) + }); + + $scope.$on('newModule', function(e) { + $scope.loadDefaultTemplate(); + }); +} + +export default angular.module('webAppApp.new_module', []) + .controller('NewModuleController', newModuleController) + .name; diff --git a/client/app/custom_modules/new_module/new_module.controller.spec.js b/client/app/custom_modules/new_module/new_module.controller.spec.js new file mode 100644 index 0000000..b5219bf --- /dev/null +++ b/client/app/custom_modules/new_module/new_module.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: NewModuleCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.new_module')); + + var NewModuleCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + NewModuleCtrl = $controller('NewModuleCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/custom_modules/new_module/new_module.html b/client/app/custom_modules/new_module/new_module.html new file mode 100644 index 0000000..aa0dcf6 --- /dev/null +++ b/client/app/custom_modules/new_module/new_module.html @@ -0,0 +1,144 @@ +
+ +
+
+
New Module
+
+
+
+

+ + +

+
+
+

+ + +

+
+
+

+ + +

+
+
+

+ + +

+
+
+ +
+
+

+ + +

+
+
+

+ + +

+
+
+ + +

+ + +

+ +

+ + +

+ +
+
+
Options
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ + +
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+ +
+ +
+
+
+ + +
+
+ + + + + +
+ + +
diff --git a/client/app/designer/designer.component.js b/client/app/designer/designer.component.js new file mode 100644 index 0000000..0c2a381 --- /dev/null +++ b/client/app/designer/designer.component.js @@ -0,0 +1,89 @@ +'use strict'; +const angular = require('angular'); + +const uiRouter = require('angular-ui-router'); + +import routes from './designer.routes'; + +export class DesignerComponent { + /*@ngInject*/ + constructor($scope,Projects,ansible) { + 'ngInject'; + $scope.selectedInventoryFileName = null; + + /** + * Get list of projects from server + */ + $scope.getProjects = function(){ + $scope.projects = Projects.resource.query(function(){ + if($scope.projects.length){ + $scope.selectedProjectID = localStorage.selectedProjectID || $scope.projects[0]._id; + $scope.projectSelected($scope.selectedProjectID) + } + + }) + }; + + /** + * On ProjectSelected - set selectedProjectID in cache + * @param projectID + */ + $scope.projectSelected = function(projectID){ + localStorage.selectedProjectID = projectID; + + $scope.selectedProject = Projects.resource.get({id: projectID},function(){ + Projects.selectedProject = $scope.selectedProject; + $scope.listOfInventoryFiles(); + $scope.$broadcast('projectLoaded'); + }) + + }; + + + /** + * Get List of inventory files in project root folder + */ + $scope.listOfInventoryFiles = function(){ + + var rolesTestFolder = null; + + /*if(roleName){ + rolesTestFolder = projectFolder + '/' + roleName + '/tests' + }*/ + + ansible.getInventoryList(function(response){ + $scope.inventoryFiles = response.data; + console.log($scope.inventoryFiles); + Projects.selectedInventoryFileName = localStorage.selectedInventoryFileName || $scope.inventoryFiles[0]; + localStorage.selectedInventoryFileName = $scope.inventoryFiles[0]; + $scope.selectedInventoryFileName = localStorage.selectedInventoryFileName + }, + function(response){ + $scope.err_msg = response.data + },rolesTestFolder) + }; + + /** + * Set selected inventory file in local cache. + * @param selectedInventoryFileName - Selected inventory file name + */ + $scope.inventoryFileSelected = function(selectedInventoryFileName){ + localStorage.selectedInventoryFileName = selectedInventoryFileName; + }; + + /** + * Main - Get Projects + */ + + $scope.getProjects(); + } +} + +export default angular.module('webAppApp.designer', [uiRouter]) + .config(routes) + .component('designer', { + template: require('./designer.html'), + controller: DesignerComponent, + controllerAs: 'designerCtrl' + }) + .name; diff --git a/client/app/designer/designer.component.spec.js b/client/app/designer/designer.component.spec.js new file mode 100644 index 0000000..ad0bc81 --- /dev/null +++ b/client/app/designer/designer.component.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Component: DesignerComponent', function() { + // load the controller's module + beforeEach(module('webAppApp.designer')); + + var DesignerComponent; + + // Initialize the controller and a mock scope + beforeEach(inject(function($componentController) { + DesignerComponent = $componentController('designer', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/designer.css b/client/app/designer/designer.css new file mode 100644 index 0000000..791fbcc --- /dev/null +++ b/client/app/designer/designer.css @@ -0,0 +1,10 @@ +.logconsole { + font:12px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas'; + color: #fff; + padding:5px; + margin:5px; + font-size:14px; + background: black; +} + +.ace_editor { height: 800px; } diff --git a/client/app/designer/designer.html b/client/app/designer/designer.html new file mode 100644 index 0000000..70def7a --- /dev/null +++ b/client/app/designer/designer.html @@ -0,0 +1,55 @@ +
+
+
+
+
+
Projects
+
+ +
+ Ansible Version:{{selectedProject.ansibleVersion}} +
+
+
+
+ +
+
+
Inventory Files
+
+ +
+ An inventory file to work with +
+
+
+
+ +
+
+
Menu
+
+ +
+ + + + +
+ +
+
+
+
{{err_msg}}
+
+ +
+ +
+ +
+ +
+
diff --git a/client/app/designer/designer.routes.js b/client/app/designer/designer.routes.js new file mode 100644 index 0000000..c559162 --- /dev/null +++ b/client/app/designer/designer.routes.js @@ -0,0 +1,10 @@ +'use strict'; + +export default function($stateProvider) { + 'ngInject'; + $stateProvider + .state('designer', { + url: '/designer', + template: '' + }); +} diff --git a/client/app/designer/execution/executeModal.html b/client/app/designer/execution/executeModal.html new file mode 100644 index 0000000..4bd5d1f --- /dev/null +++ b/client/app/designer/execution/executeModal.html @@ -0,0 +1,185 @@ + + + + diff --git a/client/app/designer/execution/execution.controller.js b/client/app/designer/execution/execution.controller.js new file mode 100644 index 0000000..15f270d --- /dev/null +++ b/client/app/designer/execution/execution.controller.js @@ -0,0 +1,475 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function executionController($scope,$sce, $uibModalInstance, $timeout, ansi2html, ansible, tags, selectedProject, selectedPlaybook, selectedPlay, executionType, executionName, readOnly, runData, projectFolder, roleName) { + 'ngInject'; + $scope.view = 'console'; + $scope.selectedInventoryFile = {value:null}; + $scope.verbose_detail = {value:null}; + $scope.verbose = {value:'verbose'}; + $scope.check_mode = { + value: 'No_Check' + }; + $scope.additional_tags = {show: false}; + $scope.additional_vars = {show: false}; + $scope.refreshLog = true; + $scope.all_tags = []; + $scope.all_hosts = []; + $scope.readOnly = readOnly; + $scope.readyForPlay = false; + + /** + * Execute Ansible Playbook + */ + $scope.executeAnsiblePlayBook = function(){ + $scope.AnsiblePlayBookLoading = true; + var reqBody = {}; + //reqBody.inventory_file_contents = inventory_file_contents; + //reqBody.playbook_file_contents = yaml; + //reqBody.tags = tags || []; + reqBody.tags = []; + + $scope.all_tags.map(tag => { + if(tag.selected){ + if(tag.name) + reqBody.tags.push(tag.name.trim()) + } + }); + + reqBody.limit_to_hosts = []; + + $scope.all_hosts.map(host => { + if(host.selected){ + if(host.name) + reqBody.limit_to_hosts.push(host.name.trim()) + } + }); + + reqBody.verbose = $scope.verbose_detail.value || $scope.verbose.value; + reqBody.check_mode = $scope.check_mode.value; + + reqBody.inventory_file_name = $scope.selectedInventoryFile.value; + + if(roleName){ + reqBody.inventory_file_name = roleName + '/tests/' + reqBody.inventory_file_name; + } + + + console.log("Check Mode = " + reqBody.check_mode); + + reqBody.selectedPlaybook = selectedPlaybook.playbook; + reqBody.executionType = executionType; + reqBody.executionName = executionName; + + reqBody.ansibleEngine = angular.copy(selectedProject.ansibleEngine); + + // Override project folder for roles + if(projectFolder) + reqBody.ansibleEngine.projectFolder = projectFolder; + + if(selectedPlay && selectedPlay.play) + reqBody.host = selectedPlay.play.hosts; + + $scope.result = "Running..."; + + ansible.executeAnsiblePlayBook(reqBody,function(response){ + //$scope.result = $sce.trustAsHtml(ansi2html.toHtml(response.data).replace(/\n/g, "
")); + $scope.refreshLog = true; + $scope.executionData = response.data; + + setTimeout(function(){ + $scope.refreshLogs(); + },3000); + + }, function(response){ + if(response.data) + $scope.result = $sce.trustAsHtml(ansi2html.toHtml(response.data).replace(/\n/g, "
")); + $scope.AnsiblePlayBookLoading = false; + console.log("error" + $scope.result) + }, 'PlaybookExecutionModal'); + + }; + + /*setTimeout(function(){ + $scope.executeAnsiblePlayBook(); + },200);*/ + + + /** + * Get logs + */ + $scope.getLogs = function(){ + ansible.getLogs($scope.executionData,function(successResponse) { + $scope.result = $sce.trustAsHtml(ansi2html.toHtml(successResponse.data.replace('SCRIPT_FINISHED','')).replace(/\n/g, "
")); + + if(successResponse.data.indexOf('SCRIPT_FINISHED') > -1){ + $scope.refreshLog = false; + $scope.AnsiblePlayBookLoading = false; + } + $scope.processAnsibleOutput(successResponse.data) + + }); + }; + + /** + * Refersh Logs + */ + $scope.refreshLogs = function(){ + if($scope.logRefreshTimer){ + $timeout.cancel( $scope.logRefreshTimer ); + } + + $scope.getLogs(); + $scope.logRefreshTimer = $timeout( + function(){ + //$scope.getLogs(tile); + if($scope.refreshLog) { + $scope.refreshLogs(); + } + }, + 10000 + ); + + $scope.$on( + "$destroy", + function( event ) { + $timeout.cancel( $scope.logRefreshTimer ); + } + ); + + }; + + /** + * Close the modal + */ + $scope.ok = function () { + $uibModalInstance.close(null); + }; + + /** + * Cancel modal + */ + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; + + /** + * Run a random command on the server + * TODO: Remove this later. + * @param command + */ + $scope.runCommand = function(command){ + command = command || $scope.command; + ansible.executeCommand( command, + function(response){ + $scope.result = $sce.trustAsHtml(ansi2html.toHtml(response.data).replace(/\n/g, "
").replace(/ /g," ")); + + }, function(response){ + $scope.result = $sce.trustAsHtml(ansi2html.toHtml(response.data).replace(/\n/g, "
")); + + }) + }; + + /** + * Run Ansible Playbook + * @constructor + */ + $scope.Run = function(){ + $scope.executeAnsiblePlayBook(); + }; + + /** + * This is used when viewing the logs from the Runs view + */ + if($scope.readOnly){ + $scope.executionData = runData; + $scope.refreshLog = true; + $scope.refreshLogs() + } + + /** + * Get List of inventory files + */ + $scope.listOfInventoryFiles = function(){ + + var rolesTestFolder = null; + + if(roleName){ + rolesTestFolder = projectFolder + '/' + roleName + '/tests' + } + + ansible.getInventoryList(function(response){ + $scope.inventoryFiles = response.data; + if($scope.inventoryFiles.length) + $scope.selectedInventoryFile = {value:$scope.inventoryFiles[0]}; + /** + * Run Get Tags + */ + if(!readOnly) + $scope.getTags(); + }, + function(response){ + /*$scope.err_msg = response.data;*/ + $scope.result = $sce.trustAsHtml(ansi2html.toHtml(response.data).replace(/\n/g, "
")); + $scope.view = 'console' + },rolesTestFolder) + }; + + $scope.listOfInventoryFiles(); + + /** + * Get List of Tags based on playbook and inventory file + */ + $scope.getTags = function(){ + var inventory_file_name = $scope.selectedInventoryFile.value; + + if(roleName){ + inventory_file_name = roleName + '/tests/' + inventory_file_name; + } + + var selectedPlaybookName = selectedPlaybook.playbook; + + var ansibleEngine = angular.copy(selectedProject.ansibleEngine); + + // Override project folder for roles + if(projectFolder) + ansibleEngine.projectFolder = projectFolder; + + ansible.getTagList(selectedPlaybookName,inventory_file_name,ansibleEngine, + function(response){ + console.log(response.data) + + /*var re = /TAGS: \[(.*)\]/g; + var m; + + var all_tags = [] + + while ((m = re.exec(response.data)) !== null) { + if (m.index === re.lastIndex) { + re.lastIndex++; + } + // View your result using the m-variable. + // eg m[0] etc. + if(m[1]) + all_tags.push(m[1]) + } + + $scope.all_tags = all_tags.join(',').split(',');*/ + + if(!response.data.playbooks)return null; + + var playbooks = response.data.playbooks; + $scope.all_hosts = []; + $scope.all_tags = []; + + angular.forEach(playbooks, playbook => { + angular.forEach(playbook.plays, play => { + $scope.all_hosts = $scope.all_hosts.concat(play.hosts); + $scope.all_tags = $scope.all_tags.concat(play.tags); + angular.forEach(play.tasks, task => { + $scope.all_tags = $scope.all_tags.concat(task.tags); + }) + }) + }); + + // Get Unique List of tags + $scope.all_tags = Array.from(new Set($scope.all_tags)); + + // Get Unique List of hosts + $scope.all_hosts = Array.from(new Set($scope.all_hosts)); + + $scope.all_hosts = $scope.all_hosts.map(host => {return {name:host,selected:false}}); + + $scope.all_tags = $scope.all_tags.map(tag => {return {name:tag,selected:false}}); + + if(tags){ + angular.forEach(tags, tag => { + var tag_found = false; + angular.forEach($scope.all_tags, (all_tag,index) => { + if(tag == all_tag.name){ + tag_found = true; + all_tag.selected = true + } + }); + if(!tag_found) + $scope.all_tags.push({name:tag,selected:true}) + }) + + } + + $scope.readyForPlay = true; + + }, + function(error){ + //console.log(error.data) + //$scope.err_msg = error.data; + $scope.result = $sce.trustAsHtml(ansi2html.toHtml(error.data).replace(/\n/g, "
")); + $scope.view = 'console' + }) + + }; + + /** + * Process Ansible Output and show graphically + * @param ansibleOutput + */ + $scope.processAnsibleOutput_old = function(ansibleOutput){ + + $scope.ansibleOutputResult = []; + //https://regex101.com/r/yD6lZ6/1 + //var re = /(PLAY|TASK) \[(.*)\] (.*)\n(.*?):([^]*?)(?=TASK|PLAY)/gm; + //var re = /(PLAY|TASK) \[(.*)\] (.*)\n(?:(.*?)\s?(.*): \[(.*)\](.*)=> ([^]*?)(?=TASK|PLAY)|(.*?): \[(.*)\](.*)|(.*)|(?=TASK|PLAY))/gm + //var re = /(PLAY|TASK) \[(.*)\] (.*)\n(?:(.*?)\s?(.*): \[(.*)\](.*)=> ([^]*?)(?=\n\n)|(.*?): \[(.*)\](.*)|(.*)|(?=\n\n))/gm; + var re = /({[^]+})/g + var m; + + while ((m = re.exec(ansibleOutput)) !== null) { + if (m.index === re.lastIndex) { + re.lastIndex++; + } + // View your result using the m-variable. + // eg m[0] etc. + + var type = m[1]; //TASK,PLAY + var name = m[2]; // ansible-role-vra : debug delete instance + var status = m[5]; //ok, skipping, failed + var host = m[6]; //localhost , localhost -> localhost + var status_2 = m[7]; + var result = m[8]; + + if(result){ + //var result_object_string = result.replace(/({[^]+})[^]+/,"$1"); + var result_object_string = result; + var resultObject = null; + try{ + resultObject = JSON.parse(result_object_string); + + //resultObject.formattedStdErr = resultObject.stderr.replace(/\\r\\n/g,'
').replace(/\\n/g, "
"); + resultObject.formattedStdErr = resultObject.stderr.replace(/\n/g,'
'); + resultObject.formattedStdOut = resultObject.stderr.replace(/\n/g,'
'); + + }catch(e){ + console.log("Error converting ansible output result object to javascript") + } + } + + $scope.ansibleOutputResult.push({ + type:type, + name:name, + status:status, + host:host, + status_2:status_2, + resultString:result, + resultObject:resultObject + }) + + } + + } + + + $scope.processAnsibleOutput = function(ansibleOutput){ + + $scope.ansibleOutputResult = []; + $scope.ansibleOutputObject = { + 'plays' : [], + 'stats' : {} + + } + //var re = /(.*{[^]+}.*)/g; + var re = /--------BEGIN--------([^]+?)--------END--------/gm; + var m; + + while ((m = re.exec(ansibleOutput)) !== null) { + if (m.index === re.lastIndex) { + re.lastIndex++; + } + // View your result using the m-variable. + // eg m[0] etc. + + try{ + //$scope.ansibleOutputObject = JSON.parse(m[1]); + var resultItem = JSON.parse(m[1]); + if('play' in resultItem){ + $scope.ansibleOutputObject.plays.push(resultItem); + } else if('task' in resultItem){ + + var current_play = $scope.ansibleOutputObject.plays[$scope.ansibleOutputObject.plays.length-1] + var newTask = true; + angular.forEach(current_play.tasks, (task, index)=>{ + if(task.task.id === resultItem.task.id){ + newTask = false; + current_play.tasks[index] = resultItem + } + }) + + + if(newTask) + current_play.tasks.push(resultItem); + + } else if('stats' in resultItem){ + $scope.ansibleOutputObject.stats = resultItem.stats; + } + + }catch(e){ + console.log("Error parsing ansible output" + e); + } + + + //var plays = $scope.ansibleOutputObject.plays; + + } + + console.log($scope.ansibleOutputObject); + + /*while ((m = re.exec(ansibleOutput)) !== null) { + if (m.index === re.lastIndex) { + re.lastIndex++; + } + // View your result using the m-variable. + // eg m[0] etc. + + + + var type = m[1]; //TASK,PLAY + var name = m[2]; // ansible-role-vra : debug delete instance + var status = m[5]; //ok, skipping, failed + var host = m[6]; //localhost , localhost -> localhost + var status_2 = m[7]; + var result = m[8]; + + if(result){ + //var result_object_string = result.replace(/({[^]+})[^]+/,"$1"); + var result_object_string = result; + var resultObject = null; + try{ + resultObject = JSON.parse(result_object_string); + + //resultObject.formattedStdErr = resultObject.stderr.replace(/\\r\\n/g,'
').replace(/\\n/g, "
"); + resultObject.formattedStdErr = resultObject.stderr.replace(/\n/g,'
'); + resultObject.formattedStdOut = resultObject.stderr.replace(/\n/g,'
'); + + }catch(e){ + console.log("Error converting ansible output result object to javascript") + } + } + + $scope.ansibleOutputResult.push({ + type:type, + name:name, + status:status, + host:host, + status_2:status_2, + resultString:result, + resultObject:resultObject + }) + + }*/ + + } +} + +export default angular.module('webAppApp.execution', []) + .controller('ExecutionController', executionController) + .name; diff --git a/client/app/designer/execution/execution.controller.spec.js b/client/app/designer/execution/execution.controller.spec.js new file mode 100644 index 0000000..5731261 --- /dev/null +++ b/client/app/designer/execution/execution.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: ExecutionCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.execution')); + + var ExecutionCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + ExecutionCtrl = $controller('ExecutionCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/file_browser/file_browser.component.js b/client/app/designer/file_browser/file_browser.component.js new file mode 100644 index 0000000..45104ba --- /dev/null +++ b/client/app/designer/file_browser/file_browser.component.js @@ -0,0 +1,104 @@ +'use strict'; +const angular = require('angular'); + +const uiRouter = require('angular-ui-router'); + +import routes from './file_browser.routes'; + +export class FileBrowserComponent { + /*@ngInject*/ + constructor($scope,ansible,editor) { + 'ngInject'; + $scope.treeOptions = { + nodeChildren: "children", + dirSelectable: true, + isLeaf: function (node) { + return !(node.type === 'directory'); + }, + injectClasses: { + ul: "a1", + li: "a2", + liSelected: "a7", + iExpanded: "a3", + iCollapsed: "a4", + iLeaf: "a5", + label: "a6", + labelSelected: "a8" + } + }; + + $scope.editContent = false; + $scope.selectedFile = {showSource: true}; + + var loadProjectFiles = function(){ + ansible.getProjectFiles(function(response){ + $scope.projectFiles = response.data; + },function(error){ + + }); + }; + + $scope.$on('projectLoaded',function(){ + loadProjectFiles(); + }); + + if($scope.$parent.selectedProject && $scope.$parent.selectedProject.ansibleEngine) { + loadProjectFiles(); + } + + /** + * Show selected item in the tree + * @param file + * @param parent + */ + $scope.showSelected = function (file, parent) { + + if (file.children) { + $scope.selectedFile.content = JSON.stringify(file, null, '\t'); + $scope.docType = 'json'; + $scope.selectedFile.tasks = null; + return; + } + + var command = 'cat "' + file.path + '"'; + $scope.showSource = true; + $scope.markdownContent = ''; + $scope.docType = 'text'; + $scope.selectedFile.content = 'Loading..'; + $scope.selectedFile.tasks = null; + $scope.selectedFileName = file.name; + $scope.selectedFilePath = file.path; + $scope.parentNode = parent; + + ansible.executeCommand(command, + function (response) { + console.log(response.data) + editor.setContentAndType(response.data, file, $scope.selectedFile); + + var parentDirectory = file.path.replace(/^(.+)\/(.+)\/([^/]+)$/, "$2"); + if (parentDirectory == 'tasks') { + $scope.selectedFile.tasks = YAML.parse(response.data) || []; + } + + if (parentDirectory == 'group_vars' || parentDirectory == 'host_vars') { + $scope.selectedFile.docType = 'yaml'; + } + + //$scope.selectedFile.content = response.data; + + }, function (response) { + $scope.selectedFile.content = response.data; + + }) + }; + } +} + +export default angular.module('webAppApp.file_browser', [uiRouter]) + .config(routes) + .component('fileBrowser', { + template: require('./file_browser.html'), + controller: FileBrowserComponent, + controllerAs: 'fileBrowserCtrl' + }) + .name; diff --git a/client/app/designer/file_browser/file_browser.component.spec.js b/client/app/designer/file_browser/file_browser.component.spec.js new file mode 100644 index 0000000..7c9b282 --- /dev/null +++ b/client/app/designer/file_browser/file_browser.component.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Component: FileBrowserComponent', function() { + // load the controller's module + beforeEach(module('webAppApp.file_browser')); + + var FileBrowserComponent; + + // Initialize the controller and a mock scope + beforeEach(inject(function($componentController) { + FileBrowserComponent = $componentController('file_browser', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/file_browser/file_browser.html b/client/app/designer/file_browser/file_browser.html new file mode 100644 index 0000000..8fe2dec --- /dev/null +++ b/client/app/designer/file_browser/file_browser.html @@ -0,0 +1,33 @@ + +
+

File Browser

+
+ + {{node.name}} + +
+ +
+ + + + +
+ +
+ +
+
+
+ +
diff --git a/client/app/designer/file_browser/file_browser.routes.js b/client/app/designer/file_browser/file_browser.routes.js new file mode 100644 index 0000000..37d510e --- /dev/null +++ b/client/app/designer/file_browser/file_browser.routes.js @@ -0,0 +1,10 @@ +'use strict'; + +export default function($stateProvider) { + 'ngInject'; + $stateProvider + .state('designer.file_browser', { + url: '/file_browser', + template: '' + }); +} diff --git a/client/app/designer/inventory/inventory.component.js b/client/app/designer/inventory/inventory.component.js new file mode 100644 index 0000000..1334ed9 --- /dev/null +++ b/client/app/designer/inventory/inventory.component.js @@ -0,0 +1,278 @@ +'use strict'; +const angular = require('angular'); + +const uiRouter = require('angular-ui-router'); + +import routes from './inventory.routes'; + + +export class InventoryComponent { + /*@ngInject*/ + constructor($scope, $uibModal, ansible) { + + 'ngInject'; + $scope.selectedInventory = {inventory: "", content: ""}; + + $scope.editInventory = {value: false}; + $scope.selectedGroup = {group: null}; + $scope.selectedHost = {host: null}; + + $scope.complexVar = {}; + + $scope.$on('projectLoaded', function () { + $scope.getInventorys() + }); + + //To fix a warning message in console + $scope.aceLoaded = function(_editor){ + _editor.$blockScrolling = Infinity; + }; + + // --------------------------------------- PLAYBOOKS ---------------- + + $scope.getInventorys = function () { + ansible.getInventoryList( + function (response) { + $scope.inventorys = response.data; + }, + function (response) { + console.log(response.data) + } + ) + }; + + + if ($scope.$parent.selectedProject && $scope.$parent.selectedProject.ansibleEngine) { + $scope.getInventorys() + } + + $scope.loadingModuleCode = false; + + $scope.showInventoryCode = function (inventory_name) { + $scope.loadingModuleCode = true; + if (!inventory_name) { + $scope.selectedInventory.content = "Select a module"; + return; + } + ansible.readInventory(inventory_name, function (response) { + $scope.loadingModuleCode = false; + $scope.selectedInventory.content = response.data.split("Stream :: close")[0]; + + $scope.inventory_data_json = ansible.parseINIString($scope.selectedInventory.content); + $scope.inventory_data_json['name'] = inventory_name + + }); + }; + + $scope.$watch('selectedInventory.inventory', function (newValue, oldValue) { + if (newValue && newValue !== oldValue) { + $scope.selectedInventory.content = "Loading Code..."; + $scope.showInventoryCode(newValue) + } + }); + + + $scope.showCreatInventoryModal = function () { + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createTaskContent.html',*/ + templateUrl: 'app/designer/inventory/new_inventory/new_inventory.html', + controller: 'NewInventoryController', + size: 'md', + backdrop: 'static', + keyboard: false, + closeByEscape: false, + closeByDocument: false, + resolve: { + selectedProject: function () { + return $scope.$parent.selectedProject + } + } + }); + + modalInstance.result.then(function () { + $scope.getInventorys(); + }, function () { + + }); + + }; + + $scope.editGroup = function (group) { + $scope.showCreateGroupModal(group); + }; + + $scope.showCreateGroupModal = function (editGroup) { + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createTaskContent.html',*/ + templateUrl: 'app/designer/inventory/new_group/new_group.html', + controller: 'NewGroupController', + size: 'lg', + backdrop: 'static', + keyboard: false, + closeByEscape: false, + closeByDocument: false, + resolve: { + selectedProject: function () { + return $scope.$parent.selectedProject + }, + + editGroup: function () { + return editGroup + } + } + }); + + modalInstance.result.then(function (group) { + if(!editGroup)$scope.addGroup(group); + else $scope.selectedInventory.content = ansible.jsonToAnsibleInventoryIni($scope.inventory_data_json); + + $scope.saveInventory(); + }, function () { + + }); + + }; + + $scope.editHost = function (host) { + + var hostMemberOfGroups = getHostMemberOfGroups(host); + + $scope.showCreateHostModal({name: host, members: hostMemberOfGroups.join(',')}); + }; + + $scope.showCreateHostModal = function (editHost) { + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createTaskContent.html',*/ + templateUrl: 'app/designer/inventory/new_host/new_host.html', + controller: 'NewHostController', + size: 'lg', + backdrop: 'static', + keyboard: false, + closeByEscape: false, + closeByDocument: false, + resolve: { + selectedProject: function () { + return $scope.$parent.selectedProject + }, + editHost: function () { + return editHost + } + } + }); + + modalInstance.result.then(function (host) { + $scope.addHost(host); + $scope.saveInventory(); + }, function () { + + }); + }; + + $scope.saveInventory = function () { + $scope.saveInventoryLoading = true; + ansible.createInventory($scope.selectedInventory.inventory, $scope.selectedInventory.content, + function (response) { + $scope.saveInventoryLoading = false; + $scope.editInventory.value = false; + }, + function (response) { + $scope.saveInventoryLoading = false; + $scope.err_msg = response.data; + }) + }; + + $scope.deleteInventory = function () { + $scope.deleteInventoryLoading = true; + ansible.deleteInventory($scope.selectedInventory.inventory, + function (response) { + $scope.deleteInventoryLoading = false; + $scope.selectedInventory.inventory = ""; + $scope.getInventorys(); + }, + function (response) { + $scope.deleteInventoryLoading = false; + $scope.err_msg = response.data; + }) + }; + + $scope.addGroup = function (group) { + + $scope.inventory_data_json.groups.push(group); + $scope.selectedInventory.content = ansible.jsonToAnsibleInventoryIni($scope.inventory_data_json); + // To refresh All Hosts list + $scope.inventory_data_json = ansible.parseINIString($scope.selectedInventory.content) + }; + + $scope.addHost = function (host) { + if ($scope.inventory_data_json.hosts.indexOf(host.name) < 0) + $scope.inventory_data_json.hosts.push(host.name); + + var host_member_of_groups = host.members.split(','); + + angular.forEach($scope.inventory_data_json.groups, function (group) { + if ((host_member_of_groups.indexOf(group.name)) > -1 && group.members.indexOf(host.name) < 0) { + group.members.push(host.name) + } + }); + + $scope.selectedInventory.content = ansible.jsonToAnsibleInventoryIni($scope.inventory_data_json); + // To refresh All Hosts list + $scope.inventory_data_json = ansible.parseINIString($scope.selectedInventory.content) + }; + + + $scope.deleteGroup = function (index) { + $scope.inventory_data_json.groups.splice(index, 1); + $scope.selectedInventory.content = ansible.jsonToAnsibleInventoryIni($scope.inventory_data_json); + // To refresh All Hosts list + $scope.inventory_data_json = ansible.parseINIString($scope.selectedInventory.content); + + $scope.saveInventory(); + + }; + + $scope.deleteHost = function (index, group) { + + var hostname = $scope.inventory_data_json.hosts[index]; + + $scope.inventory_data_json.hosts.splice(index, 1); + + angular.forEach($scope.inventory_data_json.groups, function (group) { + var memberIndex = group.members.indexOf(hostname) + if (memberIndex > -1) { + group.members.splice(memberIndex, 1) + } + }); + + $scope.selectedInventory.content = ansible.jsonToAnsibleInventoryIni($scope.inventory_data_json); + // To refresh All Hosts list + $scope.inventory_data_json = ansible.parseINIString($scope.selectedInventory.content); + + $scope.saveInventory(); + }; + + + var getHostMemberOfGroups = function (host) { + var groups = []; + angular.forEach($scope.inventory_data_json.groups, function (group) { + var memberIndex = group.members.indexOf(host); + if (memberIndex > -1) { + groups.push(group.name) + } + }); + return groups; + }; + } +} + +export default angular.module('webAppApp.inventory', [uiRouter]) + .config(routes) + .component('inventory', { + template: require('./inventory.html'), + controller: InventoryComponent, + controllerAs: 'inventoryCtrl' + }) + .name; diff --git a/client/app/designer/inventory/inventory.component.spec.js b/client/app/designer/inventory/inventory.component.spec.js new file mode 100644 index 0000000..8e9e893 --- /dev/null +++ b/client/app/designer/inventory/inventory.component.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Component: InventoryComponent', function() { + // load the controller's module + beforeEach(module('webAppApp.inventory')); + + var InventoryComponent; + + // Initialize the controller and a mock scope + beforeEach(inject(function($componentController) { + InventoryComponent = $componentController('inventory', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/inventory/inventory.css b/client/app/designer/inventory/inventory.css new file mode 100644 index 0000000..e69de29 diff --git a/client/app/designer/inventory/inventory.html b/client/app/designer/inventory/inventory.html new file mode 100644 index 0000000..18dfbea --- /dev/null +++ b/client/app/designer/inventory/inventory.html @@ -0,0 +1,154 @@ +
+ + + +
+ + + + + + + + + + + + + +
SelectName
+ {{inventory}}
+
+ +
+
+ +
+
+
Groups - {{inventory_data_json.groups.length}}
+
+ +
+ + + + + + + + + + + + + + + + + +
SelectNameMembersActions
+ {{group.name}}{{group.members.length}} +
+ + +
+
+
+ +
+
+
+ +
+
+
Group Members - {{selectedGroup.group.name}}
+
+ +
+ + + + + + + + + + + + + + + + +
SelectNameActions
+ {{host}} +
+ +
+
+
+ +
+
+
+ +
+ +
+
+
+
Hosts - {{inventory_data_json.hosts.length}}
+
+ +
+ + + + + + + + + + + + + + + + +
SelectNameActions
+ {{host}} +
+ + +
+
+
+ +
+
+
+ + +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
diff --git a/client/app/designer/inventory/inventory.routes.js b/client/app/designer/inventory/inventory.routes.js new file mode 100644 index 0000000..17e508f --- /dev/null +++ b/client/app/designer/inventory/inventory.routes.js @@ -0,0 +1,10 @@ +'use strict'; + +export default function($stateProvider) { + 'ngInject'; + $stateProvider + .state('designer.inventory', { + url: '/inventory', + template: '' + }); +} diff --git a/client/app/designer/inventory/new_group/new_group.controller.js b/client/app/designer/inventory/new_group/new_group.controller.js new file mode 100644 index 0000000..be566f4 --- /dev/null +++ b/client/app/designer/inventory/new_group/new_group.controller.js @@ -0,0 +1,82 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function newGroupController($scope,$uibModalInstance, yamlFile, ansible, selectedProject, editGroup, YAML, $timeout) { + 'ngInject'; + $scope.newGroup = editGroup || {name:null}; + $scope.variableViewType = {type:'YAML'}; + + $scope.complexVar = {}; + $scope.complexVarString = {}; + + console.log("$scope.newGroup.name" + $scope.newGroup.name); + + $scope.aceLoaded = function (_editor) { + _editor.$blockScrolling = Infinity; + }; + + if($scope.newGroup.members){ + $scope.newGroup.members = $scope.newGroup.members.join(','); + } + + if($scope.newGroup.name){ + $scope.getGroupLoading = true; + ansible.getGroupVarsFile($scope.newGroup.name, + function(response){ + $scope.getGroupLoading = false; + $scope.complexVarStringYaml = response.data; + $scope.complexVar = YAML.parse(response.data); + + $timeout(function(){ + $scope.$broadcast('membersUpdated') + },100); + + },function(response){ + $scope.getGroupLoading = false; + $scope.err_msg = response.data; + }) + } + + $scope.$watch('complexVar',function(){ + $scope.complexVarString = JSON.stringify($scope.complexVar, null, '\t'); + $scope.complexVarStringYaml = yamlFile.jsonToYamlFile($scope.complexVar, 'Group Variables - ' + $scope.newGroup.name); + }, true); + + $scope.$watch('newGroup',function(){ + $scope.complexVarString = JSON.stringify($scope.complexVar, null, '\t'); + $scope.complexVarStringYaml = yamlFile.jsonToYamlFile($scope.complexVar, 'Group Variables - ' + $scope.newGroup.name); + }, true); + + $scope.createGroup = function(){ + $scope.createGroupLoading = true; + ansible.updateGroupVarsFile($scope.newGroup.name,$scope.complexVarStringYaml, + function(response){ + $scope.createGroupLoading = false; + console.log("Success"); + $scope.ok() + },function(response){ + $scope.createGroupLoading = false; + $scope.err_msg = response.data; + }); + }; + + $scope.ok = function () { + var resultGroup = {name:$scope.newGroup.name}; + + if($scope.newGroup.members){ + resultGroup.members = $scope.newGroup.members.split(','); + $scope.newGroup.members = $scope.newGroup.members.split(',') + } + + $uibModalInstance.close(resultGroup); + }; + + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; +} + +export default angular.module('webAppApp.new_group', []) + .controller('NewGroupController', newGroupController) + .name; diff --git a/client/app/designer/inventory/new_group/new_group.controller.spec.js b/client/app/designer/inventory/new_group/new_group.controller.spec.js new file mode 100644 index 0000000..85bd896 --- /dev/null +++ b/client/app/designer/inventory/new_group/new_group.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: NewGroupCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.new_group')); + + var NewGroupCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + NewGroupCtrl = $controller('NewGroupCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/inventory/new_group/new_group.html b/client/app/designer/inventory/new_group/new_group.html new file mode 100644 index 0000000..62d0b49 --- /dev/null +++ b/client/app/designer/inventory/new_group/new_group.html @@ -0,0 +1,45 @@ + + diff --git a/client/app/designer/inventory/new_host/new_host.controller.js b/client/app/designer/inventory/new_host/new_host.controller.js new file mode 100644 index 0000000..9386039 --- /dev/null +++ b/client/app/designer/inventory/new_host/new_host.controller.js @@ -0,0 +1,69 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function newHostController($scope,$uibModalInstance, yamlFile, ansible, selectedProject, editHost, YAML, $timeout) { + $scope.newHost = editHost || {name:null}; + + $scope.variableViewType = {type:'YAML'}; + + $scope.complexVar = {}; + $scope.complexVarString = {}; + + if($scope.newHost.name){ + $scope.getHostLoading = true; + ansible.getHostVarsFile($scope.newHost.name, + function(response){ + $scope.getHostLoading = false; + $scope.complexVarStringYaml = response.data; + $scope.complexVar = YAML.parse(response.data); + $timeout(function(){ + $scope.$broadcast('membersUpdated') + },100); + + },function(response){ + $scope.getHostLoading = false; + $scope.err_msg = response.data; + }) + } + + $scope.$watch('complexVar',function(){ + $scope.complexVarString = JSON.stringify($scope.complexVar, null, '\t'); + $scope.complexVarStringYaml = yamlFile.jsonToYamlFile($scope.complexVar, 'Host Variables - ' + $scope.newHost.name); + }, true); + + $scope.$watch('newHost',function(){ + $scope.complexVarString = JSON.stringify($scope.complexVar, null, '\t'); + $scope.complexVarStringYaml = yamlFile.jsonToYamlFile($scope.complexVar, 'Host Variables - ' + $scope.newHost.name); + }, true); + + $scope.aceLoaded = function(_editor){ + _editor.$blockScrolling = Infinity; + }; + + $scope.createHost = function(){ + $scope.createHostLoading = true; + ansible.updateHostVarsFile($scope.newHost.name,$scope.complexVarStringYaml, + function(response){ + $scope.createHostLoading = false; + console.log("Success"); + $scope.ok() + },function(response){ + $scope.createHostLoading = false; + $scope.err_msg = response.data; + }); + }; + + $scope.ok = function () { + if(!$scope.newHost.members)$scope.newHost.members = 'Un grouped'; + $uibModalInstance.close($scope.newHost); + }; + + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; +} + +export default angular.module('webAppApp.new_host', []) + .controller('NewHostController', newHostController) + .name; diff --git a/client/app/designer/inventory/new_host/new_host.controller.spec.js b/client/app/designer/inventory/new_host/new_host.controller.spec.js new file mode 100644 index 0000000..218aa92 --- /dev/null +++ b/client/app/designer/inventory/new_host/new_host.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: NewHostCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.new_host')); + + var NewHostCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + NewHostCtrl = $controller('NewHostCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/inventory/new_host/new_host.html b/client/app/designer/inventory/new_host/new_host.html new file mode 100644 index 0000000..fbf7cc0 --- /dev/null +++ b/client/app/designer/inventory/new_host/new_host.html @@ -0,0 +1,46 @@ + + diff --git a/client/app/designer/inventory/new_inventory/new_inventory.controller.js b/client/app/designer/inventory/new_inventory/new_inventory.controller.js new file mode 100644 index 0000000..4510f2c --- /dev/null +++ b/client/app/designer/inventory/new_inventory/new_inventory.controller.js @@ -0,0 +1,40 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function newInventoryController($scope,$uibModalInstance,ansible,selectedProject) { + $scope.newInventory = {name:null}; + + $scope.createInventoryLoading = false; + + $scope.createInventory = function(){ + + if($scope.newInventory.name.match(/\./)){ + $scope.err_msg = "Inventory files should not have extension" + return + } + + $scope.createInventoryLoading = true; + ansible.createInventory($scope.newInventory.name,'# Inventory File - ' + $scope.newInventory.name, + function(response){ + $scope.createInventoryLoading = false; + $scope.ok(); + }, + function(response){ + $scope.createInventoryLoading = false; + $scope.err_msg = response.data; + }) + }; + + $scope.ok = function () { + $uibModalInstance.close(null); + }; + + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; +} + +export default angular.module('webAppApp.new_inventory', []) + .controller('NewInventoryController', newInventoryController) + .name; diff --git a/client/app/designer/inventory/new_inventory/new_inventory.controller.spec.js b/client/app/designer/inventory/new_inventory/new_inventory.controller.spec.js new file mode 100644 index 0000000..df0c54e --- /dev/null +++ b/client/app/designer/inventory/new_inventory/new_inventory.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: NewInventoryCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.new_inventory')); + + var NewInventoryCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + NewInventoryCtrl = $controller('NewInventoryCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/inventory/new_inventory/new_inventory.html b/client/app/designer/inventory/new_inventory/new_inventory.html new file mode 100644 index 0000000..c4ececc --- /dev/null +++ b/client/app/designer/inventory/new_inventory/new_inventory.html @@ -0,0 +1,21 @@ + + diff --git a/client/app/designer/playbook/new_play/new_play.controller.js b/client/app/designer/playbook/new_play/new_play.controller.js new file mode 100644 index 0000000..23736ac --- /dev/null +++ b/client/app/designer/playbook/new_play/new_play.controller.js @@ -0,0 +1,134 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function newPlayController($scope, $uibModalInstance, ansible, plays, selectedPlayIndex) { + $scope.loading_msg = ''; + $scope.title = "Create Play"; + $scope.editMode = false; + $scope.editHostMode = false; + + var selectedPlay; + if(selectedPlayIndex > -1){ + selectedPlay = plays[selectedPlayIndex]; + $scope.title = "Edit Play"; + $scope.editMode = true; + if(selectedPlay && selectedPlay.tags)$scope.tags = selectedPlay.tags.join(','); + } + + $scope.newPlay = selectedPlay || {}; + + $scope.newPlay_roles = $scope.newPlay.roles; + + $scope.createPlayLoading = false; + + $scope.createPlay = function () { + $scope.ok($scope.newPlay) + }; + + $scope.ok = function (newPlay) { + if($scope.tags) + newPlay.tags = $scope.tags.split(','); + + + if($scope.newPlay_roles && $scope.newPlay_roles.length){ + var roles = []; + angular.forEach($scope.newPlay_roles,function(role){ + roles.push(role.text) + }); + newPlay.roles = roles; + }else if(newPlay.roles){ + delete newPlay.roles; + } + + $uibModalInstance.close(newPlay); + }; + + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; + + + $scope.getHostsFromInventory = function(){ + + var hosts = []; + + angular.forEach($scope.inventory_data_json.hosts, function(host){ + hosts.push({name:host}) + }); + + angular.forEach($scope.inventory_data_json.groups, function(group){ + if(group.name !== 'Un grouped') + hosts.push({name:group.name}) + }); + + return hosts; + }; + + $scope.listOfInventoryFiles = function(){ + $scope.loading_msg = 'Loading Inventory Files'; + ansible.getInventoryList(function(response){ + $scope.loading_msg = ''; + $scope.inventoryFiles = response.data; + }, + function(response){ + $scope.loading_msg = ''; + $scope.err_msg = response.data + }) + }; + + + $scope.listOfRoles = function(){ + $scope.loading_msg = 'Loading Roles'; + ansible.getRoleList(function(response){ + $scope.loading_msg = ''; + $scope.roleList = response.data; + }, + function(response){ + $scope.loading_msg = ''; + $scope.err_msg = response.data + }) + }; + + $scope.inventorySelected = function(selectedInventoryFile){ + $scope.loading_msg = 'Loading Hosts'; + ansible.readInventory(selectedInventoryFile, + function(response){ + $scope.loading_msg = ''; + $scope.inventory_data_json = ansible.parseINIString(response.data); + $scope.hosts = $scope.getHostsFromInventory(); + + },function(response){ + $scope.loading_msg = ''; + $scope.err_msg = response.data + }) + }; + + $scope.getHostObject = function(hostname){ + var result = $scope.hosts.filter(function(host){ + return host.name == hostname + }); + + if(result.length){ + return result[0] + } + + }; + + $scope.listOfInventoryFiles(); + $scope.listOfRoles(); + + $scope.loadTags = function(query){ + if($scope.roleList){ + var tempList = $scope.roleList.filter(function(role){ + return role.indexOf(query) > -1 + }); + + return tempList + } + } +} + +export default angular.module('webAppApp.new_play', []) + .controller('NewPlayController', newPlayController) + .name; diff --git a/client/app/designer/playbook/new_play/new_play.controller.spec.js b/client/app/designer/playbook/new_play/new_play.controller.spec.js new file mode 100644 index 0000000..c8b68db --- /dev/null +++ b/client/app/designer/playbook/new_play/new_play.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: NewPlayCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.new_play')); + + var NewPlayCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + NewPlayCtrl = $controller('NewPlayCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/playbook/new_play/new_play.html b/client/app/designer/playbook/new_play/new_play.html new file mode 100644 index 0000000..765a0bf --- /dev/null +++ b/client/app/designer/playbook/new_play/new_play.html @@ -0,0 +1,96 @@ + + diff --git a/client/app/designer/playbook/new_playbook/new_playbook.controller.js b/client/app/designer/playbook/new_playbook/new_playbook.controller.js new file mode 100644 index 0000000..9f045bb --- /dev/null +++ b/client/app/designer/playbook/new_playbook/new_playbook.controller.js @@ -0,0 +1,35 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function newPlaybookController($scope,$uibModalInstance,ansible) { + $scope.newPlaybook = {name:null}; + + $scope.createPlaybookLoading = false; + + $scope.createPlaybook = function(){ + $scope.createPlaybookLoading = true; + + ansible.createPlaybook($scope.newPlaybook.name + '.yml',"", + function(response){ + $scope.createPlaybookLoading = false; + $scope.ok(); + }, + function(response){ + $scope.createPlaybookLoading = false; + $scope.err_msg = response.data; + }) + }; + + $scope.ok = function () { + $uibModalInstance.close(null); + }; + + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; +} + +export default angular.module('webAppApp.new_playbook', []) + .controller('NewPlaybookController', newPlaybookController) + .name; diff --git a/client/app/designer/playbook/new_playbook/new_playbook.controller.spec.js b/client/app/designer/playbook/new_playbook/new_playbook.controller.spec.js new file mode 100644 index 0000000..1d3cc3c --- /dev/null +++ b/client/app/designer/playbook/new_playbook/new_playbook.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: NewPlaybookCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.new_playbook')); + + var NewPlaybookCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + NewPlaybookCtrl = $controller('NewPlaybookCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/playbook/new_playbook/new_playbook.html b/client/app/designer/playbook/new_playbook/new_playbook.html new file mode 100644 index 0000000..a1a0962 --- /dev/null +++ b/client/app/designer/playbook/new_playbook/new_playbook.html @@ -0,0 +1,19 @@ + + diff --git a/client/app/designer/playbook/playbook.component.js b/client/app/designer/playbook/playbook.component.js new file mode 100644 index 0000000..0ebd39a --- /dev/null +++ b/client/app/designer/playbook/playbook.component.js @@ -0,0 +1,328 @@ +'use strict'; +const angular = require('angular'); + +const uiRouter = require('angular-ui-router'); + +import routes from './playbook.routes'; + +export class PlaybookComponent { + /*@ngInject*/ + constructor($scope,$uibModal,YAML,ansible,yamlFile) { + 'ngInject'; + $scope.isopen = {playbooks:true,plays:false,tasks:false}; + + $scope.selectedPlaybook = {playbook: "",content: ""}; + $scope.selectedPlay = {play: ""}; + + $scope.showSaveButton = {}; + $scope.loadingButtons = {}; + + $scope.editPlaybook = {value:false}; + + $scope.loadingModuleCode = false; + + $scope.$on('projectLoaded',function(){ + $scope.getPlaybooks() + }); + + //To fix a warning message in console + $scope.aceLoaded = function(_editor){ + _editor.$blockScrolling = Infinity; + }; + + // --------------------------------------- PLAYBOOKS ---------------- + + $scope.getPlaybooks = function(){ + ansible.getPlaybookList( + function(response){ + $scope.playbooks = response.data; + }, + function(response){ + console.log(response.data) + } + ) + }; + + + if($scope.$parent.selectedProject && $scope.$parent.selectedProject.ansibleEngine){ + $scope.getPlaybooks() + } + + $scope.showPlaybookCode = function(playbook_name){ + $scope.loadingModuleCode = true; + + if(!playbook_name){ + $scope.selectedPlaybook.content = "Select a module"; + return; + } + ansible.readPlaybook(playbook_name,function(response) { + $scope.isopen.playbooks = true; + $scope.isopen.plays = true + $scope.loadingModuleCode = false; + $scope.selectedPlaybook.content = response.data.split("Stream :: close")[0]; + $scope.getPlaysFromPlayBook($scope.selectedPlaybook.content); + + }); + }; + + $scope.$watch('selectedPlaybook.playbook',function(newValue,oldValue){ + if(newValue && newValue !== oldValue){ + $scope.selectedPlaybook.content = "Loading Code..."; + $scope.showPlaybookCode(newValue); + } + }); + + $scope.$watch('selectedPlay.play',function(newValue,oldValue){ + if(newValue && newValue !== oldValue){ + $scope.selectedPlay.play.tasks = $scope.selectedPlay.play.tasks || []; + $scope.isopen.playbooks = false; + $scope.isopen.plays = false; + $scope.isopen.tasks = true; + $scope.isopen.roles = true; + } + }); + + + $scope.showCreatePlaybookModal = function(){ + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createTaskContent.html',*/ + templateUrl: 'app/designer/playbook/new_playbook/new_playbook.html', + controller: 'NewPlaybookController', + size: 'md', + backdrop : 'static', + keyboard : false, + closeByEscape : false, + closeByDocument : false, + resolve: { + selectedProject: function(){ + return $scope.$parent.selectedProject + } + } + }); + + modalInstance.result.then(function () { + $scope.getPlaybooks(); + }, function () { + + }); + + }; + + $scope.editPlaybookMethod = function(){ + $scope.editPlaybook.value = true; + $scope.uneditedPlaybokContents = $scope.selectedPlaybook.content + }; + + $scope.cancelPlaybookChanges = function(){ + $scope.editPlaybook.value = false; + $scope.selectedPlaybook.content = $scope.uneditedPlaybokContents + }; + + $scope.savePlaybook = function(buttonVariable){ + console.log("Saving Playbook") + $scope.loadingButtons[buttonVariable] = true; + + ansible.createPlaybook($scope.selectedPlaybook.playbook,$scope.selectedPlaybook.content, + function(response){ + $scope.loadingButtons[buttonVariable] = false; + $scope.showSaveButton[buttonVariable] = false; + $scope.editPlaybook.value = false; + }, + function(response){ + $scope.loadingButtons[buttonVariable] = false; + $scope.showSaveButton[buttonVariable] = false; + $scope.err_msg = response.data; + }) + }; + + $scope.deletePlaybook = function(){ + $scope.deletePlaybookLoading = true; + ansible.deletePlaybook($scope.selectedPlaybook.playbook, + function(response){ + $scope.deletePlaybookLoading = false; + $scope.selectedPlaybook.playbook = ""; + $scope.getPlaybooks(); + }, + function(response){ + $scope.deletePlaybookLoading = false; + $scope.err_msg = response.data; + }) + }; + + + //--------------- PLAY -------------- + + $scope.showCreatePlayModal = function(selectedPlayIndex){ + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createPlayContent.html',*/ + templateUrl: 'app/designer/playbook/new_play/new_play.html', + controller: 'NewPlayController', + size: 'lg', + backdrop : 'static', + keyboard : false, + closeByEscape : false, + closeByDocument : false, + resolve: { + selectedProject: function () { + return $scope.$parent.selectedProject; + }, + plays: function () { + return $scope.plays; + }, + selectedPlayIndex: function () { + return selectedPlayIndex; + } + } + }); + + modalInstance.result.then( + function (newPlay) { + if(selectedPlayIndex == null) + $scope.plays.push(newPlay); + + $scope.clearEmptyTasks($scope.plays); + + $scope.selectedPlaybook.content = yamlFile.jsonToYamlFile($scope.plays, 'Playbook file: ' + $scope.selectedPlaybook.playbook) + $scope.savePlaybook(); + }, function () { + + }); + + }; + + + // FUNCTION - GET PLAYS FROM PLAYBOOK + + $scope.getPlaysFromPlayBook = function(playbookYamlData){ + $scope.plays = YAML.parse(playbookYamlData) || [] + }; + + // FUNCTION - DELETE PLAY + + $scope.deletePlay = function(index){ + $scope.plays.splice(index,1); + $scope.selectedPlaybook.content = yamlFile.jsonToYamlFile($scope.plays, 'Playbook file: ' + $scope.selectedPlaybook.playbook) + $scope.savePlaybook(); + $scope.selectedPlay = {play: ""}; + }; + + // ------------------- EXECUTE PLAYBOOK MODAL ------------- + + $scope.executeAnsiblePlayBook = function(tags,executionType,executionName,selectedPlay){ + console.log("Tags type" + typeof tags) + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createTaskContent.html',*/ + templateUrl: 'app/designer/execution/executeModal.html', + controller: 'ExecutionController', + size: 'lg', + backdrop : 'static', + keyboard : false, + closeByEscape : false, + closeByDocument : false, + resolve: { + tags: function(){ + return tags + }, + selectedProject: function(){ + return $scope.$parent.selectedProject + }, + selectedPlaybook: function(){ + return $scope.selectedPlaybook + }, + selectedPlay: function(){ + return selectedPlay + }, + executionType: function(){ + return executionType + }, + executionName: function(){ + return executionName + }, + readOnly: function(){ + return false + }, + runData: function(){ + return null + }, + projectFolder: function(){ + return null + }, + roleName: function(){ + return null + } + } + }); + }; + + + $scope.clearEmptyTasks = function(plays){ + //Check for empty tasks list + angular.forEach(plays,function(play){ + if((play.tasks && !play.tasks.length) || !play.tasks){ + delete play.tasks + } + }); + + }; + + // ---------------------- TASKS ------------------- + + $scope.updatePlaybookFileContent = function(save,buttonVariable){ + + var playsCopy = angular.copy($scope.plays); + + $scope.clearEmptyTasks(playsCopy); + + $scope.selectedPlaybook.content = yamlFile.jsonToYamlFile(playsCopy, 'Playbook file: ' + $scope.selectedPlaybook.playbook) + if(save) + $scope.savePlaybook(buttonVariable); + }; + + $scope.moveUp = function(list,index,buttonVariable){ + if(!$scope.preChangeData) $scope.preChangeData = angular.copy(list); + var temp = angular.copy(list[index]); + list[index] = list[index-1]; + list[index-1] = temp; + + $scope.updatePlaybookFileContent(false); + + $scope.showSaveButton[buttonVariable] = true + + }; + + $scope.cancelChange = function(buttonVariable){ + if($scope.preChangeData){ + $scope.plays = angular.copy($scope.preChangeData); + $scope.preChangeData = null + + } + $scope.updatePlaybookFileContent(false); + + $scope.showSaveButton[buttonVariable] = false + }; + + $scope.moveDown = function(list,index,buttonVariable){ + if(!$scope.preChangeData) $scope.preChangeData = angular.copy(list); + var temp = angular.copy(list[index]); + list[index] = list[index+1]; + list[index+1] = temp; + + $scope.updatePlaybookFileContent(false); + + $scope.showSaveButton[buttonVariable] = true + + }; + } +} + +export default angular.module('webAppApp.playbook', [uiRouter]) + .config(routes) + .component('playbook', { + template: require('./playbook.html'), + controller: PlaybookComponent, + controllerAs: 'playbookCtrl' + }) + .name; diff --git a/client/app/designer/playbook/playbook.component.spec.js b/client/app/designer/playbook/playbook.component.spec.js new file mode 100644 index 0000000..5e5977f --- /dev/null +++ b/client/app/designer/playbook/playbook.component.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Component: PlaybookComponent', function() { + // load the controller's module + beforeEach(module('webAppApp.playbook')); + + var PlaybookComponent; + + // Initialize the controller and a mock scope + beforeEach(inject(function($componentController) { + PlaybookComponent = $componentController('playbook', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/playbook/playbook.css b/client/app/designer/playbook/playbook.css new file mode 100644 index 0000000..e69de29 diff --git a/client/app/designer/playbook/playbook.html b/client/app/designer/playbook/playbook.html new file mode 100644 index 0000000..72b1ae1 --- /dev/null +++ b/client/app/designer/playbook/playbook.html @@ -0,0 +1,166 @@ +
+ +
+ + Playbooks + + + + + + + + +
+ + + + + + + + + + + + + + + +
SelectName
+ {{playbook}}
+
+
+ +
+ + Plays + + +
+ + + + + + + + + + + + + + + + + + + + +
SelectNameHostsActions
+ {{play.name}}{{play.hosts}} +
+ + +
+ + +
+
+
+ + + + +
+ +
+ + Tasks + + + +
+ +
+ + Roles + +
+ + + + + + + + + + + + + +
SelectName
+ {{role}}
+
+ +
+
+
File Browser
+
+ + + {{node.name}} + +
+
+
+
+ + +
+ +
+ +
+
+ +
+ +
diff --git a/client/app/designer/playbook/playbook.routes.js b/client/app/designer/playbook/playbook.routes.js new file mode 100644 index 0000000..13f26ff --- /dev/null +++ b/client/app/designer/playbook/playbook.routes.js @@ -0,0 +1,10 @@ +'use strict'; + +export default function($stateProvider) { + 'ngInject'; + $stateProvider + .state('designer.playbook', { + url: '/playbook', + template: '' + }); +} diff --git a/client/app/designer/roles/new_file/new_file.controller.js b/client/app/designer/roles/new_file/new_file.controller.js new file mode 100644 index 0000000..7872728 --- /dev/null +++ b/client/app/designer/roles/new_file/new_file.controller.js @@ -0,0 +1,58 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function newFileController($scope,$uibModalInstance,ansible,selectedDirectory,copyFile,selectedFileName) { + $scope.newFile = {name:null}; + $scope.createFileLoading = false; + $scope.title = 'New File'; + + var parentDirectory = selectedDirectory; + + // If copyFile use selectedFileName to create new role from + // else nullify selectedFileName + if(!copyFile){ + selectedFileName = null; + } + else { + $scope.title = 'Copy File'; + $scope.newFile.name = 'Copy of ' + selectedFileName; + } + + /** + * Create/Copy File - Either a new role or copy an existing role + */ + $scope.createFile = function(){ + + $scope.createFileLoading = true; + ansible.createFile(parentDirectory + '/' + $scope.newFile.name, + function(response){ + $scope.createFileLoading = false; + $scope.ok(); + }, + function(response){ + $scope.createFileLoading = false; + $scope.err_msg = response.data; + }, + selectedFileName + ) + }; + + /** + * Close create/copy modal + */ + $scope.ok = function () { + $uibModalInstance.close(null); + }; + + /** + * Cancel modal + */ + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; +} + +export default angular.module('webAppApp.new_file', []) + .controller('NewFileController', newFileController) + .name; diff --git a/client/app/designer/roles/new_file/new_file.controller.spec.js b/client/app/designer/roles/new_file/new_file.controller.spec.js new file mode 100644 index 0000000..4a488a9 --- /dev/null +++ b/client/app/designer/roles/new_file/new_file.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: NewFileCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.new_file')); + + var NewFileCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + NewFileCtrl = $controller('NewFileCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/roles/new_file/new_file.html b/client/app/designer/roles/new_file/new_file.html new file mode 100644 index 0000000..daec11c --- /dev/null +++ b/client/app/designer/roles/new_file/new_file.html @@ -0,0 +1,19 @@ + + diff --git a/client/app/designer/roles/new_role/new_role.controller.js b/client/app/designer/roles/new_role/new_role.controller.js new file mode 100644 index 0000000..a1fcf40 --- /dev/null +++ b/client/app/designer/roles/new_role/new_role.controller.js @@ -0,0 +1,56 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function newRoleController($scope,$uibModalInstance,ansible,selectedRoleName,copyRole) { + + $scope.newRole = {name:null}; + $scope.createRoleLoading = false; + $scope.title = 'New Role'; + + // If copyRole use selectedRoleName to create new role from + // else nullify selectedRoleName + if(!copyRole){ + selectedRoleName = null; + } + else { + $scope.title = 'Copy Role'; + $scope.newRole.name = 'Copy of ' + selectedRoleName; + } + + /** + * Create/Copy Role - Either a new role or copy an existing role + */ + $scope.createRole = function(){ + $scope.createRoleLoading = true; + ansible.createRole($scope.newRole.name, + function(response){ + $scope.createRoleLoading = false; + $scope.ok(); + }, + function(response){ + $scope.createRoleLoading = false; + $scope.err_msg = response.data; + }, + selectedRoleName + ) + }; + + /** + * Close create/copy modal + */ + $scope.ok = function () { + $uibModalInstance.close(null); + }; + + /** + * Cancel modal + */ + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; +} + +export default angular.module('webAppApp.new_role', []) + .controller('NewRoleController', newRoleController) + .name; diff --git a/client/app/designer/roles/new_role/new_role.controller.spec.js b/client/app/designer/roles/new_role/new_role.controller.spec.js new file mode 100644 index 0000000..bda468b --- /dev/null +++ b/client/app/designer/roles/new_role/new_role.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: NewRoleCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.new_role')); + + var NewRoleCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + NewRoleCtrl = $controller('NewRoleCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/roles/new_role/new_role.html b/client/app/designer/roles/new_role/new_role.html new file mode 100644 index 0000000..b4bcba8 --- /dev/null +++ b/client/app/designer/roles/new_role/new_role.html @@ -0,0 +1,19 @@ + + diff --git a/client/app/designer/roles/roles.component.js b/client/app/designer/roles/roles.component.js new file mode 100644 index 0000000..f11827f --- /dev/null +++ b/client/app/designer/roles/roles.component.js @@ -0,0 +1,463 @@ +'use strict'; +const angular = require('angular'); + +const uiRouter = require('angular-ui-router'); + +import routes from './roles.routes'; + +export class RolesComponent { + /*@ngInject*/ + constructor($scope, ansible, $uibModal, yamlFile, Projects, editor) { + 'ngInject'; + + $scope.treeOptions = { + nodeChildren: "children", + dirSelectable: true, + isLeaf: function (node) { + return !(node.type === 'directory'); + }, + injectClasses: { + ul: "a1", + li: "a2", + liSelected: "a7", + iExpanded: "a3", + iCollapsed: "a4", + iLeaf: "a5", + label: "a6", + labelSelected: "a8" + } + }; + + $scope.isopen = {roles: true, filebrowser: false, tasks: false}; + + $scope.selectedRole = {role: "", tasks: null}; + + $scope.selectedFile = {showSource: true, markdownContent: true, content: ""}; + + $scope.editRole = {value: false}; + $scope.showSaveFileButton = false; + + $scope.$on('projectLoaded', function () { + $scope.getRoles() + }); + + $scope.aceLoaded = function (_editor) { + _editor.$blockScrolling = Infinity; + }; + + // --------------------------------------- PLAYBOOKS ---------------- + + $scope.getRoles = function () { + ansible.getRoleList( + function (response) { + $scope.roles = response.data; + if(localStorage.selectedRoleName) + $scope.selectedRole.role = localStorage.selectedRoleName + }, + function (response) { + console.log(response.data) + } + ) + }; + + var getRoleByName = function(roleName){ + var result = null; + angular.forEach($scope.roles,function(role){ + if(role.name == roleName){ + result = role + } + }); + return result; + }; + + if ($scope.$parent.selectedProject && $scope.$parent.selectedProject.ansibleEngine) { + $scope.getRoles() + } + + $scope.loadingModuleCode = false; + + $scope.markdownContent = ""; + + $scope.showRoleCode = function (role_name) { + $scope.loadingModuleCode = true; + $scope.markdownContent = ''; + $scope.docType = 'text'; + $scope.selectedFile.content = 'Loading Role Files..'; + $scope.selectedRole.tasks = null; + $scope.roleData = null; + + if (!role_name) { + $scope.selectedFile.content = "Select a module"; + return; + } + ansible.getRoleFiles(role_name, function (response) { + $scope.loadingModuleCode = false; + $scope.selectedFile.content = JSON.stringify(response.data, null, '\t'); + $scope.docType = 'json'; + $scope.roleData = response.data; + + }); + }; + + $scope.$watch('selectedRole.role', function (newValue, oldValue) { + if (newValue && newValue !== oldValue) { + $scope.currentRole = newValue; + $scope.reloadRole(); + //$scope.isopen.roles = false; + $scope.isopen.filebrowser = true; + localStorage.selectedRoleName = $scope.selectedRole.role; + } + }); + + $scope.reloadRole = function () { + $scope.selectedFile.content = "Loading Code..."; + $scope.docType = 'txt'; + $scope.showRoleCode($scope.currentRole); + }; + + + + /*var setDocType = function (data, file) { + if (typeof data == 'object') { + $scope.selectedFile.content = JSON.stringify(data, null, '\t'); + } else { + $scope.selectedFile.content = data; + } + + $scope.docType = editor.ui_ace_doctype_map[file.extension.replace('.', '')]; + + if ($scope.docType == 'markdown') { + $scope.markdownContent = $scope.selectedFile.content; + $scope.selectedFile.showSource = false; + } + };*/ + + /** + * Show selected item in the tree + * @param file + * @param parent + */ + $scope.showSelected = function (file, parent, decrypt) { + + if($scope.editRole.value){ + return + } + + + if (file.children) { + $scope.selectedFile.content = JSON.stringify(file, null, '\t'); + $scope.docType = 'json'; + $scope.selectedRole.tasks = null; + return; + } + + $scope.selectedFile.content = 'Loading..'; + + var command = 'cat "' + file.path + '"'; + $scope.encryptedFile = false; + if(decrypt){ + command = 'ansible-vault view "' + file.path + '" --vault-password-file ~/.vault_pass.txt' + $scope.encryptedFile = true; + $scope.selectedFile.content = 'Loading Encrypted File..'; + } + + $scope.selectedFile.showSource = true; + $scope.markdownContent = ''; + $scope.docType = 'text'; + + $scope.selectedRole.tasks = null; + $scope.selectedFileName = file.name; + $scope.selectedFilePath = file.path; + $scope.parentNode = parent; + + ansible.executeCommand(command, + function (response) { + $scope.preChangeData = null; + editor.setContentAndType(response.data, file, $scope.selectedFile); + + var parentDirectory = file.path.replace(/^(.+)\/(.+)\/([^/]+)$/, "$2"); + if (parentDirectory == 'tasks') { + $scope.selectedRole.tasks = YAML.parse(response.data) || []; + $scope.isopen.tasks = true; + $scope.isopen.roles = false; + } + + if(response.data.indexOf('ANSIBLE_VAULT') > -1){ + editor.setContentAndType('Decrypting content...', file, $scope.selectedFile); + $scope.showSelected(file, parent, true); + } + + }, function (response) { + $scope.selectedFile.content = response.data; + + }) + }; + + $scope.showCreateFileModal = function (selectedFile, copyFile) { + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createTaskContent.html',*/ + templateUrl: 'app/designer/roles/new_file/new_file.html', + controller: 'NewFileController', + size: 'md', + backdrop: 'static', + keyboard: false, + closeByEscape: false, + closeByDocument: false, + resolve: { + copyFile: function () { + return copyFile + }, + selectedDirectory: function () { + if (selectedFile.type == 'directory') + return selectedFile.path; + else return $scope.parentNode.path + }, + selectedFileName: function () { + return selectedFile + } + } + }); + + modalInstance.result.then(function () { + //$scope.getRoles(); + $scope.reloadRole(); + }, function () { + + }); + }; + + $scope.showCreateRoleModal = function (copyRole) { + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createTaskContent.html',*/ + templateUrl: 'app/designer/roles/new_role/new_role.html', + controller: 'NewRoleController', + size: 'md', + backdrop: 'static', + keyboard: false, + closeByEscape: false, + closeByDocument: false, + resolve: { + copyRole: function () { + return copyRole + }, + selectedRoleName: function () { + return $scope.selectedRole.role + } + } + }); + + modalInstance.result.then(function () { + $scope.getRoles(); + }, function () { + + }); + + }; + + $scope.showSearchRoleModal = function () { + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createTaskContent.html',*/ + templateUrl: 'app/designer/roles/search_role/search_role.html', + controller: 'SearchRoleController', + size: 'lg', + backdrop: 'static', + keyboard: false, + closeByEscape: false, + closeByDocument: false, + resolve: { + selectedProject: function () { + return $scope.$parent.selectedProject + } + } + }); + + modalInstance.result.then(function () { + $scope.getRoles(); + }, function () { + + }); + + }; + + + $scope.saveRole = function () { + $scope.saveRoleLoading = true; + ansible.createRole($scope.selectedRole.role, $scope.selectedFile.content, + function (response) { + $scope.saveRoleLoading = false; + $scope.editRole.value = false; + }, + function (response) { + $scope.saveRoleLoading = false; + $scope.err_msg = response.data; + }) + }; + + $scope.deleteRole = function () { + $scope.deleteRoleLoading = true; + ansible.deleteRole($scope.selectedRole.role, + function (response) { + $scope.deleteRoleLoading = false; + $scope.selectedRole.role = ""; + $scope.selectedFile.content = ""; + $scope.roleData = null; + $scope.getRoles(); + }, + function (response) { + $scope.deleteRoleLoading = false; + $scope.err_msg = response.data; + $scope.selectedFile.content = ""; + $scope.roleData = null; + }) + }; + + $scope.loadingButtons = {}; + $scope.showSaveButton = {}; + + // ------------- PLAYBOOK ------------------ + $scope.saveTasksFile = function (buttonStates) { + + buttonStates = buttonStates || {}; + + buttonStates.loading = true; + var tasksFileContent = $scope.selectedFile.content; + + + ansible.createPlaybook($scope.selectedFilePath, tasksFileContent, + function (response) { + buttonStates.loading = false; + buttonStates.save = false; + }, + function (response) { + buttonStates.loading = false; + buttonStates.save = false; + buttonStates.err_msg = false; + }) + }; + + + $scope.updatePlaybookFileContent = function (save, buttonStates, preChangedData) { + $scope.selectedRole.tasks = preChangedData || $scope.selectedRole.tasks; + $scope.selectedFile.content = yamlFile.jsonToYamlFile($scope.selectedRole.tasks, 'Tasks File: ' + $scope.selectedFileName); + if (save) + $scope.saveTasksFile(buttonStates); + }; + + + $scope.editFile = function (selectedFile) { + if (selectedFile.type == 'directory')return; + + if (!$scope.preChangeData){ + console.log("No prechanged data, setting pre change data"); + $scope.preChangeData = angular.copy($scope.selectedFile.content); + } + + $scope.editRole.value = true; + $scope.showSaveFileButton = true; + + }; + + $scope.cancelFileChanges = function (selectedFile) { + if ($scope.preChangeData) { + console.log("Replacing content with pre changed data"); + $scope.selectedFile.content = angular.copy($scope.preChangeData); + $scope.preChangeData = null; + console.log("Clearing pre changed data") + } + + $scope.editRole.value = false; + $scope.showSaveFileButton = false; + + }; + + $scope.saveFile = function (selectedFile) { + $scope.showSaveFileButtonLoading = true; + $scope.preChangeData = null; + ansible.updateFile(selectedFile.path, $scope.selectedFile.content, + function (response) { + $scope.showSaveFileButtonLoading = false; + $scope.showSaveFileButton = false; + $scope.editRole.value = false; + }, function (error) { + $scope.showSaveFileButtonLoading = false; + $scope.err_msg = error.data; + }) + + }; + + $scope.deleteFile = function (selectedFile) { + ansible.deleteFile(selectedFile.path, function (response) { + $scope.reloadRole(); + }, function (error) { + $scope.showSaveFileButtonLoading = false; + $scope.err_msg = error.data; + }) + }; + // ------------------- EXECUTE PLAYBOOK MODAL ------------- + + $scope.executeAnsiblePlayBook = function (tags, executionType, executionName, selectedPlay) { + console.log("Tags type" + typeof tags); + + var projectRolesFolder = Projects.selectedProject.ansibleEngine.projectFolder + '/roles'; + var rolesFolder = projectRolesFolder + '/' + $scope.selectedRole.role; + var roleName = $scope.selectedRole.role; + console.log("Projects Roles Folder" + projectRolesFolder); + + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createTaskContent.html',*/ + templateUrl: 'app/designer/execution/executeModal.html', + controller: 'ExecutionController', + size: 'lg', + backdrop: 'static', + keyboard: false, + closeByEscape: false, + closeByDocument: false, + resolve: { + tags: function () { + return tags + }, + selectedProject: function () { + return Projects.selectedProject + }, + selectedPlaybook: function () { + return {playbook: $scope.selectedRole.role + '/tests/test.yml'}; + }, + selectedPlay: function () { + return selectedPlay + }, + executionType: function () { + return executionType + }, + executionName: function () { + return executionName + }, + readOnly: function () { + return false + }, + runData: function () { + return null + }, + projectFolder: function () { + return projectRolesFolder + }, + roleName: function () { + return roleName + } + } + }); + }; + } +} + +export default angular.module('webAppApp.roles', [uiRouter]) + .config(routes) + .component('roles', { + template: require('./roles.html'), + controller: RolesComponent, + controllerAs: 'rolesCtrl' + }) + .name; diff --git a/client/app/designer/roles/roles.component.spec.js b/client/app/designer/roles/roles.component.spec.js new file mode 100644 index 0000000..ab15dcf --- /dev/null +++ b/client/app/designer/roles/roles.component.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Component: RolesComponent', function() { + // load the controller's module + beforeEach(module('webAppApp.roles')); + + var RolesComponent; + + // Initialize the controller and a mock scope + beforeEach(inject(function($componentController) { + RolesComponent = $componentController('roles', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/roles/roles.css b/client/app/designer/roles/roles.css new file mode 100644 index 0000000..e69de29 diff --git a/client/app/designer/roles/roles.html b/client/app/designer/roles/roles.html new file mode 100644 index 0000000..ae532e5 --- /dev/null +++ b/client/app/designer/roles/roles.html @@ -0,0 +1,110 @@ +
+ + +
+ + Roles + + + + + + + + + + +
+ + + + + + + + + + + + + +
SelectName
+ {{role}}
+
+ +
+ +
+ + File Browser {{selectedRole.role ? '-' + selectedRole.role : ''}} + + + + {{node.name}} + + + + + + + + +
+ +
+ + Tasks {{selectedRole.role ? '-' + selectedRole.role : ''}} + + + +
+ +
+ + +
+ + +
+ + + + +
+ +
+ +
+
+ +
diff --git a/client/app/designer/roles/roles.routes.js b/client/app/designer/roles/roles.routes.js new file mode 100644 index 0000000..db7be0e --- /dev/null +++ b/client/app/designer/roles/roles.routes.js @@ -0,0 +1,10 @@ +'use strict'; + +export default function($stateProvider) { + 'ngInject'; + $stateProvider + .state('designer.roles', { + url: '/roles', + template: '' + }); +} diff --git a/client/app/designer/roles/search_role/search_role.controller.js b/client/app/designer/roles/search_role/search_role.controller.js new file mode 100644 index 0000000..9ed4e60 --- /dev/null +++ b/client/app/designer/roles/search_role/search_role.controller.js @@ -0,0 +1,66 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function searchRoleController($scope,ansible,selectedProject,$uibModalInstance) { + + $scope.searchText = ''; + $scope.searchLoading = false; + + $scope.selectedRole = {role:{}}; + + + + $scope.searchRoles = function(){ + $scope.searchResult = []; + $scope.searchLoading = true; + ansible.searchRolesGalaxy($scope.searchText, + function(response){ + $scope.searchLoading = false; + $scope.searchResult = $scope.searchResult.concat(response.data) + },function(response){ + $scope.searchLoading = false; + $scope.err_msg = response.data + }); + + ansible.searchRolesGithub($scope.searchText, + function(response){ + $scope.searchLoading = false; + $scope.searchResult = $scope.searchResult.concat(response.data) + },function(response){ + $scope.searchLoading = false; + $scope.err_msg = response.data + }) + + + }; + + + $scope.importRole = function(role){ + $scope.importLoading = true; + + if(role.type === 'galaxy')role.url = role.name; + + ansible.importRole(role.type,role.url, + function(response){ + $scope.importLoading = false; + $scope.ok(); + },function(response){ + $scope.importLoading = false; + $scope.err_msg = response.data + }); + }; + + + $scope.ok = function () { + $uibModalInstance.close(null); + }; + + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; +} + +export default angular.module('webAppApp.search_role', []) + .controller('SearchRoleController', searchRoleController) + .name; diff --git a/client/app/designer/roles/search_role/search_role.controller.spec.js b/client/app/designer/roles/search_role/search_role.controller.spec.js new file mode 100644 index 0000000..dec48bb --- /dev/null +++ b/client/app/designer/roles/search_role/search_role.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: SearchRoleCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.search_role')); + + var SearchRoleCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + SearchRoleCtrl = $controller('SearchRoleCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/roles/search_role/search_role.html b/client/app/designer/roles/search_role/search_role.html new file mode 100644 index 0000000..421c8e3 --- /dev/null +++ b/client/app/designer/roles/search_role/search_role.html @@ -0,0 +1,70 @@ + + diff --git a/client/app/designer/tasks/new_task/new_task.controller.js b/client/app/designer/tasks/new_task/new_task.controller.js new file mode 100644 index 0000000..153328c --- /dev/null +++ b/client/app/designer/tasks/new_task/new_task.controller.js @@ -0,0 +1,446 @@ +'use strict'; +const angular = require('angular'); + + + +/*@ngInject*/ +export function newTaskController($window, $scope, $sce, $uibModal, ansi2html, ansible, $uibModalInstance, tasksList, selectedTaskIndex, copyTask, files, selectedPlay, selectedRole, $filter, Projects) { + var selectedTask; + + /** + * Edit task - in case of edit task , selectedTaskIndex is not null. + * Set selectedTask to a copy of selected task to edit. + */ + if(selectedTaskIndex > -1 && tasksList){ + if(copyTask){ + selectedTask = angular.copy(tasksList[selectedTaskIndex]); + selectedTask.name = "Copy of " + selectedTask.name; + selectedTaskIndex = null; + }else{ + selectedTask = tasksList[selectedTaskIndex] + } + + } + + /** + * List of files for include purpose + */ + if(files){ + $scope.files = files; + } + + $scope.getModuleDescriptionLoading = false; + $scope.modulesLoading = false; + + $scope.modules = null; + $scope.singeLineModules = ["shell"]; + $scope.showHelp = false; + + $scope.newTask = {}; + $scope.title = "New Task"; + $scope.createTaskLoading = false; + + /** + * Get Ansible Modules + * If Edit Task, get module description for selected task + */ + $scope.getAnsibleModules = function(){ + $scope.modulesLoading = true; + ansible.getAnsibleModules(function(response){ + $scope.modules = response; + $scope.modulesLoading = false; + + if(selectedTask){ + $scope.title = "Edit Task"; + selectedTask = angular.copy(selectedTask); + $scope.newTask = selectedTask; + if(selectedTask.tags)$scope.newTask.tags = $scope.newTask.tags.join(','); + var module = $scope.getModuleFromTask(selectedTask); + $scope.getModuleDescription(module,true) + } + + }, function(response){ + $scope.result = $sce.trustAsHtml(ansi2html.toHtml(response.data).replace(/\n/g, "
").replace(/ /g," ")); + + }); + + }; + + + /** + * Get Module Description whenever a module is selected by the user + * @param module - Module Object + * @param override - Override variables in case of edit task + * @param refresh - Refresh module description from server. Don't display from cache + */ + $scope.getModuleDescription = function(module,override,refresh){ + + if(!module)return; + + var module_copy = angular.copy(module); + + $scope.getModuleDescriptionLoading = true; + var moduleName = module.name; + + if($scope.singeLineModules.indexOf(moduleName) > -1){ + module.singleLine = true; + } + + $scope.detailHelp = ""; + $scope.examples = ""; + module.variables = []; + + ansible.getAnsibleModuleDescription(moduleName, + function(response){ + $scope.showHelp = true; + $scope.result = $sce.trustAsHtml(ansi2html.toHtml(response).replace(/\n/g, "
").replace(/ /g," ")); + + $scope.detailHelp = response; + $scope.examples = response.substr(response.indexOf("#")); + //var re = /(^[-=] .*)/gm; + //var re = /(^[-=] .*)[^]*?(?:(\(Choices[^]+?\))?\s*(\[.*\])|(?=^[-=]|^EXAMPLES))/gm; + var re = /(^[-=] .*)([^]*?)(?:(\(Choices[^]+?\))?\s*(\[.*\])|(?=^[-=]|^EXAMPLES))/gm; + var m; + + while ((m = re.exec($scope.detailHelp.split("EXAMPLES")[0]+"EXAMPLES")) !== null) { + //while ((m = re.exec($scope.detailHelp.split("#")[0])) !== null) { + //while ((m = re.exec($scope.detailHelp)) !== null) { + if (m.index === re.lastIndex) { + re.lastIndex++; + } + // View your result using the m-variable. + // eg m[0] etc. + + var option_name = m[1]; + var description = m[2]; + var choices = m[3]; + var default_value = m[4]; + + + var breakup = option_name.split(" "); + var variable_name = breakup[1]; + var mandatory = breakup[0] == "="; + + var complex_value = {}; + + if(default_value) + default_value = default_value.replace(/\[Default: (.*)\]/,"$1"); + + if(default_value == 'None') + default_value = null + + var variable_value = default_value || ''; + + if(choices) + choices = choices.replace(/\s+/g,"").replace(/\n\s+/g,"").replace(/\(Choices:(.*)\)/,"$1").split(","); + + if(override && module_copy.variables){ + var matching_variable = module_copy.variables.filter(function(item){ + if(item.name == variable_name){ + return true + } + }); + if(matching_variable.length){ + variable_value = matching_variable[0].value; + if(typeof variable_value=='object'){ + complex_value = angular.copy(variable_value) + } + } + + } + + module.variables.push({name:variable_name,description:description,mandatory:mandatory,value:variable_value,complexValue:complex_value,choices:choices,default_value:default_value}); + $scope.getModuleDescriptionLoading = false; + } + }, function(response){ + $scope.result = $sce.trustAsHtml(ansi2html.toHtml(response.data).replace(/\n/g, "
")); + console.log(ansi2html.toHtml(response.data)); + $scope.detailHelp = response.data; + $scope.getModuleDescriptionLoading = false; + },refresh) + }; + + /** + * Reload Module Description and variables. Ignore displaying from cache. + * Used when a custom module is updated and description and variables info need to be updated. + * @param module - module object + */ + $scope.reloadModuleDetails = function(module){ + + if(selectedTask){ + $scope.getModuleDescription(module,true,true) + }else{ + $scope.getModuleDescription(module,false,true) + } + }; + + + /** + * Identify module from a given task object. + * @param task - Single task object containing task properties + * @returns {{}} + */ + $scope.getModuleFromTask = function(task){ + var moduleObject = {}; + $scope.local_action = false; + var task_properties = null; + + var module = ansible.getModuleFromTask(task); + + if(module === 'include'){ + module = null; + task.tags = task.include.replace(/.*tags=(.*)/,"$1") + return; + }else if(module === 'local_action'){ + $scope.local_action = true; + module = task.local_action.module; + task_properties = task.local_action; + delete task_properties.module; + }else{ + task_properties = task[module]; + } + + angular.forEach($scope.modules, function(item,index) { + if(item.name == module){ + moduleObject = item; + $scope.newTask.module = item; + } + }); + + + if(!(moduleObject && moduleObject.name)){ + $scope.err_msg = "Unable to find module " + module + " in Ansible controller"; + return + } + + //moduleObject.name = module; + moduleObject.variables = []; + if(typeof task_properties == "string"){ + moduleObject.variables.push({'name':'free_form','value':task_properties}); + + var re = /\b(\w+)=\s*([^=]*\S)\b\s*(?=\w+=|$)/g; + var m; + + while ((m = re.exec(task_properties)) !== null) { + if (m.index === re.lastIndex) { + re.lastIndex++; + } + // View your result using the m-variable. + // eg m[0] etc. + var k=m[1]; + var v=m[2]; + moduleObject.variables.push({'name':k,'value':v}) + } + + }else if(typeof task_properties == "object"){ + + angular.forEach(task_properties,function(value,key){ + this.push({'name':key,'value':value,'complexValue':value}) + },moduleObject.variables) + + } + return moduleObject + + }; + + /** + * Create Task - Creates new task object and set task variables. + * Push new task object to tasksList + * Close new task modal + */ + $scope.createTask = function(){ + + if(!$scope.newTask.module && !$scope.newTask.include){ + $scope.err_msg = "Must select atleast one module or include statement"; + return + } + + $scope.createTaskLoading = true; + + if(!tasksList){ + tasksList = [] + } + + var taskObject = {name:$scope.newTask.name}; + + if($scope.newTask.include) + taskObject['include'] = $scope.newTask.include; + + if($scope.newTask.tags) + taskObject['tags'] = $scope.newTask.tags.split(','); + + if($scope.newTask.register) + taskObject['register'] = $scope.newTask.register; + + if($scope.newTask.async){ + taskObject['async'] = $scope.newTask.async; + if(!$scope.newTask.poll) + $scope.newTask.poll = 10; + taskObject['poll'] = $scope.newTask.poll; + } + + var variablesObject = null; + if($scope.newTask.module){ + if($scope.newTask.module.singleLine){ + variablesObject = ""; + //Add all mandatory variables first + angular.forEach($scope.newTask.module.variables.filter(function(item){ + return item.mandatory + }),function(item){ + if(item.name == 'free_form'){ + variablesObject += item.value; + }else if(item.value){ + variablesObject += " " + item.name + "=" + item.value; + } + }); + + //Add optional variables + angular.forEach($scope.newTask.module.variables.filter(function(item){ + return !item.mandatory + }),function(item){ + if(item.value != item.default_value){ + if(item.name == 'free_form'){ + variablesObject += item.value; + }else if(item.value){ + variablesObject += " " + item.name + "=" + item.value; + } + } + }); + + }else{ + variablesObject = {}; + angular.forEach($scope.newTask.module.variables,function(item){ + if((item.value || (item.isComplexVariable && item.complexValue)) && item.value != item.default_value){ + if(item.isComplexVariable){ + variablesObject[item.name] = item.complexValue; + }else{ + variablesObject[item.name] = item.value; + } + + } + }); + } + + taskObject[$scope.newTask.module.name] = variablesObject; + + if($scope.local_action){ + variablesObject.module = $scope.newTask.module.name; + taskObject['local_action'] = variablesObject; + } + } + + + + if(selectedTaskIndex != null){ + // If Edit Task + + tasksList[selectedTaskIndex] = taskObject + + }else{ + // If New Task + + tasksList.push(taskObject); + } + + $uibModalInstance.close(taskObject); + }; + + /** + * Close modal + */ + $scope.ok = function () { + $uibModalInstance.close($scope.newTask); + }; + + + /** + * Cancel modal + */ + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; + + /** + * Get host variables using Ansible Python API in the backend + */ + $scope.getHostVars = function(){ + + if(!(selectedPlay && selectedPlay.play && selectedPlay.play.hosts))return; + + ansible.getVars(Projects.selectedInventoryFileName,selectedPlay.play.hosts,function(response){ + console.log(response.data); + if(response.data.length) + $scope.hostvars = $filter('dictToKeyValueArray')(response.data[0]); + else $scope.err_msg = "Getting host variables - No variables returned" ; + + },function(error){ + console.log(error.data); + $scope.err_msg = "Getting host variables - " + error.data; + }) + }; + + if(selectedPlay) + $scope.getHostVars(); + + + $scope.getRoleVars = function(){ + + if(!(selectedRole && selectedRole.role))return; + + ansible.getRoleVars(selectedRole.role,function(response){ + console.log(response.data); + if(response.data) + $scope.hostvars = $filter('dictToKeyValueArray')(response.data); + else $scope.err_msg = "Getting host variables - No variables returned" ; + + },function(error){ + console.log(error.data); + $scope.err_msg = "Getting host variables - " + error.data; + }) + }; + + if(selectedRole) + $scope.getRoleVars(); + + + if(!$scope.modules){ + $scope.getAnsibleModules(); + } + + $scope.showComplexVariable = function(variable){ + variable.isComplexVariable = true; + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createTaskContent.html',*/ + templateUrl: 'app/modals/complex_var_modal/complexVariable.html', + controller: 'ComplexVarModalController', + size: 'sm', + backdrop: 'static', + keyboard: false, + closeByEscape: false, + closeByDocument: false, + resolve: { + path: function () { + return variable.name + }, + hostvars: function(){ + return $scope.hostvars + }, + members: function(){ + return variable.complexValue + } + } + }); + + modalInstance.result.then(function (selectedItem) { + variable.complexValue = selectedItem + }, function () { + + }); + + } +} + +export default angular.module('webAppApp.new_task', []) + .controller('NewTaskController', newTaskController) + .name; diff --git a/client/app/designer/tasks/new_task/new_task.controller.spec.js b/client/app/designer/tasks/new_task/new_task.controller.spec.js new file mode 100644 index 0000000..c77640c --- /dev/null +++ b/client/app/designer/tasks/new_task/new_task.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: NewTaskCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.new_task')); + + var NewTaskCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + NewTaskCtrl = $controller('NewTaskCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/designer/tasks/new_task/new_task.html b/client/app/designer/tasks/new_task/new_task.html new file mode 100644 index 0000000..c7e6d7d --- /dev/null +++ b/client/app/designer/tasks/new_task/new_task.html @@ -0,0 +1,134 @@ + + + + diff --git a/client/app/designer/tasks/tasks.css b/client/app/designer/tasks/tasks.css new file mode 100644 index 0000000..e69de29 diff --git a/client/app/designer/tasks/tasks.directive.js b/client/app/designer/tasks/tasks.directive.js new file mode 100644 index 0000000..0d1921e --- /dev/null +++ b/client/app/designer/tasks/tasks.directive.js @@ -0,0 +1,229 @@ +'use strict'; +const angular = require('angular'); + +export default angular.module('webAppApp.tasks', []) + .directive('tasks', function(ansible, $uibModal) { + return { + templateUrl: 'app/designer/tasks/tasks.html', + restrict: 'EA', + scope: { + tasksList: '=', + selectedPlay: '=', + savePlaybook: '&', + selectedRole: '=', + updatePlaybookFileContent: '&', + executeAnsiblePlayBook: '&', + files: '=' //List of files for include purpose + }, + link: function (scope, element, attrs) { + scope.getModuleFromTask = ansible.getModuleFromTask; + + scope.buttonStates = {loading:false,save:false,err_msg:false}; + + scope.tasksMetaData = []; + + scope.$watch('tasksList',function(){ + console.log('tasks list changed'); + scope.tasksMetaData = []; + + angular.forEach(scope.tasksList,function(task){ + var taskModule = ansible.getModuleFromTask(task); + var taskName = task.name; + + if(taskModule === 'include'){ + taskName = task[taskModule].replace(/(.*yml) .*/,"$1") + } + + scope.tasksMetaData.push({taskModule:taskModule,taskName:taskName,selected:false}) + }) + + },true); + + + /** + * Detect when the user selects tasks. + * Enable play button if tasks are selected and has tags assigned + * Enable delete button if tasks are selected + */ + scope.$watch('tasksMetaData',function(newValue,oldValue){ + scope.selectedTasksPlayButton = false; + scope.selectedTasksDeleteButton = false + + if(!(scope.tasksMetaData))return; + + var selectedTasks = scope.tasksMetaData.filter(item => item.selected); + var includeTasks = scope.tasksMetaData.filter(item => item.taskModule === 'include'); + var selectedTasksWithoutTags = []; + + /** + * Find selected tasks without any tags. + * If there are any play button will not be enabled + */ + angular.forEach(scope.tasksMetaData,function(item,index){ + scope.tasksListItem = scope.tasksList[index]; + if(!scope.tasksListItem.tags && item.selected){ + selectedTasksWithoutTags.push(scope.tasksListItem) + } + }); + + console.log("selectedTasksWithoutTags=") + console.log(selectedTasksWithoutTags) + + if(selectedTasks.length){ + //if(!includeTasks.length && !selectedTasksWithoutTags.length){ + if(!selectedTasksWithoutTags.length){ + scope.selectedTasksPlayButton = true + } + scope.selectedTasksDeleteButton = true + + }else{ + scope.selectedTasksPlayButton = false; + scope.selectedTasksDeleteButton = false + } + + },true); + + + //scope.moveUp = scope.moveUp(); + //scope.moveDown = scope.moveDown(); + scope.savePlaybook = scope.savePlaybook(); + scope.updatePlaybookFileContent = scope.updatePlaybookFileContent(); + scope.executeAnsiblePlayBook = scope.executeAnsiblePlayBook(); + + scope.showTaskModal = function(selectedTaskIndex, copyTask){ + var modalInstance = $uibModal.open({ + animation: true, + /*templateUrl: 'createTaskContent.html',*/ + templateUrl: 'app/designer/tasks/new_task/new_task.html', + controller: 'NewTaskController', + size: 'lg', + backdrop : 'static', + keyboard : false, + closeByEscape : false, + closeByDocument : false, + resolve: { + selectedProject: function () { + return scope.$parent.selectedProject; + }, + selectedPlay: function(){ + return scope.selectedPlay + }, + selectedRole: function(){ + return scope.selectedRole + }, + tasksList: function () { + return scope.tasksList; + }, + selectedTaskIndex: function(){ + return selectedTaskIndex + }, + copyTask : function(){ + return copyTask + }, + //List of files for include purpose + files: function(){ + return scope.files + } + } + }); + + modalInstance.result.then( + function (newTask) { + // if(!selectedTaskIndex) + // scope.tasksList.push(newTask); + scope.updatePlaybookFileContent(true); + //$scope.selectedPlay = {play: ""}; + }, function () { + + }); + + }; + + + scope.deleteTask = function(index){ + scope.tasksList.splice(index,1); + scope.updatePlaybookFileContent(true); + }; + + scope.deleteTasks = function(){ + + scope.tasksMetaData.filter(function(item, index){ + if(item.selected){ + scope.tasksList.splice(index,1); + } + }); + scope.updatePlaybookFileContent(true); + }; + + + scope.moveUp = function(list,index,buttonVariable){ + if(!scope.preChangeData) scope.preChangeData = angular.copy(list); + var temp = angular.copy(list[index]); + list[index] = list[index-1]; + list[index-1] = temp; + + scope.updatePlaybookFileContent(false); + + scope.buttonStates.save = true + + }; + + scope.cancelChange = function(buttonVariable){ + if(scope.preChangeData){ + //scope.tasksList = angular.copy(scope.preChangeData); + scope.selectedPlay.play.tasks = angular.copy(scope.preChangeData); + scope.preChangeData = null + + } + scope.updatePlaybookFileContent(false,null,scope.tasksList); + + scope.buttonStates.save = false; + }; + + scope.moveDown = function(list,index,buttonVariable){ + if(!scope.preChangeData) scope.preChangeData = angular.copy(list); + var temp = angular.copy(list[index]); + list[index] = list[index+1]; + list[index+1] = temp; + + scope.updatePlaybookFileContent(false); + scope.buttonStates.save = true; + + }; + + + scope.executeSelectedTasks = function(){ + + /*var selectedTasks = scope.tasksMetaData.map(function(item){return item.selected});*/ + var selectedTags = []; + var selectedTaskNames = []; + /*if(selectedTasks.length){ + selectedTags = selectedTasks.map(function(item){return item.tags}); + selectedTaskNames = selectedTasks.map(function(item){return item.name}) + }*/ + + angular.forEach(scope.tasksMetaData, function(item,index){ + if(item.selected){ + if(scope.tasksList[index].tags){ + // As tags is an array and each task can have multiple tags + var task_tags = scope.tasksList[index].tags + if(typeof task_tags == 'object') + task_tags = task_tags[0] //task_tags.join(',') + selectedTags.push(task_tags); + selectedTaskNames.push(scope.tasksList[index].name) + } + } + }); + + if(selectedTags.length){ + var play = scope.selectedPlay && scope.selectedPlay.play; + scope.executeAnsiblePlayBook(selectedTags,'Tasks',selectedTaskNames.join(","),play) + } + + + }; + + } + }; + }) + .name; diff --git a/client/app/designer/tasks/tasks.directive.spec.js b/client/app/designer/tasks/tasks.directive.spec.js new file mode 100644 index 0000000..1ee658f --- /dev/null +++ b/client/app/designer/tasks/tasks.directive.spec.js @@ -0,0 +1,20 @@ +'use strict'; + +describe('Directive: tasks', function() { + // load the directive's module and view + beforeEach(module('webAppApp.tasks')); + beforeEach(module('app/designer/tasks/tasks.html')); + + var element, scope; + + beforeEach(inject(function($rootScope) { + scope = $rootScope.$new(); + })); + + it('should make hidden element visible', inject(function($compile) { + element = angular.element(''); + element = $compile(element)(scope); + scope.$apply(); + expect(element.text()).to.equal('this is the tasks directive'); + })); +}); diff --git a/client/app/designer/tasks/tasks.html b/client/app/designer/tasks/tasks.html new file mode 100644 index 0000000..f05d002 --- /dev/null +++ b/client/app/designer/tasks/tasks.html @@ -0,0 +1,53 @@ +
+ + + + + + + + + + + + + + + + + +
SelectNameModuleActions
+ {{tasksMetaData[$index].taskName}}{{tasksMetaData[$index].taskModule}} +
+ + + + + + +
+
+
+ + + + + + diff --git a/client/app/directives/complexVar/complexVar.controller.js b/client/app/directives/complexVar/complexVar.controller.js new file mode 100644 index 0000000..707c2db --- /dev/null +++ b/client/app/directives/complexVar/complexVar.controller.js @@ -0,0 +1,60 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function complexVarController($scope,$filter) { + 'ngInject'; + var loadMembers = function(){ + $scope.membersCopy = angular.copy($scope.members); + //var membersArray = $filter('addDotInKey')($filter('dictToKeyValueArray')($scope.membersCopy)); + var membersArray = ($filter('dictToKeyValueArraySimple')($scope.membersCopy)); + + $scope.tabgroup = $scope.tabgroup || 0; + + if(membersArray.length) + $scope.membersCopy = membersArray; + else + $scope.membersCopy = [{key:"",value:""}]; + + $scope.path = $scope.path || ""; + + angular.forEach($scope.membersCopy,function(member){ + if(Object.prototype.toString.call(member.value) === '[object Object]'){ + member.type = 'object'; + }else if(Object.prototype.toString.call(member.value) === '[object Array]'){ + member.type = 'array'; + } + + }) + + }; + + loadMembers(); + + $scope.setMemberType = function(member,type){ + if(type === 'object'){ + member.value = {}; + member.type = 'object'; + }else if(type === 'array'){ + member.value = {}; + member.type = 'array'; + } + }; + + $scope.$on('membersUpdated',function(){ + console.log('On Members Updated'); + console.log($scope.members); + loadMembers(); + }); + + $scope.$watch('membersCopy',function(){ + if($scope.type === 'object') + $scope.members = $filter('removeDotInKey')($filter('keyValueArrayToDict')($scope.membersCopy)); + else if($scope.type === 'array') + $scope.members = $filter('keyValueArrayToArray')($scope.membersCopy); + },true) +} + +export default angular.module('webAppApp.complexVarCtrl', []) + .controller('ComplexVarController', complexVarController) + .name; diff --git a/client/app/directives/complexVar/complexVar.controller.spec.js b/client/app/directives/complexVar/complexVar.controller.spec.js new file mode 100644 index 0000000..9ace95d --- /dev/null +++ b/client/app/directives/complexVar/complexVar.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: ComplexVarCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.complexVar')); + + var ComplexVarCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + ComplexVarCtrl = $controller('ComplexVarCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/directives/complexVar/complexVar.css b/client/app/directives/complexVar/complexVar.css new file mode 100644 index 0000000..b8e523b --- /dev/null +++ b/client/app/directives/complexVar/complexVar.css @@ -0,0 +1,8 @@ +.complex-var-input { + line-height: 1; + padding-right: 5px; + padding-left: 5px; + padding-top: 2px; + padding-bottom: 2px; + height: 25px; +} diff --git a/client/app/directives/complexVar/complexVar.directive.js b/client/app/directives/complexVar/complexVar.directive.js new file mode 100644 index 0000000..116f3ea --- /dev/null +++ b/client/app/directives/complexVar/complexVar.directive.js @@ -0,0 +1,20 @@ +'use strict'; +const angular = require('angular'); + +export default angular.module('webAppApp.complexVar', []) + .directive('complexVar', function() { + return { + template: require('./complexVar.html'), + restrict: 'EA', + scope: { + members: '=', + path: '=', + type: '=', + inputWidth: '=', + tabgroup: '=?bind', + hostVars: '=' + }, + controller: 'ComplexVarController' + }; + }) + .name; diff --git a/client/app/directives/complexVar/complexVar.directive.spec.js b/client/app/directives/complexVar/complexVar.directive.spec.js new file mode 100644 index 0000000..58496c9 --- /dev/null +++ b/client/app/directives/complexVar/complexVar.directive.spec.js @@ -0,0 +1,20 @@ +'use strict'; + +describe('Directive: complexVar', function() { + // load the directive's module and view + beforeEach(module('webAppApp.complexVar')); + beforeEach(module('app/directives/complexVar/complexVar.html')); + + var element, scope; + + beforeEach(inject(function($rootScope) { + scope = $rootScope.$new(); + })); + + it('should make hidden element visible', inject(function($compile) { + element = angular.element(''); + element = $compile(element)(scope); + scope.$apply(); + expect(element.text()).to.equal('this is the complexVar directive'); + })); +}); diff --git a/client/app/directives/complexVar/complexVar.html b/client/app/directives/complexVar/complexVar.html new file mode 100644 index 0000000..65c33db --- /dev/null +++ b/client/app/directives/complexVar/complexVar.html @@ -0,0 +1,38 @@ +
+
+
+
Key
+
+
+
Value
+
+
+ +
+ +
+
+ +
+
+ + +
+
+
+ + + + +
+
+
+
+ + +
+ + +
+
+ diff --git a/client/app/filters/addDotInKey/addDotInKey.filter.js b/client/app/filters/addDotInKey/addDotInKey.filter.js new file mode 100644 index 0000000..331033f --- /dev/null +++ b/client/app/filters/addDotInKey/addDotInKey.filter.js @@ -0,0 +1,14 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function addDotInKeyFilter() { + return function(input) { + return JSON.parse(JSON.stringify(input).replace(/__dot__/g,'.')); + }; +} + + +export default angular.module('webAppApp.addDotInKey', []) + .filter('addDotInKey', addDotInKeyFilter) + .name; diff --git a/client/app/filters/addDotInKey/addDotInKey.filter.spec.js b/client/app/filters/addDotInKey/addDotInKey.filter.spec.js new file mode 100644 index 0000000..e9ba83d --- /dev/null +++ b/client/app/filters/addDotInKey/addDotInKey.filter.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Filter: addDotInKey', function() { + // load the filter's module + beforeEach(module('webAppApp.addDotInKey')); + + // initialize a new instance of the filter before each test + var addDotInKey; + beforeEach(inject(function($filter) { + addDotInKey = $filter('addDotInKey'); + })); + + it('should return the input prefixed with "addDotInKey filter:"', function() { + var text = 'angularjs'; + expect(addDotInKey(text)).to.equal('addDotInKey filter: ' + text); + }); +}); diff --git a/client/app/filters/dictToKeyValueArray/dictToKeyValueArray.filter.js b/client/app/filters/dictToKeyValueArray/dictToKeyValueArray.filter.js new file mode 100644 index 0000000..beef309 --- /dev/null +++ b/client/app/filters/dictToKeyValueArray/dictToKeyValueArray.filter.js @@ -0,0 +1,29 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function dictToKeyValueArrayFilter() { + var convert = function (input,prefix) { + var result = []; + angular.forEach(input,function(value,key){ + key = key.replace(/\./g,"__dot__"); + if(prefix){ + key = prefix + '.' + key; + } + if(typeof value != 'object'){ + result.push({"key":key ,"value":value}) + }else{ + result = result.concat(convert(value,key)) + } + + }); + return result; + }; + + return convert; +} + + +export default angular.module('webAppApp.dictToKeyValueArray', []) + .filter('dictToKeyValueArray', dictToKeyValueArrayFilter) + .name; diff --git a/client/app/filters/dictToKeyValueArray/dictToKeyValueArray.filter.spec.js b/client/app/filters/dictToKeyValueArray/dictToKeyValueArray.filter.spec.js new file mode 100644 index 0000000..6026cc4 --- /dev/null +++ b/client/app/filters/dictToKeyValueArray/dictToKeyValueArray.filter.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Filter: dictToKeyValueArray', function() { + // load the filter's module + beforeEach(module('webAppApp.dictToKeyValueArray')); + + // initialize a new instance of the filter before each test + var dictToKeyValueArray; + beforeEach(inject(function($filter) { + dictToKeyValueArray = $filter('dictToKeyValueArray'); + })); + + it('should return the input prefixed with "dictToKeyValueArray filter:"', function() { + var text = 'angularjs'; + expect(dictToKeyValueArray(text)).to.equal('dictToKeyValueArray filter: ' + text); + }); +}); diff --git a/client/app/filters/dictToKeyValueArraySimple/dictToKeyValueArraySimple.filter.js b/client/app/filters/dictToKeyValueArraySimple/dictToKeyValueArraySimple.filter.js new file mode 100644 index 0000000..bdd5e86 --- /dev/null +++ b/client/app/filters/dictToKeyValueArraySimple/dictToKeyValueArraySimple.filter.js @@ -0,0 +1,20 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function dictToKeyValueArraySimpleFilter() { + var convert = function (input,prefix) { + var result = []; + angular.forEach(input,function(value,key){ + result.push({"key":key ,"value":value}) + }); + return result; + }; + + return convert; +} + + +export default angular.module('webAppApp.dictToKeyValueArraySimple', []) + .filter('dictToKeyValueArraySimple', dictToKeyValueArraySimpleFilter) + .name; diff --git a/client/app/filters/dictToKeyValueArraySimple/dictToKeyValueArraySimple.filter.spec.js b/client/app/filters/dictToKeyValueArraySimple/dictToKeyValueArraySimple.filter.spec.js new file mode 100644 index 0000000..b18b7c2 --- /dev/null +++ b/client/app/filters/dictToKeyValueArraySimple/dictToKeyValueArraySimple.filter.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Filter: dictToKeyValueArraySimple', function() { + // load the filter's module + beforeEach(module('webAppApp.dictToKeyValueArraySimple')); + + // initialize a new instance of the filter before each test + var dictToKeyValueArraySimple; + beforeEach(inject(function($filter) { + dictToKeyValueArraySimple = $filter('dictToKeyValueArraySimple'); + })); + + it('should return the input prefixed with "dictToKeyValueArraySimple filter:"', function() { + var text = 'angularjs'; + expect(dictToKeyValueArraySimple(text)).to.equal('dictToKeyValueArraySimple filter: ' + text); + }); +}); diff --git a/client/app/filters/json2yaml/json2yaml.filter.js b/client/app/filters/json2yaml/json2yaml.filter.js new file mode 100644 index 0000000..3742e79 --- /dev/null +++ b/client/app/filters/json2yaml/json2yaml.filter.js @@ -0,0 +1,118 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function json2yamlFilter() { + + + /* + * TODO, lots of concatenation (slow in js) + */ + var spacing = " "; + + function getType(obj) { + var type = typeof obj; + if (obj instanceof Array) { + return 'array'; + } else if (type == 'string') { + return 'string'; + } else if (type == 'boolean') { + return 'boolean'; + } else if (type == 'number') { + return 'number'; + } else if (type == 'undefined' || obj === null) { + return 'null'; + } else { + return 'hash'; + } + } + + function convert(obj, ret) { + var type = getType(obj); + + switch(type) { + case 'array': + convertArray(obj, ret); + break; + case 'hash': + convertHash(obj, ret); + break; + case 'string': + convertString(obj, ret); + break; + case 'null': + ret.push('null'); + break; + case 'number': + ret.push(obj.toString()); + break; + case 'boolean': + ret.push(obj ? 'true' : 'false'); + break; + } + } + + function convertArray(obj, ret) { + for (var i=0; i { + this.awesomeThings = response.data; + }); + } + + addThing() { + if(this.newThing) { + this.$http.post('/api/things', { + name: this.newThing + }); + this.newThing = ''; + } + } + + deleteThing(thing) { + this.$http.delete(`/api/things/${thing._id}`); + } +} + +export default angular.module('app2App.main', [uiRouter]) + .config(routing) + .component('main', { + template: require('./main.html'), + controller: MainController + }) + .name; diff --git a/client/app/main/main.component.spec.js b/client/app/main/main.component.spec.js new file mode 100644 index 0000000..34f4962 --- /dev/null +++ b/client/app/main/main.component.spec.js @@ -0,0 +1,37 @@ +'use strict'; + +import main from './main.component'; +import { + MainController +} from './main.component'; + +describe('Component: MainComponent', function() { + beforeEach(angular.mock.module(main)); + beforeEach(angular.mock.module('stateMock')); + + var scope; + var mainComponent; + var state; + var $httpBackend; + + // Initialize the controller and a mock scope + beforeEach(inject(function(_$httpBackend_, $http, $componentController, $rootScope, $state) { + $httpBackend = _$httpBackend_; + $httpBackend.expectGET('/api/things') + .respond(['HTML5 Boilerplate', 'AngularJS', 'Karma', 'Express']); + + scope = $rootScope.$new(); + state = $state; + mainComponent = $componentController('main', { + $http, + $scope: scope + }); + })); + + it('should attach a list of things to the controller', function() { + mainComponent.$onInit(); + $httpBackend.flush(); + expect(mainComponent.awesomeThings.length) + .to.equal(4); + }); +}); diff --git a/client/app/main/main.css b/client/app/main/main.css new file mode 100644 index 0000000..959d765 --- /dev/null +++ b/client/app/main/main.css @@ -0,0 +1,27 @@ +.thing-form { + margin: 20px 0; +} + +#banner { + border-bottom: none; + margin-top: -20px; +} + +#banner h1 { + font-size: 60px; + line-height: 1; + letter-spacing: -1px; +} + +.hero-unit { + position: relative; + padding: 30px 15px; + color: #F5F5F5; + text-align: center; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); + background: #2d363a; +} + +.navbar-text { + margin-left: 15px; +} diff --git a/client/app/main/main.html b/client/app/main/main.html new file mode 100644 index 0000000..cd4fb40 --- /dev/null +++ b/client/app/main/main.html @@ -0,0 +1,82 @@ + + +
+
+
+
+ +
+
+

Generate Ansible Playbooks

+
    +
  • Generate Ansible playbooks using UI
  • +
  • Create playbooks, plays and tasks in few clicks
  • +
  • Lists all available ansible modules
  • +
  • Provides description and examples of modules
  • +
  • Provides an interface to input parameters for each module
  • +
+
+
+
+
+

Import input

+

Import inputs into ansible inventory file format.

+
+
+ +
+ +
+
+
+ +
+
+

Play!

+
    +
  • Run your playbooks and view results
  • +
  • Run playbooks at a play or task level
  • +
+
+ +
+
+ +
+ +
+
+
+
+

About us

+
+
+
+

Mumshad Mannambeth

+ +
+
+
+
This is a proof-of-concept implementation. This is only to be used for testing purpose and NOT to be used in production.
+
+
+
+
+ +
+
+
+

Also check out

+
+ + +
+
+
+
diff --git a/client/app/main/main.routes.js b/client/app/main/main.routes.js new file mode 100644 index 0000000..280e75b --- /dev/null +++ b/client/app/main/main.routes.js @@ -0,0 +1,10 @@ +'use strict'; + +export default function routes($stateProvider) { + 'ngInject'; + + $stateProvider.state('main', { + url: '/', + template: '
' + }); +} diff --git a/client/app/modals/complex_var_modal/complexVariable.html b/client/app/modals/complex_var_modal/complexVariable.html new file mode 100644 index 0000000..c940c81 --- /dev/null +++ b/client/app/modals/complex_var_modal/complexVariable.html @@ -0,0 +1,19 @@ + + diff --git a/client/app/modals/complex_var_modal/complex_var_modal.controller.js b/client/app/modals/complex_var_modal/complex_var_modal.controller.js new file mode 100644 index 0000000..f58d35c --- /dev/null +++ b/client/app/modals/complex_var_modal/complex_var_modal.controller.js @@ -0,0 +1,21 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function complexVarModalController($scope,$uibModalInstance,hostvars,members) { + + $scope.members = members || {}; + $scope.hostvars = hostvars; + + $scope.ok = function () { + $uibModalInstance.close($scope.members); + }; + + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; +} + +export default angular.module('webAppApp.complex_var_modal', []) + .controller('ComplexVarModalController', complexVarModalController) + .name; diff --git a/client/app/modals/complex_var_modal/complex_var_modal.controller.spec.js b/client/app/modals/complex_var_modal/complex_var_modal.controller.spec.js new file mode 100644 index 0000000..4839e0b --- /dev/null +++ b/client/app/modals/complex_var_modal/complex_var_modal.controller.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Controller: ComplexVarModalCtrl', function() { + // load the controller's module + beforeEach(module('webAppApp.complex_var_modal')); + + var ComplexVarModalCtrl; + + // Initialize the controller and a mock scope + beforeEach(inject(function($controller) { + ComplexVarModalCtrl = $controller('ComplexVarModalCtrl', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/project/project.component.js b/client/app/project/project.component.js new file mode 100644 index 0000000..e00d25e --- /dev/null +++ b/client/app/project/project.component.js @@ -0,0 +1,121 @@ +'use strict'; +const angular = require('angular'); + +const uiRouter = require('angular-ui-router'); + +import routes from './project.routes'; + +export class ProjectComponent { + /*@ngInject*/ + constructor($scope, Projects) { + 'ngInject'; + var default_project_folder = '/opt/ehc-ansible-projects/'; + + $scope.blankProject = { + name: '', + ansibleEngine: { + ansibleHost: '', + ansibleHostUser: '', + ansibleHostPassword: '', + projectFolder: '', + customModules: '' + + } + }; + + $scope.msg = ""; + $scope.msg_status = ""; + + $scope.newProject = $scope.blankProject; + + $scope.editProjectFlag = false; + + $scope.saveButtonIcon = "fa-save"; + + $scope.showProjectForm = function(){ + $scope.showCreateProject = true; + $scope.editProjectFlag = false; + $scope.newProject = $scope.blankProject; + $scope.msg = ""; + $scope.msg_status = ""; + }; + + + $scope.hideProjectForm = function(){ + $scope.showCreateProject = false; + $scope.editProjectFlag = false; + $scope.newProject = $scope.blankProject; + $scope.msg = ""; + $scope.msg_status = ""; + }; + + $scope.getProjects = function(){ + console.log("Getting Projects"); + $scope.projects = Projects.resource.query(function(){ + console.log($scope.projects); + }) + + }; + + $scope.getProjects(); + + $scope.deleteProject = function(project){ + project.$remove(function(){ + $scope.getProjects(); + }) + }; + + $scope.editProject = function(project){ + $scope.showCreateProject = true; + $scope.editProjectFlag = true; + $scope.newProject = project; + }; + + $scope.createProject = function(){ + $scope.newProject.creationTime = new Date(); + //$scope.newProject.ansibleEngine.projectFolder = default_project_folder + '/' + $scope.newProject.name + //$scope.newProject.ansibleEngine.customModules = $scope.newProject.ansibleEngine.projectFolder + '/library' + + var projectSavedCallback = function(){ + $scope.showCreateProject = false; + $scope.msg = "Project Saved Successfully"; + $scope.msg_status = "success"; + $scope.getProjects(); + $scope.saveButtonIcon = 'fa-save'; + }; + + var projectSaveFailedCallback = function(errResponse){ + $scope.msg = errResponse.data; + $scope.msg_status = "danger"; + $scope.saveButtonIcon = 'fa-save'; + }; + + $scope.saveButtonIcon = 'fa-spinner fa-spin'; + + if($scope.editProjectFlag){ + $scope.editProjectFlag = false; + $scope.newProject.$update(projectSavedCallback,projectSaveFailedCallback); + }else{ + Projects.resource.save($scope.newProject,projectSavedCallback,projectSaveFailedCallback); + } + + + }; + + $scope.$watch('newProject.name', function(newValue, oldValue){ + console.log("Changed"); + $scope.newProject.ansibleEngine.projectFolder = '/opt/ansible-projects/' + newValue; + $scope.newProject.ansibleEngine.customModules = '/opt/ansible-projects/' + newValue + '/library'; + }); + + } +} + +export default angular.module('webAppApp.project', [uiRouter]) + .config(routes) + .component('project', { + template: require('./project.html'), + controller: ProjectComponent, + controllerAs: 'projectCtrl' + }) + .name; diff --git a/client/app/project/project.component.spec.js b/client/app/project/project.component.spec.js new file mode 100644 index 0000000..12db8ac --- /dev/null +++ b/client/app/project/project.component.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Component: ProjectComponent', function() { + // load the controller's module + beforeEach(module('webAppApp.project')); + + var ProjectComponent; + + // Initialize the controller and a mock scope + beforeEach(inject(function($componentController) { + ProjectComponent = $componentController('project', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/project/project.css b/client/app/project/project.css new file mode 100644 index 0000000..73b4fa8 --- /dev/null +++ b/client/app/project/project.css @@ -0,0 +1,5 @@ +.hint { + /* Copy styles from ng-messages */ + font-size: 12px; + /* Set our own color */ + color: grey; } diff --git a/client/app/project/project.html b/client/app/project/project.html new file mode 100644 index 0000000..d35d6ef --- /dev/null +++ b/client/app/project/project.html @@ -0,0 +1,80 @@ +
+
+
+ + + +
+

+ + +

+ +

+ + +

Ansible Controller system - A linux system with Ansible Installed on it. Required if you want to execute Ansible playbooks. You could skip this and still generate playbooks but not test them.
+

+ +

+ + +

+ +

+ + +

+ +

+ + +

A directory path to store files of this project
+

+ +

+ + +

A directory path to store custom modules for this project
+

+ + + + +
+ +
{{msg}}
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + +
NameAnsible HostAnsible VersionTimeActions
{{project.name}}{{project.ansibleEngine.ansibleHost}}{{project.ansibleVersion}}{{project.creationTime | date : 'medium'}} +
+ + +
+
+
+
diff --git a/client/app/project/project.routes.js b/client/app/project/project.routes.js new file mode 100644 index 0000000..9702d4f --- /dev/null +++ b/client/app/project/project.routes.js @@ -0,0 +1,10 @@ +'use strict'; + +export default function($stateProvider) { + 'ngInject'; + $stateProvider + .state('project', { + url: '/project', + template: '' + }); +} diff --git a/client/app/providers/ansi2html/ansi2html.service.js b/client/app/providers/ansi2html/ansi2html.service.js new file mode 100644 index 0000000..4a3df36 --- /dev/null +++ b/client/app/providers/ansi2html/ansi2html.service.js @@ -0,0 +1,32 @@ +'use strict'; +const angular = require('angular'); + +var ansi_to_html = require('ansi-to-html'); + +/*@ngInject*/ +export function ansi2htmlProvider() { + // Private variables + var salutation = 'Hello'; + + // Private constructor + function Greeter() { + this.greet = function() { + return salutation; + }; + } + + // Public API for configuration + this.setSalutation = function(s) { + salutation = s; + }; + + // Method for instantiating + this.$get = ['$window', function ($window) { + // configure JSONEditor using provider's configuration + return new ansi_to_html(); + }]; +} + +export default angular.module('webAppApp.ansi2html', []) + .provider('ansi2html', ansi2htmlProvider) + .name; diff --git a/client/app/providers/ansi2html/ansi2html.service.spec.js b/client/app/providers/ansi2html/ansi2html.service.spec.js new file mode 100644 index 0000000..2025d77 --- /dev/null +++ b/client/app/providers/ansi2html/ansi2html.service.spec.js @@ -0,0 +1,16 @@ +'use strict'; + +describe('Service: ansi2html', function() { + // load the service's module + beforeEach(module('webAppApp.ansi2html')); + + // instantiate service + var ansi2html; + beforeEach(inject(function(_ansi2html_) { + ansi2html = _ansi2html_; + })); + + it('should do something', function() { + expect(!!ansi2html).to.be.true; + }); +}); diff --git a/client/app/providers/yaml/yaml.service.js b/client/app/providers/yaml/yaml.service.js new file mode 100644 index 0000000..c363515 --- /dev/null +++ b/client/app/providers/yaml/yaml.service.js @@ -0,0 +1,30 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function yamlProvider() { + // Private variables + var salutation = 'Hello'; + + // Private constructor + function Greeter() { + this.greet = function() { + return salutation; + }; + } + + // Public API for configuration + this.setSalutation = function(s) { + salutation = s; + }; + + // Method for instantiating + this.$get = ['$window', function ($window) { + // configure JSONEditor using provider's configuration + return $window.YAML; + }]; +} + +export default angular.module('webAppApp.yaml', []) + .provider('YAML', yamlProvider) + .name; diff --git a/client/app/providers/yaml/yaml.service.spec.js b/client/app/providers/yaml/yaml.service.spec.js new file mode 100644 index 0000000..0ce3fc1 --- /dev/null +++ b/client/app/providers/yaml/yaml.service.spec.js @@ -0,0 +1,16 @@ +'use strict'; + +describe('Service: yaml', function() { + // load the service's module + beforeEach(module('webAppApp.yaml')); + + // instantiate service + var yaml; + beforeEach(inject(function(_yaml_) { + yaml = _yaml_; + })); + + it('should do something', function() { + expect(!!yaml).to.be.true; + }); +}); diff --git a/client/app/runs/runs.component.js b/client/app/runs/runs.component.js new file mode 100644 index 0000000..ac33ccb --- /dev/null +++ b/client/app/runs/runs.component.js @@ -0,0 +1,124 @@ +'use strict'; +const angular = require('angular'); + +const uiRouter = require('angular-ui-router'); + +import routes from './runs.routes'; + +export class RunsComponent { + /*@ngInject*/ + constructor(ansible,$scope,$sce,$uibModal,ansi2html,Projects) { + 'ngInject'; + /** + * Get list of projects from server + */ + $scope.getProjects = function(){ + $scope.projects = Projects.resource.query(function(){ + if($scope.projects.length){ + $scope.selectedProjectID = localStorage.selectedProjectID || $scope.projects[0]._id; + $scope.projectSelected($scope.selectedProjectID) + } + + }) + }; + + $scope.getProjects(); + + /** + * On ProjectSelected - set selectedProjectID in cache + * @param projectID + */ + $scope.projectSelected = function(projectID){ + localStorage.selectedProjectID = projectID; + + $scope.selectedProject = Projects.resource.get({id: projectID},function(){ + Projects.selectedProject = $scope.selectedProject; + $scope.$broadcast('projectLoaded'); + }) + + }; + + $scope.showLogs = function(runData){ + var modalInstance = $uibModal.open({ + animation: false, + templateUrl: 'app/designer/execution/executeModal.html', + controller: 'ExecutionController', + size: 'lg', + backdrop : 'static', + keyboard : false, + closeByEscape : false, + closeByDocument : false, + resolve: { + inventory_file_contents: function () { + return null; + }, + yaml: function () { + return null; + }, + tags: function(){ + return null + }, + selectedProject: function(){ + return null + }, + selectedPlaybook: function(){ + return null + }, + selectedPlay: function(){ + return null + }, + executionType: function(){ + return null + }, + executionName: function(){ + return null + }, + readOnly: function(){ + return true + }, + runData: function(){ + return runData + }, + projectFolder: function () { + return null + }, + roleName: function () { + return null + } + } + }); + } + + $scope.executeAnsiblePlayBook = function(tags,executionType,executionName,selectedPlay){ + + }; + + $scope.getLogs = function(){ + ansible.getLogs($scope.executionData,function(successResponse) { + $scope.result = $sce.trustAsHtml(ansi2html.toHtml(successResponse.data).replace(/\n/g, "
")); + + if(successResponse.data.indexOf('SCRIPT_FINISHED') > -1){ + $scope.refreshLog = false; + } + + }); + }; + + ansible.query( + function(response){ + $scope.runs = response.data + }, + function(response){ + + }) + } +} + +export default angular.module('webAppApp.runs', [uiRouter]) + .config(routes) + .component('runs', { + template: require('./runs.html'), + controller: RunsComponent, + controllerAs: 'runsCtrl' + }) + .name; diff --git a/client/app/runs/runs.component.spec.js b/client/app/runs/runs.component.spec.js new file mode 100644 index 0000000..4d6fe48 --- /dev/null +++ b/client/app/runs/runs.component.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +describe('Component: RunsComponent', function() { + // load the controller's module + beforeEach(module('webAppApp.runs')); + + var RunsComponent; + + // Initialize the controller and a mock scope + beforeEach(inject(function($componentController) { + RunsComponent = $componentController('runs', {}); + })); + + it('should ...', function() { + expect(1).to.equal(1); + }); +}); diff --git a/client/app/runs/runs.css b/client/app/runs/runs.css new file mode 100644 index 0000000..e69de29 diff --git a/client/app/runs/runs.html b/client/app/runs/runs.html new file mode 100644 index 0000000..ad39cd0 --- /dev/null +++ b/client/app/runs/runs.html @@ -0,0 +1,44 @@ + +
+

+ + +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HostsTagsVerboseCheck ModeSelected PlaybookTypeNameTimeLogs
{{run.host}}{{run.tags.join(', ')}}{{run.verbose}}{{run.check_mode}}{{run.selectedPlaybook}}{{run.executionType}}{{run.executionName}}{{run.executionTime | date : 'medium'}}
+
+
diff --git a/client/app/runs/runs.routes.js b/client/app/runs/runs.routes.js new file mode 100644 index 0000000..397ccb9 --- /dev/null +++ b/client/app/runs/runs.routes.js @@ -0,0 +1,10 @@ +'use strict'; + +export default function($stateProvider) { + 'ngInject'; + $stateProvider + .state('runs', { + url: '/runs', + template: '' + }); +} diff --git a/client/app/services/ansible/ansible.service.js b/client/app/services/ansible/ansible.service.js new file mode 100644 index 0000000..cfaa237 --- /dev/null +++ b/client/app/services/ansible/ansible.service.js @@ -0,0 +1,302 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function ansibleService($http,YAML,Projects) { + // AngularJS will instantiate a singleton by calling "new" on this function + // AngularJS will instantiate a singleton by calling "new" on this function + + var AnsibleService = this; + + var uri = '/api/ansible/'; + var emptyPlaybookContent = '---\n# Playbook File -'; + var defaultTaskProperties = ["variables","tags","name","notify","with_items","first_available_file","only_if","user","sudo","connection","when","register","ignore_errors","selected","changed_when","delegate_to","vars","poll","async","args","with_together"]; + + + AnsibleService.modules = {}; + + this.getAnsibleModules = function(successCallback,errorCallback,parent,refresh){ + if(!refresh && AnsibleService.modules[Projects.selectedProject.ansibleEngine.ansibleHost]) + return successCallback(AnsibleService.modules[Projects.selectedProject.ansibleEngine.ansibleHost]); + + try{ + if(!refresh && JSON.parse(localStorage['modules_' + Projects.selectedProject.ansibleEngine.ansibleHost])) + return successCallback(JSON.parse(localStorage['modules_' + Projects.selectedProject.ansibleEngine.ansibleHost])); + }catch(e){ + //console.error(e) + } + + $http.post(uri + 'modules',{ansibleEngine:Projects.selectedProject.ansibleEngine}).then(function(response){ + var result = response.data.split('\n'); + AnsibleService.modules[Projects.selectedProject.ansibleEngine.ansibleHost] = result.map(function(item){ + return {"name" : item.split(" ")[0], "description": item.split(/ (.+)?/)[1]} + }); + + localStorage['modules_' + Projects.selectedProject.ansibleEngine.ansibleHost] = JSON.stringify(AnsibleService.modules[Projects.selectedProject.ansibleEngine.ansibleHost]); + + successCallback(AnsibleService.modules[Projects.selectedProject.ansibleEngine.ansibleHost]) + + },errorCallback) + }; + + + this.getAnsibleModuleDescription = function(moduleName, successCallback,errorCallback,refresh){ + + try{ + if(!refresh && JSON.parse(localStorage['module_description_' + Projects.selectedProject.ansibleEngine.ansibleHost + '_' + moduleName])) + return successCallback(JSON.parse(localStorage['module_description_' + Projects.selectedProject.ansibleEngine.ansibleHost + '_' + moduleName])); + }catch(e){ + + } + + var command = 'ansible-doc ' + moduleName; + + if(Projects.selectedProject.ansibleEngine.customModules){ + command = 'export ANSIBLE_LIBRARY="' + Projects.selectedProject.ansibleEngine.customModules + '"; ' + command; + } + + $http.post(uri + 'command',{ansibleEngine:Projects.selectedProject.ansibleEngine,command:command}) + .then(function(response){ + + localStorage['module_description_' + Projects.selectedProject.ansibleEngine.ansibleHost + '_' + moduleName] = JSON.stringify(response.data); + successCallback(response.data) + + },errorCallback) + + }; + + this.executeAnsiblePlayBook = function(body,successCallback,errorCallback,parent){ + $http.post(uri + 'execute',body).then(successCallback,errorCallback) + }; + + this.executeCommand = function(command, successCallback,errorCallback){ + $http.post(uri + 'command',{command:command,ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.getLogs = function(executionData,successCallback,errorCallback){ + $http.get(uri+'logs/'+executionData._id).then(successCallback,errorCallback); + }; + + this.query = function(successCallback,errorCallback){ + $http.get(uri).then(successCallback,errorCallback); + }; + + this.getModuleFromTask = function(task){ + var module = null; + angular.forEach(JSON.parse(angular.toJson(task)), function(value, key) { + if(defaultTaskProperties.indexOf(key) < 0){ + module = key + } + }); + + if(module === 'include' && !task.tags && task.include.indexOf('tags') > -1){ + task.tags = task.include.replace(/.*tags=(.*)/,"$1") + } + + return module; + + }; + // -------------------------- PROJECT ------------------------- + this.getProjectFiles = function(successCallback,errorCallback){ + $http.post(uri + 'project/files',{ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + + // -------------------------- PLAYBOOK ------------------------- + + this.getPlaybookList = function(successCallback,errorCallback){ + $http.post(uri + 'playbook/list',{ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.deletePlaybook = function(playbookName,successCallback,errorCallback){ + + $http.post(uri + 'playbook/delete',{ansibleEngine:Projects.selectedProject.ansibleEngine,playbookName:playbookName}).then(successCallback,errorCallback) + + }; + + this.createPlaybook = function(playbookName,playbookFileContents,successCallback,errorCallback){ + var playbookContent = playbookFileContents || (emptyPlaybookContent + playbookName); + $http.post(uri + 'playbook/create',{ansibleEngine:Projects.selectedProject.ansibleEngine,playbookName:playbookName,playbookFileContents:playbookContent}).then(successCallback,errorCallback) + }; + + this.readPlaybook = function(playbookName,successCallback,errorCallback){ + $http.post(uri + 'playbook/get',{ansibleEngine:Projects.selectedProject.ansibleEngine,playbookName:playbookName}).then(successCallback,errorCallback) + }; + + this.readPlaybookData = function(playbookData){ + return YAML.parse(playbookData) + }; + + // -------------------------- ROLES ------------------------- + + this.getRoleList = function(successCallback,errorCallback){ + $http.post(uri + 'roles/list',{ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.searchRolesGalaxy = function(searchText,successCallback,errorCallback){ + $http.post(uri + 'roles/search/galaxy',{searchText:searchText,ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.searchRolesGithub = function(searchText,successCallback,errorCallback){ + $http.post(uri + 'roles/search/github',{searchText:searchText,ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.createRole = function(roleName,successCallback,errorCallback,selectedRoleName){ + $http.post(uri + 'roles/create',{roleName:roleName,selectedRoleName:selectedRoleName,ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.importRole = function(roleType,roleNameUri,successCallback,errorCallback){ + $http.post(uri + 'roles/import',{roleType:roleType,roleNameUri:roleNameUri,ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.deleteRole = function(roleName,successCallback,errorCallback){ + $http.post(uri + 'roles/delete',{roleName:roleName,ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.getRoleFiles = function(roleName,successCallback,errorCallback){ + $http.post(uri + 'roles/files',{roleName:roleName,ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + // -------------------------- FILES ------------------------- + + this.createFile = function(fileAbsolutePath,successCallback,errorCallback,selectedFileName){ + $http.post(uri + 'files/create',{fileAbsolutePath:fileAbsolutePath,selectedFileName:selectedFileName,ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.updateFile = function(fileAbsolutePath,fileContents,successCallback,errorCallback,selectedFileName){ + $http.post(uri + 'files/update',{fileAbsolutePath:fileAbsolutePath,fileContents:fileContents,selectedFileName:selectedFileName,ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.deleteFile = function(fileAbsolutePath,successCallback,errorCallback,selectedFileName){ + $http.post(uri + 'files/delete',{fileAbsolutePath:fileAbsolutePath,selectedFileName:selectedFileName,ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + // -------------------------- INVENTORY ------------------------- + + this.getInventoryList = function(successCallback,errorCallback, projectFolder){ + // Override project folder for other cases, such as roles + var ansibleEngine = Projects.selectedProject.ansibleEngine; + if(projectFolder){ + ansibleEngine = angular.copy(Projects.selectedProject.ansibleEngine); + ansibleEngine.projectFolder = projectFolder + } + $http.post(uri + 'inventory/list',{ansibleEngine:ansibleEngine}).then(successCallback,errorCallback) + }; + + this.readInventory = function(inventoryName,successCallback,errorCallback){ + $http.post(uri + 'inventory/get',{inventoryName:inventoryName,ansibleEngine:Projects.selectedProject.ansibleEngine}).then(successCallback,errorCallback) + }; + + this.deleteInventory = function(inventoryName,successCallback,errorCallback){ + $http.post(uri + 'inventory/delete',{ansibleEngine:Projects.selectedProject.ansibleEngine,inventoryName:inventoryName}).then(successCallback,errorCallback) + }; + + this.createInventory = function(inventoryName,inventoryFileContents,successCallback,errorCallback){ + $http.post(uri + 'inventory/create',{ansibleEngine:Projects.selectedProject.ansibleEngine,inventoryName:inventoryName,inventoryFileContents:inventoryFileContents}).then(successCallback,errorCallback) + }; + + // -------------------------- Variable Files ------------------------- + + this.getVars = function(inventoryFileName, hostName, successCallback,errorCallback){ + $http.post(uri + 'vars/hosts/get',{ansibleEngine:Projects.selectedProject.ansibleEngine,hostName:hostName,inventoryFileName:inventoryFileName}).then(successCallback,errorCallback) + }; + + this.getRoleVars = function(roleName, successCallback,errorCallback){ + $http.post(uri + 'vars/roles/get',{ansibleEngine:Projects.selectedProject.ansibleEngine,roleName:roleName}).then(successCallback,errorCallback) + }; + + this.updateGroupVarsFile = function(groupName,groupVarsContents,successCallback,errorCallback){ + $http.post(uri + 'vars_file/groups/update',{ansibleEngine:Projects.selectedProject.ansibleEngine,groupName:groupName,groupVarsContents:groupVarsContents}).then(successCallback,errorCallback) + }; + + this.getGroupVarsFile = function(groupName,successCallback,errorCallback){ + $http.post(uri + 'vars_file/groups/get',{ansibleEngine:Projects.selectedProject.ansibleEngine,groupName:groupName}).then(successCallback,errorCallback) + }; + + this.updateHostVarsFile = function(hostName,hostVarsContents,successCallback,errorCallback){ + $http.post(uri + 'vars_file/hosts/update',{ansibleEngine:Projects.selectedProject.ansibleEngine,hostName:hostName,hostVarsContents:hostVarsContents}).then(successCallback,errorCallback) + }; + + this.getHostVarsFile = function(hostName,successCallback,errorCallback){ + $http.post(uri + 'vars_file/hosts/get',{ansibleEngine:Projects.selectedProject.ansibleEngine,hostName:hostName}).then(successCallback,errorCallback) + }; + + // ------------------- TAGS LIST -------------------------- + + this.getTagList = function(selectedPlaybook,inventory_file_name,ansibleEngine,successCallback,errorCallback){ + $http.post(uri + 'tags/list',{ansibleEngine:ansibleEngine,inventory_file_name:inventory_file_name,selectedPlaybook:selectedPlaybook}).then(successCallback,errorCallback) + }; + + // ------------- SOME HELPER FUNCTIONS -------------- + + this.parseINIString = function(data){ + var regex = { + section: /^\s*\[\s*([^\]]*)\s*\]\s*$/, + param: /^\s*([\w\.\-\_]+).*$/, + comment: /^\s*;.*$/ + }; + + var hosts = []; + var groups = []; + var lines = data.split(/\r\n|\r|\n/); + var group = null; + + group = {'name':'Un grouped', 'members': [], 'type': 'default'}; + groups.push(group); + + // groups.push({'name':'All Hosts', 'members': hosts, 'type': 'default'}); + + lines.forEach(function(line){ + if(regex.comment.test(line)){ + return; + }else if(regex.param.test(line)){ + var match = line.match(regex.param); + var host = match[1]; + if(hosts.indexOf(host) < 0){ + hosts.push(host); + } + + if(group && group.members.indexOf(host) < 0){ + group.members.push(host); + } + + }else if(regex.section.test(line)){ + var match = line.match(regex.section); + group = {'name':match[1], 'members': [], 'type': 'userdefined'}; + groups.push(group); + } + }); + return {'hosts':hosts,'groups':groups}; + }; + + + this.jsonToAnsibleInventoryIni = function(inventoryData){ + + var name = inventoryData.name; + var hosts = inventoryData.hosts; + var groups = inventoryData.groups; + + var result_lines = ['# Inventory File - ' + name, '']; + + angular.forEach(groups,function(group){ + if(group.name == 'All Hosts')return; + + if(group.name !== 'Un grouped'){ + result_lines.push(''); + result_lines.push('[' + group.name + ']'); + } + + angular.forEach(group.members,function(member){ + result_lines.push(member); + }) + + }); + + return result_lines.join('\n') + + } +} + +export default angular.module('webAppApp.ansible', []) + .service('ansible', ansibleService) + .name; diff --git a/client/app/services/ansible/ansible.service.spec.js b/client/app/services/ansible/ansible.service.spec.js new file mode 100644 index 0000000..331dd39 --- /dev/null +++ b/client/app/services/ansible/ansible.service.spec.js @@ -0,0 +1,16 @@ +'use strict'; + +describe('Service: ansible', function() { + // load the service's module + beforeEach(module('webAppApp.ansible')); + + // instantiate service + var ansible; + beforeEach(inject(function(_ansible_) { + ansible = _ansible_; + })); + + it('should do something', function() { + expect(!!ansible).to.be.true; + }); +}); diff --git a/client/app/services/editor/editor.service.js b/client/app/services/editor/editor.service.js new file mode 100644 index 0000000..39bd220 --- /dev/null +++ b/client/app/services/editor/editor.service.js @@ -0,0 +1,48 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function editorService() { + // Service logic + // ... + + var ui_ace_doctype_map = { + '': 'ini', + 'txt': 'text', + 'text': 'text', + 'yml': 'yaml', + 'yaml': 'yaml', + 'json': 'json', + 'md': 'markdown', + 'html': 'html', + 'py': 'python', + 'j2': 'ini' + }; + + var setContentAndType = function (data, file, selectedFile) { + if (typeof data == 'object') { + selectedFile.content = JSON.stringify(data, null, '\t'); + } else { + selectedFile.content = data; + } + + selectedFile.docType = ui_ace_doctype_map[file.extension.replace('.', '')]; + selectedFile.showSource = true; + + if (selectedFile.docType == 'markdown') { + selectedFile.markdownContent = selectedFile.content; + selectedFile.showSource = false; + } + }; + + // Public API here + return { + ui_ace_doctype_map: ui_ace_doctype_map, + setContentAndType: setContentAndType + }; +} + + +export default angular.module('webAppApp.editor', []) + .factory('editor', editorService) + .name; diff --git a/client/app/services/editor/editor.service.spec.js b/client/app/services/editor/editor.service.spec.js new file mode 100644 index 0000000..d63e286 --- /dev/null +++ b/client/app/services/editor/editor.service.spec.js @@ -0,0 +1,16 @@ +'use strict'; + +describe('Service: editor', function() { + // load the service's module + beforeEach(module('webAppApp.editor')); + + // instantiate service + var editor; + beforeEach(inject(function(_editor_) { + editor = _editor_; + })); + + it('should do something', function() { + expect(!!editor).to.be.true; + }); +}); diff --git a/client/app/services/projects/projects.service.js b/client/app/services/projects/projects.service.js new file mode 100644 index 0000000..6d8b82a --- /dev/null +++ b/client/app/services/projects/projects.service.js @@ -0,0 +1,20 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function projectsService($resource) { + // AngularJS will instantiate a singleton by calling "new" on this function + this.resource = $resource('/api/projects/:id', { + id: '@_id' + },{ + update: { + method: 'PUT' // this method issues a PUT request + }}); + + this.selectedProject = null; + this.selectedInventoryFileName = null +} + +export default angular.module('webAppApp.projects', []) + .service('Projects', projectsService) + .name; diff --git a/client/app/services/projects/projects.service.spec.js b/client/app/services/projects/projects.service.spec.js new file mode 100644 index 0000000..77f3e59 --- /dev/null +++ b/client/app/services/projects/projects.service.spec.js @@ -0,0 +1,16 @@ +'use strict'; + +describe('Service: projects', function() { + // load the service's module + beforeEach(module('webAppApp.projects')); + + // instantiate service + var projects; + beforeEach(inject(function(_projects_) { + projects = _projects_; + })); + + it('should do something', function() { + expect(!!projects).to.be.true; + }); +}); diff --git a/client/app/services/yaml/yaml.service.js b/client/app/services/yaml/yaml.service.js new file mode 100644 index 0000000..c204389 --- /dev/null +++ b/client/app/services/yaml/yaml.service.js @@ -0,0 +1,19 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function yamlService() { + // AngularJS will instantiate a singleton by calling "new" on this function + + this.$get = ['$window', function ($window) { + // configure JSONEditor using provider's configuration + console.log("Window"); + + return $window.YAML; + }]; + +} + +export default angular.module('webAppApp.yaml', []) + .factory('YAML', yamlService) + .name; diff --git a/client/app/services/yaml/yaml.service.spec.js b/client/app/services/yaml/yaml.service.spec.js new file mode 100644 index 0000000..0ce3fc1 --- /dev/null +++ b/client/app/services/yaml/yaml.service.spec.js @@ -0,0 +1,16 @@ +'use strict'; + +describe('Service: yaml', function() { + // load the service's module + beforeEach(module('webAppApp.yaml')); + + // instantiate service + var yaml; + beforeEach(inject(function(_yaml_) { + yaml = _yaml_; + })); + + it('should do something', function() { + expect(!!yaml).to.be.true; + }); +}); diff --git a/client/app/services/yamlFile/yamlFile.service.js b/client/app/services/yamlFile/yamlFile.service.js new file mode 100644 index 0000000..6dbc2d0 --- /dev/null +++ b/client/app/services/yamlFile/yamlFile.service.js @@ -0,0 +1,24 @@ +'use strict'; +const angular = require('angular'); + +/*@ngInject*/ +export function yamlFileService(YAML) { + // AngularJS will instantiate a singleton by calling "new" on this function + this.jsonToYamlFile = function(jsonData, fileDescription){ + + var yamlFilePrefix = ''; + + yamlFilePrefix += '---\n'; + + if(fileDescription) + yamlFilePrefix += '# ' + fileDescription + '\n'; + + var yamlData = yamlFilePrefix + YAML.stringify(JSON.parse(angular.toJson(jsonData)),100); + + return yamlData + } +} + +export default angular.module('webAppApp.yamlFile', []) + .service('yamlFile', yamlFileService) + .name; diff --git a/client/app/services/yamlFile/yamlFile.service.spec.js b/client/app/services/yamlFile/yamlFile.service.spec.js new file mode 100644 index 0000000..120f110 --- /dev/null +++ b/client/app/services/yamlFile/yamlFile.service.spec.js @@ -0,0 +1,16 @@ +'use strict'; + +describe('Service: yamlFile', function() { + // load the service's module + beforeEach(module('webAppApp.yamlFile')); + + // instantiate service + var yamlFile; + beforeEach(inject(function(_yamlFile_) { + yamlFile = _yamlFile_; + })); + + it('should do something', function() { + expect(!!yamlFile).to.be.true; + }); +}); diff --git a/client/assets/fonts/Arual.ttf b/client/assets/fonts/Arual.ttf new file mode 100644 index 0000000..14f2650 Binary files /dev/null and b/client/assets/fonts/Arual.ttf differ diff --git a/client/assets/fonts/ExpletusSans-Regular.ttf b/client/assets/fonts/ExpletusSans-Regular.ttf new file mode 100644 index 0000000..00191c8 Binary files /dev/null and b/client/assets/fonts/ExpletusSans-Regular.ttf differ diff --git a/client/assets/fonts/PUDDLE.otf b/client/assets/fonts/PUDDLE.otf new file mode 100644 index 0000000..7679502 Binary files /dev/null and b/client/assets/fonts/PUDDLE.otf differ diff --git a/client/assets/fonts/PUDDLE.ttf b/client/assets/fonts/PUDDLE.ttf new file mode 100644 index 0000000..1a53b49 Binary files /dev/null and b/client/assets/fonts/PUDDLE.ttf differ diff --git a/client/assets/images/Button-1-play-icon.png b/client/assets/images/Button-1-play-icon.png new file mode 100644 index 0000000..185b122 Binary files /dev/null and b/client/assets/images/Button-1-play-icon.png differ diff --git a/client/assets/images/FluSchedule.ics b/client/assets/images/FluSchedule.ics new file mode 100644 index 0000000..1a39a68 --- /dev/null +++ b/client/assets/images/FluSchedule.ics @@ -0,0 +1,22 @@ +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REQUEST +PRODID:-//ddaysoftware.com//NONSGML DDay.iCal 1.0//EN +BEGIN:VEVENT +DESCRIPTION:You have signed up to receive one or more services on Thursday\ + , August 25 2016 at 11:00 AM\n\nWellness Screenings +DTEND:20160825T110000 +DTSTAMP:20160812T142534 +DTSTART:20160825T110000 +LOCATION:New York City\, NY - 2 Penn Plaza\, 17th & 18th Floor +ORGANIZER;CN=Passport Health:mailto:clinics@passporthealthusa.com +SEQUENCE:0 +SUMMARY:Passport Health Reminder +UID:1b13ebdf-8ea1-4207-82f4-4f8bec20de7b +BEGIN:VALARM +ACTION:Display +DESCRIPTION:Passport Health Reminder +TRIGGER:PT30M +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/client/assets/images/ansible_icon.png b/client/assets/images/ansible_icon.png new file mode 100644 index 0000000..07cd505 Binary files /dev/null and b/client/assets/images/ansible_icon.png differ diff --git a/client/assets/images/browser-google-chrome-icon.png b/client/assets/images/browser-google-chrome-icon.png new file mode 100644 index 0000000..5ae1d4a Binary files /dev/null and b/client/assets/images/browser-google-chrome-icon.png differ diff --git a/client/assets/images/ehc-ozone.png b/client/assets/images/ehc-ozone.png new file mode 100644 index 0000000..6c1ed81 Binary files /dev/null and b/client/assets/images/ehc-ozone.png differ diff --git a/client/assets/images/emcacademy.png b/client/assets/images/emcacademy.png new file mode 100644 index 0000000..2039957 Binary files /dev/null and b/client/assets/images/emcacademy.png differ diff --git a/client/assets/images/inside-emc-black.png b/client/assets/images/inside-emc-black.png new file mode 100644 index 0000000..8c65bb1 Binary files /dev/null and b/client/assets/images/inside-emc-black.png differ diff --git a/client/assets/images/inside-emc.png b/client/assets/images/inside-emc.png new file mode 100644 index 0000000..78f97fc Binary files /dev/null and b/client/assets/images/inside-emc.png differ diff --git a/client/assets/images/inventory.png b/client/assets/images/inventory.png new file mode 100644 index 0000000..bf0c1ee Binary files /dev/null and b/client/assets/images/inventory.png differ diff --git a/client/assets/images/migrato.png b/client/assets/images/migrato.png new file mode 100644 index 0000000..0d83dde Binary files /dev/null and b/client/assets/images/migrato.png differ diff --git a/client/assets/images/play.png b/client/assets/images/play.png new file mode 100644 index 0000000..bce6932 Binary files /dev/null and b/client/assets/images/play.png differ diff --git a/client/assets/images/playbook.png b/client/assets/images/playbook.png new file mode 100644 index 0000000..81c5570 Binary files /dev/null and b/client/assets/images/playbook.png differ diff --git a/client/assets/images/yeoman.png b/client/assets/images/yeoman.png new file mode 100644 index 0000000..7d0a1ac Binary files /dev/null and b/client/assets/images/yeoman.png differ diff --git a/client/components/auth/auth.module.js b/client/components/auth/auth.module.js new file mode 100644 index 0000000..c7d4fb0 --- /dev/null +++ b/client/components/auth/auth.module.js @@ -0,0 +1,34 @@ +'use strict'; + +import angular from 'angular'; +import constants from '../../app/app.constants'; +import util from '../util/util.module'; +import ngCookies from 'angular-cookies'; +import { + authInterceptor +} from './interceptor.service'; +import { + routerDecorator +} from './router.decorator'; +import { + AuthService +} from './auth.service'; +import { + UserResource +} from './user.service'; + +import uiRouter from 'angular-ui-router'; + +function addInterceptor($httpProvider) { + 'ngInject'; + + $httpProvider.interceptors.push('authInterceptor'); +} + +export default angular.module('app2App.auth', [constants, util, ngCookies, uiRouter]) + .factory('authInterceptor', authInterceptor) + .run(routerDecorator) + .factory('Auth', AuthService) + .factory('User', UserResource) + .config(['$httpProvider', addInterceptor]) + .name; diff --git a/client/components/auth/auth.service.js b/client/components/auth/auth.service.js new file mode 100644 index 0000000..a5fd3fa --- /dev/null +++ b/client/components/auth/auth.service.js @@ -0,0 +1,226 @@ +'use strict'; + +import * as _ from 'lodash'; + + +class _User { + _id = ''; + name = ''; + email = ''; + role = ''; + $promise = undefined; +} + +export function AuthService($location, $http, $cookies, $q, appConfig, Util, User) { + 'ngInject'; + + var safeCb = Util.safeCb; + var currentUser = new _User(); + var userRoles = appConfig.userRoles || []; + /** + * Check if userRole is >= role + * @param {String} userRole - role of current user + * @param {String} role - role to check against + */ + var hasRole = function(userRole, role) { + return userRoles.indexOf(userRole) >= userRoles.indexOf(role); + }; + + if($cookies.get('token') && $location.path() !== '/logout') { + currentUser = User.get(); + } + + var Auth = { + /** + * Authenticate user and save token + * + * @param {Object} user - login info + * @param {Function} callback - function(error, user) + * @return {Promise} + */ + login({ + email, + password + }, callback) { + return $http.post('/auth/local', { + email, + password + }) + .then(res => { + $cookies.put('token', res.data.token); + currentUser = User.get(); + return currentUser.$promise; + }) + .then(user => { + safeCb(callback)(null, user); + return user; + }) + .catch(err => { + Auth.logout(); + safeCb(callback)(err.data); + return $q.reject(err.data); + }); + }, + + /** + * Delete access token and user info + */ + logout() { + $cookies.remove('token'); + currentUser = new _User(); + }, + + /** + * Create a new user + * + * @param {Object} user - user info + * @param {Function} callback - function(error, user) + * @return {Promise} + */ + createUser(user, callback) { + return User.save(user, function(data) { + $cookies.put('token', data.token); + currentUser = User.get(); + return safeCb(callback)(null, user); + }, function(err) { + Auth.logout(); + return safeCb(callback)(err); + }) + .$promise; + }, + + /** + * Change password + * + * @param {String} oldPassword + * @param {String} newPassword + * @param {Function} callback - function(error, user) + * @return {Promise} + */ + changePassword(oldPassword, newPassword, callback) { + return User.changePassword({ + id: currentUser._id + }, { + oldPassword, + newPassword + }, function() { + return safeCb(callback)(null); + }, function(err) { + return safeCb(callback)(err); + }) + .$promise; + }, + + /** + * Gets all available info on a user + * + * @param {Function} [callback] - function(user) + * @return {Promise} + */ + getCurrentUser(callback) { + var value = _.get(currentUser, '$promise') ? currentUser.$promise : currentUser; + + return $q.when(value) + .then(user => { + safeCb(callback)(user); + return user; + }, () => { + safeCb(callback)({}); + return {}; + }); + }, + + /** + * Gets all available info on a user + * + * @return {Object} + */ + getCurrentUserSync() { + return currentUser; + }, + + /** + * Check if a user is logged in + * + * @param {Function} [callback] - function(is) + * @return {Promise} + */ + isLoggedIn(callback) { + return Auth.getCurrentUser(undefined) + .then(user => { + let is = _.get(user, 'role'); + + safeCb(callback)(is); + return is; + }); + }, + + /** + * Check if a user is logged in + * + * @return {Bool} + */ + isLoggedInSync() { + return !!_.get(currentUser, 'role'); + }, + + /** + * Check if a user has a specified role or higher + * + * @param {String} role - the role to check against + * @param {Function} [callback] - function(has) + * @return {Promise} + */ + hasRole(role, callback) { + return Auth.getCurrentUser(undefined) + .then(user => { + let has = hasRole(_.get(user, 'role'), role); + + safeCb(callback)(has); + return has; + }); + }, + + /** + * Check if a user has a specified role or higher + * + * @param {String} role - the role to check against + * @return {Bool} + */ + hasRoleSync(role) { + return hasRole(_.get(currentUser, 'role'), role); + }, + + /** + * Check if a user is an admin + * (synchronous|asynchronous) + * + * @param {Function|*} callback - optional, function(is) + * @return {Bool|Promise} + */ + isAdmin(...args) { + return Auth.hasRole(Reflect.apply([].concat, ['admin'], args)); + }, + + /** + * Check if a user is an admin + * + * @return {Bool} + */ + isAdminSync() { + // eslint-disable-next-line no-sync + return Auth.hasRoleSync('admin'); + }, + + /** + * Get auth token + * + * @return {String} - a token string used for authenticating + */ + getToken() { + return $cookies.get('token'); + } + }; + + return Auth; +} diff --git a/client/components/auth/interceptor.service.js b/client/components/auth/interceptor.service.js new file mode 100644 index 0000000..a04e7ff --- /dev/null +++ b/client/components/auth/interceptor.service.js @@ -0,0 +1,28 @@ +'use strict'; + +export function authInterceptor($rootScope, $q, $cookies, $injector, Util) { + 'ngInject'; + + var state; + return { + // Add authorization token to headers + request(config) { + config.headers = config.headers || {}; + if($cookies.get('token') && Util.isSameOrigin(config.url)) { + config.headers.Authorization = `Bearer ${$cookies.get('token')}`; + } + return config; + }, + + // Intercept 401s and redirect you to login + responseError(response) { + if(response.status === 401) { + (state || (state = $injector.get('$state'))) + .go('login'); + // remove any stale tokens + $cookies.remove('token'); + } + return $q.reject(response); + } + }; +} diff --git a/client/components/auth/router.decorator.js b/client/components/auth/router.decorator.js new file mode 100644 index 0000000..4e23192 --- /dev/null +++ b/client/components/auth/router.decorator.js @@ -0,0 +1,38 @@ +'use strict'; + +export function routerDecorator($rootScope, $state, Auth) { + 'ngInject'; + // Redirect to login if route requires auth and the user is not logged in, or doesn't have required role + + $rootScope.$on('$stateChangeStart', function(event, next) { + if(!next.authenticate) { + return; + } + + if(typeof next.authenticate === 'string') { + Auth.hasRole(next.authenticate) + .then(has => { + if(has) { + return; + } + + event.preventDefault(); + return Auth.isLoggedIn() + .then(is => { + $state.go(is ? 'main' : 'login'); + }); + }); + } else { + Auth.isLoggedIn() + .then(is => { + if(is) { + return; + } + + event.preventDefault(); + + $state.go('login'); + }); + } + }); +} diff --git a/client/components/auth/user.service.js b/client/components/auth/user.service.js new file mode 100644 index 0000000..4204298 --- /dev/null +++ b/client/components/auth/user.service.js @@ -0,0 +1,22 @@ +'use strict'; + +export function UserResource($resource) { + 'ngInject'; + + return $resource('/api/users/:id/:controller', { + id: '@_id' + }, { + changePassword: { + method: 'PUT', + params: { + controller: 'password' + } + }, + get: { + method: 'GET', + params: { + id: 'me' + } + } + }); +} diff --git a/client/components/footer/footer.component.js b/client/components/footer/footer.component.js new file mode 100644 index 0000000..64bf5a1 --- /dev/null +++ b/client/components/footer/footer.component.js @@ -0,0 +1,10 @@ +import angular from 'angular'; + +export class FooterComponent {} + +export default angular.module('directives.footer', []) + .component('footer', { + template: require('./footer.html'), + controller: FooterComponent + }) + .name; diff --git a/client/components/footer/footer.css b/client/components/footer/footer.css new file mode 100644 index 0000000..cd1753e --- /dev/null +++ b/client/components/footer/footer.css @@ -0,0 +1,6 @@ +footer.footer { + text-align: center; + padding: 30px 0; + margin-top: 70px; + border-top: 1px solid #E5E5E5; +} diff --git a/client/components/footer/footer.html b/client/components/footer/footer.html new file mode 100644 index 0000000..ab3b060 --- /dev/null +++ b/client/components/footer/footer.html @@ -0,0 +1,8 @@ +
+

Angular Fullstack v4.2.2 | + Documentation | + Chat on Gitter | + Issues | + Donate to our Open Collective +

+
diff --git a/client/components/modal/modal.css b/client/components/modal/modal.css new file mode 100644 index 0000000..ae04068 --- /dev/null +++ b/client/components/modal/modal.css @@ -0,0 +1,23 @@ +.modal-primary .modal-header, +.modal-info .modal-header, +.modal-success .modal-header, +.modal-warning .modal-header, +.modal-danger .modal-header { + color: #fff; + border-radius: 5px 5px 0 0; +} +.modal-primary .modal-header { + background: #428bca; +} +.modal-info .modal-header { + background: #5bc0de; +} +.modal-success .modal-header { + background: #5cb85c; +} +.modal-warning .modal-header { + background: #f0ad4e; +} +.modal-danger .modal-header { + background: #d9534f; +} diff --git a/client/components/modal/modal.html b/client/components/modal/modal.html new file mode 100644 index 0000000..f04d0db --- /dev/null +++ b/client/components/modal/modal.html @@ -0,0 +1,11 @@ + + + diff --git a/client/components/modal/modal.service.js b/client/components/modal/modal.service.js new file mode 100644 index 0000000..3941b64 --- /dev/null +++ b/client/components/modal/modal.service.js @@ -0,0 +1,78 @@ +'use strict'; + +import angular from 'angular'; + +export function Modal($rootScope, $uibModal) { + /** + * Opens a modal + * @param {Object} scope - an object to be merged with modal's scope + * @param {String} modalClass - (optional) class(es) to be applied to the modal + * @return {Object} - the instance $uibModal.open() returns + */ + function openModal(scope = {}, modalClass = 'modal-default') { + var modalScope = $rootScope.$new(); + + angular.extend(modalScope, scope); + + return $uibModal.open({ + template: require('./modal.html'), + windowClass: modalClass, + scope: modalScope + }); + } + + // Public API here + return { + + /* Confirmation modals */ + confirm: { + + /** + * Create a function to open a delete confirmation modal (ex. ng-click='myModalFn(name, arg1, arg2...)') + * @param {Function} del - callback, ran when delete is confirmed + * @return {Function} - the function to open the modal (ex. myModalFn) + */ + delete(del = angular.noop) { + /** + * Open a delete confirmation modal + * @param {String} name - name or info to show on modal + * @param {All} - any additional args are passed straight to del callback + */ + return function(...args) { + var slicedArgs = Reflect.apply(Array.prototype.slice, args); + var name = slicedArgs.shift(); + var deleteModal; + + deleteModal = openModal({ + modal: { + dismissable: true, + title: 'Confirm Delete', + html: `

Are you sure you want to delete ${name} ?

`, + buttons: [{ + classes: 'btn-danger', + text: 'Delete', + click(e) { + deleteModal.close(e); + } + }, { + classes: 'btn-default', + text: 'Cancel', + click(e) { + deleteModal.dismiss(e); + } + }] + } + }, 'modal-danger'); + + deleteModal.result.then(function(event) { + Reflect.apply(del, event, slicedArgs); + }); + }; + } + } + }; +} + +export default angular.module('app2App.Modal', []) + .factory('Modal', Modal) + .name; diff --git a/client/components/mongoose-error/mongoose-error.directive.js b/client/components/mongoose-error/mongoose-error.directive.js new file mode 100644 index 0000000..057e1cb --- /dev/null +++ b/client/components/mongoose-error/mongoose-error.directive.js @@ -0,0 +1,17 @@ +'use strict'; + +import angular from 'angular'; + +/** + * Removes server error when user updates input + */ +angular.module('app2App') + .directive('mongooseError', function() { + return { + restrict: 'A', + require: 'ngModel', + link(scope, element, attrs, ngModel) { + element.on('keydown', () => ngModel.$setValidity('mongoose', true)); + } + }; + }); diff --git a/client/components/navbar/navbar.component.js b/client/components/navbar/navbar.component.js new file mode 100644 index 0000000..8387bdb --- /dev/null +++ b/client/components/navbar/navbar.component.js @@ -0,0 +1,41 @@ +'use strict'; +/* eslint no-sync: 0 */ + +import angular from 'angular'; + +export class NavbarComponent { + menu = [{ + 'title': 'Home', + 'state': 'main' + },{ + 'title': 'Projects', + 'state': 'project' + },{ + 'title': 'Designer', + 'state': 'designer' + },{ + 'title': 'Runs', + 'state': 'runs' + },{ + 'title': 'Modules', + 'state': 'custom_modules' + }]; + + isCollapsed = true; + + constructor(Auth) { + 'ngInject'; + + this.isLoggedIn = Auth.isLoggedInSync; + this.isAdmin = Auth.isAdminSync; + this.getCurrentUser = Auth.getCurrentUserSync; + } + +} + +export default angular.module('directives.navbar', []) + .component('navbar', { + template: require('./navbar.html'), + controller: NavbarComponent + }) + .name; diff --git a/client/components/navbar/navbar.html b/client/components/navbar/navbar.html new file mode 100644 index 0000000..05e99b0 --- /dev/null +++ b/client/components/navbar/navbar.html @@ -0,0 +1,29 @@ + diff --git a/client/components/oauth-buttons/index.js b/client/components/oauth-buttons/index.js new file mode 100644 index 0000000..094e540 --- /dev/null +++ b/client/components/oauth-buttons/index.js @@ -0,0 +1,25 @@ +'use strict'; + +import angular from 'angular'; + +export function OauthButtonsController($window) { + 'ngInject'; + + this.loginOauth = function(provider) { + $window.location.href = `/auth/${provider}`; + }; +} + +export default angular.module('app2App.oauthButtons', []) + .directive('oauthButtons', function() { + return { + template: require('./oauth-buttons.html'), + restrict: 'EA', + controller: OauthButtonsController, + controllerAs: 'OauthButtons', + scope: { + classes: '@' + } + }; + }) + .name; diff --git a/client/components/oauth-buttons/oauth-buttons.controller.spec.js b/client/components/oauth-buttons/oauth-buttons.controller.spec.js new file mode 100644 index 0000000..9af01f9 --- /dev/null +++ b/client/components/oauth-buttons/oauth-buttons.controller.spec.js @@ -0,0 +1,32 @@ +'use strict'; + +import { + OauthButtonsController +} from './index'; + +describe('Controller: OauthButtonsController', function() { + var controller, $window; + + beforeEach(() => { + angular.module('test', []) + .controller('OauthButtonsController', OauthButtonsController); + }); + // load the controller's module + beforeEach(angular.mock.module('test')); + + // Initialize the controller and a mock $window + beforeEach(inject(function($controller) { + $window = { + location: {} + }; + + controller = $controller('OauthButtonsController', { + $window + }); + })); + + it('should attach loginOauth', function() { + expect(controller.loginOauth) + .to.be.a('function'); + }); +}); diff --git a/client/components/oauth-buttons/oauth-buttons.css b/client/components/oauth-buttons/oauth-buttons.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/client/components/oauth-buttons/oauth-buttons.css @@ -0,0 +1 @@ + diff --git a/client/components/oauth-buttons/oauth-buttons.directive.spec.js b/client/components/oauth-buttons/oauth-buttons.directive.spec.js new file mode 100644 index 0000000..5434a4f --- /dev/null +++ b/client/components/oauth-buttons/oauth-buttons.directive.spec.js @@ -0,0 +1,59 @@ +'use strict'; + +const $ = require('sprint-js'); +import OauthButtons from './index'; + +describe('Directive: oauthButtons', function() { + // load the directive's module and view + beforeEach(angular.mock.module(OauthButtons)); + // beforeEach(angular.mock.module('components/oauth-buttons/oauth-buttons.html')); + + var element, parentScope, elementScope; + + var compileDirective = function(template) { + inject(function($compile) { + element = angular.element(template); + element = $compile(element)(parentScope); + parentScope.$digest(); + elementScope = element.isolateScope(); + }); + }; + + beforeEach(inject(function($rootScope) { + parentScope = $rootScope.$new(); + })); + + it('should contain anchor buttons', function() { + compileDirective(''); + expect($(element[0]) + .find('a.btn.btn-social') + .length) + .to.be.at.least(1); + }); + + it('should evaluate and bind the classes attribute to scope.classes', function() { + parentScope.scopedClass = 'scopedClass1'; + compileDirective(''); + expect(elementScope.classes) + .to.equal('testClass1 scopedClass1'); + }); + + it('should bind scope.classes to class names on the anchor buttons', function() { + compileDirective(''); + // Add classes + elementScope.classes = 'testClass1 testClass2'; + elementScope.$digest(); + expect($(element[0]) + .find('a.btn.btn-social.testClass1.testClass2') + .length) + .to.be.at.least(1); + + // Remove classes + elementScope.classes = ''; + elementScope.$digest(); + expect($(element[0]) + .find('a.btn.btn-social.testClass1.testClass2') + .length) + .to.equal(0); + }); +}); diff --git a/client/components/oauth-buttons/oauth-buttons.html b/client/components/oauth-buttons/oauth-buttons.html new file mode 100644 index 0000000..ce6026a --- /dev/null +++ b/client/components/oauth-buttons/oauth-buttons.html @@ -0,0 +1,9 @@ + + + Connect with Facebook + + + + Connect with Google+ + + diff --git a/client/components/ui-router/ui-router.mock.js b/client/components/ui-router/ui-router.mock.js new file mode 100644 index 0000000..3e2ba06 --- /dev/null +++ b/client/components/ui-router/ui-router.mock.js @@ -0,0 +1,38 @@ +'use strict'; + +const angular = require('angular'); + +angular.module('stateMock', []); +angular.module('stateMock') + .service('$state', function($q) { + this.expectedTransitions = []; + + this.transitionTo = function(stateName) { + if(this.expectedTransitions.length > 0) { + var expectedState = this.expectedTransitions.shift(); + if(expectedState !== stateName) { + throw Error(`Expected transition to state: ${expectedState + } but transitioned to ${stateName}`); + } + } else { + throw Error(`No more transitions were expected! Tried to transition to ${stateName}`); + } + console.log(`Mock transition to: ${stateName}`); + var deferred = $q.defer(); + var promise = deferred.promise; + deferred.resolve(); + return promise; + }; + + this.go = this.transitionTo; + + this.expectTransitionTo = function(stateName) { + this.expectedTransitions.push(stateName); + }; + + this.ensureAllTransitionsHappened = function() { + if(this.expectedTransitions.length > 0) { + throw Error('Not all transitions happened!'); + } + }; + }); diff --git a/client/components/util/util.module.js b/client/components/util/util.module.js new file mode 100644 index 0000000..6f3b8a4 --- /dev/null +++ b/client/components/util/util.module.js @@ -0,0 +1,10 @@ +'use strict'; + +import angular from 'angular'; +import { + UtilService +} from './util.service'; + +export default angular.module('app2App.util', []) + .factory('Util', UtilService) + .name; diff --git a/client/components/util/util.service.js b/client/components/util/util.service.js new file mode 100644 index 0000000..d481da5 --- /dev/null +++ b/client/components/util/util.service.js @@ -0,0 +1,68 @@ +'use strict'; + +import angular from 'angular'; + +/** + * The Util service is for thin, globally reusable, utility functions + */ +export function UtilService($window) { + 'ngInject'; + + var Util = { + /** + * Return a callback or noop function + * + * @param {Function|*} cb - a 'potential' function + * @return {Function} + */ + safeCb(cb) { + return angular.isFunction(cb) ? cb : angular.noop; + }, + + /** + * Parse a given url with the use of an anchor element + * + * @param {String} url - the url to parse + * @return {Object} - the parsed url, anchor element + */ + urlParse(url) { + var a = document.createElement('a'); + a.href = url; + + // Special treatment for IE, see http://stackoverflow.com/a/13405933 for details + if(a.host === '') { + a.href = a.href; + } + + return a; + }, + + /** + * Test whether or not a given url is same origin + * + * @param {String} url - url to test + * @param {String|String[]} [origins] - additional origins to test against + * @return {Boolean} - true if url is same origin + */ + isSameOrigin(url, origins) { + url = Util.urlParse(url); + origins = origins && [].concat(origins) || []; + origins = origins.map(Util.urlParse); + origins.push($window.location); + origins = origins.filter(function(o) { + let hostnameCheck = url.hostname === o.hostname; + let protocolCheck = url.protocol === o.protocol; + // 2nd part of the special treatment for IE fix (see above): + // This part is when using well-known ports 80 or 443 with IE, + // when $window.location.port==='' instead of the real port number. + // Probably the same cause as this IE bug: https://goo.gl/J9hRta + let portCheck = url.port === o.port || o.port === '' && (url.port === '80' || url.port + === '443'); + return hostnameCheck && protocolCheck && portCheck; + }); + return origins.length >= 1; + } + }; + + return Util; +} diff --git a/client/favicon.ico b/client/favicon.ico new file mode 100644 index 0000000..8a163fb Binary files /dev/null and b/client/favicon.ico differ diff --git a/client/polyfills.js b/client/polyfills.js new file mode 100644 index 0000000..314b929 --- /dev/null +++ b/client/polyfills.js @@ -0,0 +1,28 @@ +// Polyfills +// (these modules are what are in 'angular2/bundles/angular2-polyfills' so don't use that here) + +// import 'ie-shim'; // Internet Explorer +// import 'es6-shim'; +// import 'es6-promise'; +// import 'es7-reflect-metadata'; + +// Prefer CoreJS over the polyfills above +import 'core-js/es6'; +import 'core-js/es7/reflect'; +// require('zone.js/dist/zone'); + + +if(!ENV) { + var ENV = 'development'; +} + +if(ENV === 'production') { + // Production +} else { + // Development + + + Error.stackTraceLimit = Infinity; + + // require('zone.js/dist/long-stack-trace-zone'); +} diff --git a/client/robots.txt b/client/robots.txt new file mode 100644 index 0000000..9417495 --- /dev/null +++ b/client/robots.txt @@ -0,0 +1,3 @@ +# robotstxt.org + +User-agent: * diff --git a/e2e/account/login/login.po.js b/e2e/account/login/login.po.js new file mode 100644 index 0000000..98e2b98 --- /dev/null +++ b/e2e/account/login/login.po.js @@ -0,0 +1,28 @@ +/** + * This file uses the Page Object pattern to define the main page for tests + * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ + */ + +'use strict'; + +var LoginPage = function() { + var form = this.form = element(by.css('.form')); + form.email = form.element(by.model('vm.user.email')); + form.password = form.element(by.model('vm.user.password')); + form.submit = form.element(by.css('.btn-login')); + form.oauthButtons = require('../../components/oauth-buttons/oauth-buttons.po').oauthButtons; + + this.login = function(data) { + for (var prop in data) { + var formElem = form[prop]; + if (data.hasOwnProperty(prop) && formElem && typeof formElem.sendKeys === 'function') { + formElem.sendKeys(data[prop]); + } + } + + return form.submit.click(); + }; +}; + +module.exports = new LoginPage(); + diff --git a/e2e/account/login/login.spec.js b/e2e/account/login/login.spec.js new file mode 100644 index 0000000..8c5be9d --- /dev/null +++ b/e2e/account/login/login.spec.js @@ -0,0 +1,87 @@ +'use strict'; + +var config = browser.params; +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model').default; + +describe('Login View', function() { + var page; + + var loadPage = function() { + let promise = browser.get(config.baseUrl + '/login'); + page = require('./login.po'); + return promise; + }; + + var testUser = { + name: 'Test User', + email: 'test@example.com', + password: 'test' + }; + + before(function() { + return UserModel + .remove() + .then(function() { + return UserModel.create(testUser); + }) + .then(loadPage); + }); + + after(function() { + return UserModel.remove(); + }); + + it('should include login form with correct inputs and submit button', function() { + expect(page.form.email.getAttribute('type')).to.eventually.equal('email'); + expect(page.form.email.getAttribute('name')).to.eventually.equal('email'); + expect(page.form.password.getAttribute('type')).to.eventually.equal('password'); + expect(page.form.password.getAttribute('name')).to.eventually.equal('password'); + expect(page.form.submit.getAttribute('type')).to.eventually.equal('submit'); + expect(page.form.submit.getText()).to.eventually.equal('Login'); + }); + + it('should include oauth buttons with correct classes applied', function() { + expect(page.form.oauthButtons.facebook.getText()).to.eventually.equal('Connect with Facebook'); + expect(page.form.oauthButtons.facebook.getAttribute('class')).to.eventually.contain('btn-block'); + expect(page.form.oauthButtons.google.getText()).to.eventually.equal('Connect with Google+'); + expect(page.form.oauthButtons.google.getAttribute('class')).to.eventually.contain('btn-block'); + }); + + describe('with local auth', function() { + + it('should login a user and redirecting to "/"', function() { + return page.login(testUser).then(() => { + var navbar = require('../../components/navbar/navbar.po'); + + return browser.wait( + () => element(by.css('.hero-unit')), + 5000, + `Didn't find .hero-unit after 5s` + ).then(() => { + expect(browser.getCurrentUrl()).to.eventually.equal(config.baseUrl + '/'); + expect(navbar.navbarAccountGreeting.getText()).to.eventually.equal('Hello ' + testUser.name); + }); + }); + }); + + describe('and invalid credentials', function() { + before(function() { + return loadPage(); + }) + + it('should indicate login failures', function() { + page.login({ + email: testUser.email, + password: 'badPassword' + }); + + expect(browser.getCurrentUrl()).to.eventually.equal(config.baseUrl + '/login'); + + var helpBlock = page.form.element(by.css('.form-group.has-error .help-block.ng-binding')); + expect(helpBlock.getText()).to.eventually.equal('This password is not correct.'); + }); + + }); + + }); +}); diff --git a/e2e/account/logout/logout.spec.js b/e2e/account/logout/logout.spec.js new file mode 100644 index 0000000..816e5a0 --- /dev/null +++ b/e2e/account/logout/logout.spec.js @@ -0,0 +1,51 @@ +'use strict'; + +var config = browser.params; +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model').default; + +describe('Logout View', function() { + var login = function(user) { + let promise = browser.get(config.baseUrl + '/login'); + require('../login/login.po').login(user); + return promise; + }; + + var testUser = { + name: 'Test User', + email: 'test@example.com', + password: 'test' + }; + + beforeEach(function() { + return UserModel + .remove() + .then(function() { + return UserModel.create(testUser); + }) + .then(function() { + return login(testUser); + }); + }); + + after(function() { + return UserModel.remove(); + }) + + describe('with local auth', function() { + + it('should logout a user and redirecting to "/"', function() { + var navbar = require('../../components/navbar/navbar.po'); + + expect(browser.getCurrentUrl()).to.eventually.equal(config.baseUrl + '/'); + expect(navbar.navbarAccountGreeting.getText()).to.eventually.equal('Hello ' + testUser.name); + + browser.get(config.baseUrl + '/logout'); + + navbar = require('../../components/navbar/navbar.po'); + + expect(browser.getCurrentUrl()).to.eventually.equal(config.baseUrl + '/'); + expect(navbar.navbarAccountGreeting.isDisplayed()).to.eventually.equal(false); + }); + + }); +}); diff --git a/e2e/account/signup/signup.po.js b/e2e/account/signup/signup.po.js new file mode 100644 index 0000000..9bfee98 --- /dev/null +++ b/e2e/account/signup/signup.po.js @@ -0,0 +1,30 @@ +/** + * This file uses the Page Object pattern to define the main page for tests + * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ + */ + +'use strict'; + +var SignupPage = function() { + var form = this.form = element(by.css('.form')); + form.name = form.element(by.model('vm.user.name')); + form.email = form.element(by.model('vm.user.email')); + form.password = form.element(by.model('vm.user.password')); + form.confirmPassword = form.element(by.model('vm.user.confirmPassword')); + form.submit = form.element(by.css('.btn-register')); + form.oauthButtons = require('../../components/oauth-buttons/oauth-buttons.po').oauthButtons; + + this.signup = function(data) { + for (var prop in data) { + var formElem = form[prop]; + if (data.hasOwnProperty(prop) && formElem && typeof formElem.sendKeys === 'function') { + formElem.sendKeys(data[prop]); + } + } + + return form.submit.click(); + }; +}; + +module.exports = new SignupPage(); + diff --git a/e2e/account/signup/signup.spec.js b/e2e/account/signup/signup.spec.js new file mode 100644 index 0000000..1c5ca6b --- /dev/null +++ b/e2e/account/signup/signup.spec.js @@ -0,0 +1,84 @@ +'use strict'; + +var config = browser.params; +var UserModel = require(config.serverConfig.root + '/server/api/user/user.model').default; + +describe('Signup View', function() { + var page; + + var loadPage = function() { + browser.manage().deleteAllCookies() + let promise = browser.get(config.baseUrl + '/signup'); + page = require('./signup.po'); + return promise; + }; + + var testUser = { + name: 'Test', + email: 'test@example.com', + password: 'test', + confirmPassword: 'test' + }; + + before(function() { + return loadPage(); + }); + + after(function() { + return UserModel.remove(); + }); + + it('should include signup form with correct inputs and submit button', function() { + expect(page.form.name.getAttribute('type')).to.eventually.equal('text'); + expect(page.form.name.getAttribute('name')).to.eventually.equal('name'); + expect(page.form.email.getAttribute('type')).to.eventually.equal('email'); + expect(page.form.email.getAttribute('name')).to.eventually.equal('email'); + expect(page.form.password.getAttribute('type')).to.eventually.equal('password'); + expect(page.form.password.getAttribute('name')).to.eventually.equal('password'); + expect(page.form.confirmPassword.getAttribute('type')).to.eventually.equal('password'); + expect(page.form.confirmPassword.getAttribute('name')).to.eventually.equal('confirmPassword'); + expect(page.form.submit.getAttribute('type')).to.eventually.equal('submit'); + expect(page.form.submit.getText()).to.eventually.equal('Sign up'); + }); + + it('should include oauth buttons with correct classes applied', function() { + expect(page.form.oauthButtons.facebook.getText()).to.eventually.equal('Connect with Facebook'); + expect(page.form.oauthButtons.facebook.getAttribute('class')).to.eventually.contain('btn-block'); + expect(page.form.oauthButtons.google.getText()).to.eventually.equal('Connect with Google+'); + expect(page.form.oauthButtons.google.getAttribute('class')).to.eventually.contain('btn-block'); + }); + + describe('with local auth', function() { + + before(function() { + return UserModel.remove(); + }) + + it('should signup a new user, log them in, and redirecting to "/"', function() { + page.signup(testUser); + + var navbar = require('../../components/navbar/navbar.po'); + + expect(browser.getCurrentUrl()).to.eventually.equal(config.baseUrl + '/'); + expect(navbar.navbarAccountGreeting.getText()).to.eventually.equal('Hello ' + testUser.name); + }); + + describe('and invalid credentials', function() { + before(function() { + return loadPage(); + }); + + it('should indicate signup failures', function() { + page.signup(testUser); + + expect(browser.getCurrentUrl()).to.eventually.equal(config.baseUrl + '/signup'); + expect(page.form.email.getAttribute('class')).to.eventually.contain('ng-invalid-mongoose'); + + var helpBlock = page.form.element(by.css('.form-group.has-error .help-block.ng-binding')); + expect(helpBlock.getText()).to.eventually.equal('The specified email address is already in use.'); + }); + + }); + + }); +}); diff --git a/e2e/components/navbar/navbar.po.js b/e2e/components/navbar/navbar.po.js new file mode 100644 index 0000000..b904342 --- /dev/null +++ b/e2e/components/navbar/navbar.po.js @@ -0,0 +1,16 @@ +/** + * This file uses the Page Object pattern to define the main page for tests + * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ + */ + +'use strict'; + +var NavbarComponent = function() { + this.navbar = element(by.css('.navbar')); + this.navbarHeader = this.navbar.element(by.css('.navbar-header')); + this.navbarNav = this.navbar.element(by.css('#navbar-main .nav.navbar-nav:not(.navbar-right)')); + this.navbarAccount = this.navbar.element(by.css('#navbar-main .nav.navbar-nav.navbar-right')); + this.navbarAccountGreeting = this.navbarAccount.element(by.binding('getCurrentUser().name')); +}; + +module.exports = new NavbarComponent(); diff --git a/e2e/components/oauth-buttons/oauth-buttons.po.js b/e2e/components/oauth-buttons/oauth-buttons.po.js new file mode 100644 index 0000000..9cef0d4 --- /dev/null +++ b/e2e/components/oauth-buttons/oauth-buttons.po.js @@ -0,0 +1,14 @@ +/** + * This file uses the Page Object pattern to define the main page for tests + * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ + */ + +'use strict'; + +var OauthButtons = function() { + var oauthButtons = this.oauthButtons = element(by.css('oauth-buttons')); + oauthButtons.facebook = oauthButtons.element(by.css('.btn.btn-social.btn-facebook')); + oauthButtons.google = oauthButtons.element(by.css('.btn.btn-social.btn-google')); +}; + +module.exports = new OauthButtons(); diff --git a/e2e/main/main.po.js b/e2e/main/main.po.js new file mode 100644 index 0000000..6718608 --- /dev/null +++ b/e2e/main/main.po.js @@ -0,0 +1,15 @@ +/** + * This file uses the Page Object pattern to define the main page for tests + * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ + */ + +'use strict'; + +var MainPage = function() { + this.heroEl = element(by.css('.hero-unit')); + this.h1El = this.heroEl.element(by.css('h1')); + this.imgEl = this.heroEl.element(by.css('img')); +}; + +module.exports = new MainPage(); + diff --git a/e2e/main/main.spec.js b/e2e/main/main.spec.js new file mode 100644 index 0000000..abfddf7 --- /dev/null +++ b/e2e/main/main.spec.js @@ -0,0 +1,19 @@ +'use strict'; + +var config = browser.params; + +describe('Main View', function() { + var page; + + beforeEach(function() { + let promise = browser.get(config.baseUrl + '/'); + page = require('./main.po'); + return promise; + }); + + it('should include jumbotron with correct data', function() { + expect(page.h1El.getText()).to.eventually.equal('\'Allo, \'Allo!'); + expect(page.imgEl.getAttribute('src')).to.eventually.match(/yeoman(\.[a-zA-Z0-9]*)?\.png$/); + expect(page.imgEl.getAttribute('alt')).to.eventually.equal('I\'m Yeoman'); + }); +}); diff --git a/gulpfile.babel.js b/gulpfile.babel.js new file mode 100644 index 0000000..b0b4440 --- /dev/null +++ b/gulpfile.babel.js @@ -0,0 +1,596 @@ +// Generated on 2017-06-04 using generator-angular-fullstack 4.2.2 +'use strict'; + +import _ from 'lodash'; +import del from 'del'; +import gulp from 'gulp'; +import grunt from 'grunt'; +import path from 'path'; +import through2 from 'through2'; +import gulpLoadPlugins from 'gulp-load-plugins'; +import http from 'http'; +import open from 'open'; +import lazypipe from 'lazypipe'; +import nodemon from 'nodemon'; +import {Server as KarmaServer} from 'karma'; +import runSequence from 'run-sequence'; +import {protractor, webdriver_update} from 'gulp-protractor'; +import {Instrumenter} from 'isparta'; +import webpack from 'webpack-stream'; +import makeWebpackConfig from './webpack.make'; + +var plugins = gulpLoadPlugins(); +var config; + +const clientPath = 'client'; +const serverPath = 'server'; +const paths = { + client: { + assets: `${clientPath}/assets/**/*`, + images: `${clientPath}/assets/images/**/*`, + revManifest: `${clientPath}/assets/rev-manifest.json`, + scripts: [ + `${clientPath}/**/!(*.spec|*.mock).js` + ], + styles: [`${clientPath}/{app,components}/**/*.css`], + mainStyle: `${clientPath}/app/app.css`, + views: `${clientPath}/{app,components}/**/*.html`, + mainView: `${clientPath}/index.html`, + test: [`${clientPath}/{app,components}/**/*.{spec,mock}.js`], + e2e: ['e2e/**/*.spec.js'] + }, + server: { + scripts: [ + `${serverPath}/**/!(*.spec|*.integration).js`, + `!${serverPath}/config/local.env.sample.js` + ], + json: [`${serverPath}/**/*.json`], + test: { + integration: [`${serverPath}/**/*.integration.js`, 'mocha.global.js'], + unit: [`${serverPath}/**/*.spec.js`, 'mocha.global.js'] + } + }, + karma: 'karma.conf.js', + dist: 'dist' +}; + +/******************** + * Helper functions + ********************/ + +function onServerLog(log) { + console.log(plugins.util.colors.white('[') + + plugins.util.colors.yellow('nodemon') + + plugins.util.colors.white('] ') + + log.message); +} + +function checkAppReady(cb) { + var options = { + host: 'localhost', + port: config.port + }; + http + .get(options, () => cb(true)) + .on('error', () => cb(false)); +} + +// Call page until first success +function whenServerReady(cb) { + var serverReady = false; + var appReadyInterval = setInterval(() => + checkAppReady((ready) => { + if (!ready || serverReady) { + return; + } + clearInterval(appReadyInterval); + serverReady = true; + cb(); + }), + 100); +} + +/******************** + * Reusable pipelines + ********************/ + +let lintClientScripts = lazypipe() + .pipe(plugins.eslint, `${clientPath}/.eslintrc`) + .pipe(plugins.eslint.format); + +const lintClientTestScripts = lazypipe() + .pipe(plugins.eslint, { + configFile: `${clientPath}/.eslintrc`, + envs: [ + 'browser', + 'es6', + 'mocha' + ] + }) + .pipe(plugins.eslint.format); + +let lintServerScripts = lazypipe() + .pipe(plugins.eslint, `${serverPath}/.eslintrc`) + .pipe(plugins.eslint.format); + +let lintServerTestScripts = lazypipe() + .pipe(plugins.eslint, { + configFile: `${serverPath}/.eslintrc`, + envs: [ + 'node', + 'es6', + 'mocha' + ] + }) + .pipe(plugins.eslint.format); + +let transpileServer = lazypipe() + .pipe(plugins.sourcemaps.init) + .pipe(plugins.babel, { + plugins: [ + 'transform-class-properties', + 'transform-runtime' + ] + }) + .pipe(plugins.sourcemaps.write, '.'); + +let mocha = lazypipe() + .pipe(plugins.mocha, { + reporter: 'spec', + timeout: 5000, + require: [ + './mocha.conf' + ] + }); + +let istanbul = lazypipe() + .pipe(plugins.istanbul.writeReports) + .pipe(plugins.istanbulEnforcer, { + thresholds: { + global: { + lines: 80, + statements: 80, + branches: 80, + functions: 80 + } + }, + coverageDirectory: './coverage', + rootDirectory : '' + }); + +/******************** + * Env + ********************/ + +gulp.task('env:all', () => { + let localConfig; + try { + localConfig = require(`./${serverPath}/config/local.env`); + } catch (e) { + localConfig = {}; + } + plugins.env({ + vars: localConfig + }); +}); +gulp.task('env:test', () => { + plugins.env({ + vars: {NODE_ENV: 'test'} + }); +}); +gulp.task('env:prod', () => { + plugins.env({ + vars: {NODE_ENV: 'production'} + }); +}); + +/******************** + * Tasks + ********************/ + +gulp.task('inject', cb => { + runSequence(['inject:css'], cb); +}); + +gulp.task('inject:css', () => { + return gulp.src(paths.client.mainStyle) + .pipe(plugins.inject( + gulp.src(_.union(paths.client.styles, ['!' + paths.client.mainStyle]), {read: false}) + .pipe(plugins.sort()), + { + starttag: '/* inject:css */', + endtag: '/* endinject */', + transform: (filepath) => { + let newPath = filepath + .replace(`/${clientPath}/app/`, '') + .replace(`/${clientPath}/components/`, '../components/') + .replace(/_(.*).css/, (match, p1, offset, string) => p1); + return `@import '${newPath}';`; + } + })) + .pipe(gulp.dest(`${clientPath}/app`)); +}); + +gulp.task('webpack:dev', function() { + const webpackDevConfig = makeWebpackConfig({ DEV: true }); + return gulp.src(webpackDevConfig.entry.app) + .pipe(plugins.plumber()) + .pipe(webpack(webpackDevConfig)) + .pipe(gulp.dest('.tmp')); +}); + +gulp.task('webpack:dist', function() { + const webpackDistConfig = makeWebpackConfig({ BUILD: true }); + return gulp.src(webpackDistConfig.entry.app) + .pipe(webpack(webpackDistConfig)) + .on('error', (err) => { + this.emit('end'); // Recover from errors + }) + .pipe(gulp.dest(`${paths.dist}/client`)); +}); + +gulp.task('webpack:test', function() { + const webpackTestConfig = makeWebpackConfig({ TEST: true }); + return gulp.src(webpackTestConfig.entry.app) + .pipe(webpack(webpackTestConfig)) + .pipe(gulp.dest('.tmp')); +}); + +gulp.task('webpack:e2e', function() { + const webpackE2eConfig = makeWebpackConfig({ E2E: true }); + return gulp.src(webpackE2eConfig.entry.app) + .pipe(webpack(webpackE2eConfig)) + .pipe(gulp.dest('.tmp')); +}); + +gulp.task('styles', () => { + return gulp.src(paths.client.styles) + .pipe(styles()) + .pipe(gulp.dest('.tmp/app')); +}); + +gulp.task('transpile:server', () => { + return gulp.src(_.union(paths.server.scripts, paths.server.json)) + .pipe(transpileServer()) + .pipe(gulp.dest(`${paths.dist}/${serverPath}`)); +}); + +gulp.task('lint:scripts', cb => runSequence(['lint:scripts:client', 'lint:scripts:server'], cb)); + +gulp.task('lint:scripts:client', () => { + return gulp.src(_.union( + paths.client.scripts, + _.map(paths.client.test, blob => '!' + blob) + )) + .pipe(lintClientScripts()); +}); + +gulp.task('lint:scripts:server', () => { + return gulp.src(_.union(paths.server.scripts, _.map(paths.server.test, blob => '!' + blob))) + .pipe(lintServerScripts()); +}); + +gulp.task('lint:scripts:clientTest', () => { + return gulp.src(paths.client.test) + .pipe(lintClientScripts()); +}); + +gulp.task('lint:scripts:serverTest', () => { + return gulp.src(paths.server.test) + .pipe(lintServerTestScripts()); +}); + +gulp.task('jscs', () => { + return gulp.src(_.union(paths.client.scripts, paths.server.scripts)) + .pipe(plugins.jscs()) + .pipe(plugins.jscs.reporter()); +}); + +gulp.task('clean:tmp', () => del(['.tmp/**/*'], {dot: true})); + +gulp.task('start:client', cb => { + whenServerReady(() => { + open('http://localhost:' + config.browserSyncPort); + cb(); + }); +}); + +gulp.task('start:server', () => { + process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + config = require(`./${serverPath}/config/environment`); + nodemon(`-w ${serverPath} ${serverPath}`) + .on('log', onServerLog); +}); + +gulp.task('start:server:prod', () => { + process.env.NODE_ENV = process.env.NODE_ENV || 'production'; + config = require(`./${paths.dist}/${serverPath}/config/environment`); + nodemon(`-w ${paths.dist}/${serverPath} ${paths.dist}/${serverPath}`) + .on('log', onServerLog); +}); + +gulp.task('start:server:debug', () => { + process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + config = require(`./${serverPath}/config/environment`); + // nodemon(`-w ${serverPath} --debug=5858 --debug-brk ${serverPath}`) + nodemon(`-w ${serverPath} --inspect --debug-brk ${serverPath}`) + .on('log', onServerLog); +}); + +gulp.task('watch', () => { + var testFiles = _.union(paths.client.test, paths.server.test.unit, paths.server.test.integration); + + plugins.watch(_.union(paths.server.scripts, testFiles)) + .pipe(plugins.plumber()) + .pipe(lintServerScripts()); + + plugins.watch(_.union(paths.server.test.unit, paths.server.test.integration)) + .pipe(plugins.plumber()) + .pipe(lintServerTestScripts()); +}); + +gulp.task('serve', cb => { + runSequence( + [ + 'clean:tmp', + 'lint:scripts', + 'inject', + 'copy:fonts:dev', + 'env:all' + ], + // 'webpack:dev', + ['start:server', 'start:client'], + 'watch', + cb + ); +}); + +gulp.task('serve:debug', cb => { + runSequence( + [ + 'clean:tmp', + 'lint:scripts', + 'inject', + 'copy:fonts:dev', + 'env:all' + ], + 'webpack:dev', + ['start:server:debug', 'start:client'], + 'watch', + cb + ); +}); + +gulp.task('serve:dist', cb => { + runSequence( + 'build', + 'env:all', + 'env:prod', + ['start:server:prod', 'start:client'], + cb); +}); + +gulp.task('test', cb => { + return runSequence('test:server', 'test:client', cb); +}); + +gulp.task('test:server', cb => { + runSequence( + 'env:all', + 'env:test', + 'mocha:unit', + 'mocha:integration', + cb); +}); + +gulp.task('mocha:unit', () => { + return gulp.src(paths.server.test.unit) + .pipe(mocha()); +}); + +gulp.task('mocha:integration', () => { + return gulp.src(paths.server.test.integration) + .pipe(mocha()); +}); + +gulp.task('test:server:coverage', cb => { + runSequence('coverage:pre', + 'env:all', + 'env:test', + 'coverage:unit', + 'coverage:integration', + cb); +}); + +gulp.task('coverage:pre', () => { + return gulp.src(paths.server.scripts) + // Covering files + .pipe(plugins.istanbul({ + instrumenter: Instrumenter, // Use the isparta instrumenter (code coverage for ES6) + includeUntested: true + })) + // Force `require` to return covered files + .pipe(plugins.istanbul.hookRequire()); +}); + +gulp.task('coverage:unit', () => { + return gulp.src(paths.server.test.unit) + .pipe(mocha()) + .pipe(istanbul()) + // Creating the reports after tests ran +}); + +gulp.task('coverage:integration', () => { + return gulp.src(paths.server.test.integration) + .pipe(mocha()) + .pipe(istanbul()) + // Creating the reports after tests ran +}); + +// Downloads the selenium webdriver +gulp.task('webdriver_update', webdriver_update); + +gulp.task('test:e2e', ['webpack:e2e', 'env:all', 'env:test', 'start:server', 'webdriver_update'], cb => { + gulp.src(paths.client.e2e) + .pipe(protractor({ + configFile: 'protractor.conf.js', + })) + .on('error', e => { throw e }) + .on('end', () => { process.exit() }); +}); + +gulp.task('test:client', done => { + new KarmaServer({ + configFile: `${__dirname}/${paths.karma}`, + singleRun: true + }, err => { + done(err); + process.exit(err); + }).start(); +}); + +/******************** + * Build + ********************/ + +gulp.task('build', cb => { + runSequence( + [ + 'clean:dist', + 'clean:tmp' + ], + 'inject', + 'transpile:server', + [ + 'build:images' + ], + [ + 'copy:extras', + 'copy:assets', + 'copy:fonts:dist', + 'copy:server', + 'webpack:dist' + ], + 'revReplaceWebpack', + cb); +}); + +gulp.task('clean:dist', () => del([`${paths.dist}/!(.git*|.openshift|Procfile)**`], {dot: true})); + +gulp.task('build:images', () => { + return gulp.src(paths.client.images) + .pipe(plugins.imagemin([ + plugins.imagemin.optipng({optimizationLevel: 5}), + plugins.imagemin.jpegtran({progressive: true}), + plugins.imagemin.gifsicle({interlaced: true}), + plugins.imagemin.svgo({plugins: [{removeViewBox: false}]}) + ])) + .pipe(plugins.rev()) + .pipe(gulp.dest(`${paths.dist}/${clientPath}/assets/images`)) + .pipe(plugins.rev.manifest(`${paths.dist}/${paths.client.revManifest}`, { + base: `${paths.dist}/${clientPath}/assets`, + merge: true + })) + .pipe(gulp.dest(`${paths.dist}/${clientPath}/assets`)); +}); + +gulp.task('revReplaceWebpack', function() { + return gulp.src('dist/client/app.*.js') + .pipe(plugins.revReplace({manifest: gulp.src(`${paths.dist}/${paths.client.revManifest}`)})) + .pipe(gulp.dest('dist/client')); +}); + +gulp.task('copy:extras', () => { + return gulp.src([ + `${clientPath}/favicon.ico`, + `${clientPath}/robots.txt`, + `${clientPath}/.htaccess` + ], { dot: true }) + .pipe(gulp.dest(`${paths.dist}/${clientPath}`)); +}); + +/** + * turns 'bootstrap/fonts/font.woff' into 'bootstrap/font.woff' + */ +function flatten() { + return through2.obj(function(file, enc, next) { + if(!file.isDirectory()) { + try { + let dir = path.dirname(file.relative).split(path.sep)[0]; + let fileName = path.normalize(path.basename(file.path)); + file.path = path.join(file.base, path.join(dir, fileName)); + this.push(file); + } catch(e) { + this.emit('error', new Error(e)); + } + } + next(); + }); +} +gulp.task('copy:fonts:dev', () => { + return gulp.src('node_modules/{bootstrap,font-awesome}/fonts/*') + .pipe(flatten()) + .pipe(gulp.dest(`${clientPath}/assets/fonts`)); +}); +gulp.task('copy:fonts:dist', () => { + return gulp.src('node_modules/{bootstrap,font-awesome}/fonts/*') + .pipe(flatten()) + .pipe(gulp.dest(`${paths.dist}/${clientPath}/assets/fonts`)); +}); + +gulp.task('copy:assets', () => { + return gulp.src([paths.client.assets, '!' + paths.client.images]) + .pipe(gulp.dest(`${paths.dist}/${clientPath}/assets`)); +}); + +gulp.task('copy:server', () => { + return gulp.src([ + 'package.json' + ], {cwdbase: true}) + .pipe(gulp.dest(paths.dist)); +}); + +/******************** + * Grunt ported tasks + ********************/ + +grunt.initConfig({ + buildcontrol: { + options: { + dir: paths.dist, + commit: true, + push: true, + connectCommits: false, + message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%' + }, + heroku: { + options: { + remote: 'heroku', + branch: 'master' + } + }, + openshift: { + options: { + remote: 'openshift', + branch: 'master' + } + } + } +}); + +grunt.loadNpmTasks('grunt-build-control'); + +gulp.task('buildcontrol:heroku', function(done) { + grunt.tasks( + ['buildcontrol:heroku'], //you can add more grunt tasks in this array + {gruntfile: false}, //don't look for a Gruntfile - there is none. :-) + function() {done();} + ); +}); +gulp.task('buildcontrol:openshift', function(done) { + grunt.tasks( + ['buildcontrol:openshift'], //you can add more grunt tasks in this array + {gruntfile: false}, //don't look for a Gruntfile - there is none. :-) + function() {done();} + ); +}); diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..d0e5eb6 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,98 @@ +// Karma configuration +// http://karma-runner.github.io/0.13/config/configuration-file.html +/*eslint-env node*/ + +import makeWebpackConfig from './webpack.make'; + +module.exports = function(config) { + config.set({ + // base path, that will be used to resolve files and exclude + basePath: '', + + // testing framework to use (jasmine/mocha/qunit/...) + frameworks: ['mocha', 'chai', 'sinon-chai', 'chai-as-promised', 'chai-things'], + + client: { + mocha: { + timeout: 5000 // set default mocha spec timeout + } + }, + + // list of files / patterns to load in the browser + files: ['spec.js'], + + preprocessors: { + 'spec.js': ['webpack'] + }, + + webpack: makeWebpackConfig({ TEST: true }), + + webpackMiddleware: { + // webpack-dev-middleware configuration + // i. e. + noInfo: true + }, + + coverageReporter: { + reporters: [{ + type: 'html', //produces a html document after code is run + subdir: 'client' + }, { + type: 'json', + subdir: '.', + file: 'client-coverage.json' + }], + dir: 'coverage/' //path to created html doc + }, + + plugins: [ + require('karma-chrome-launcher'), + require('karma-coverage'), + require('karma-firefox-launcher'), + require('karma-mocha'), + require('karma-chai-plugins'), + + require('karma-spec-reporter'), + require('karma-phantomjs-launcher'), + require('karma-script-launcher'), + require('karma-webpack'), + require('karma-sourcemap-loader') + ], + + // list of files / patterns to exclude + exclude: [], + + // web server port + port: 9000, + + // level of logging + // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG + logLevel: config.LOG_INFO, + + // reporter types: + // - dots + // - progress (default) + // - spec (karma-spec-reporter) + // - junit + // - growl + // - coverage + reporters: ['spec', 'coverage'], + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + browsers: ['PhantomJS'], + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: false + }); +}; diff --git a/mocha.conf.js b/mocha.conf.js new file mode 100644 index 0000000..76f5662 --- /dev/null +++ b/mocha.conf.js @@ -0,0 +1,19 @@ +'use strict'; + +// Register the Babel require hook +require('babel-core/register'); + +var chai = require('chai'); + +// Load Chai assertions +global.expect = chai.expect; +global.assert = chai.assert; +chai.should(); + +// Load Sinon +global.sinon = require('sinon'); + +// Initialize Chai plugins +chai.use(require('sinon-chai')); +chai.use(require('chai-as-promised')); +chai.use(require('chai-things')) diff --git a/mocha.global.js b/mocha.global.js new file mode 100644 index 0000000..e7ebef8 --- /dev/null +++ b/mocha.global.js @@ -0,0 +1,8 @@ +import app from './'; +import mongoose from 'mongoose'; + +after(function(done) { + app.angularFullstack.on('close', () => done()); + mongoose.connection.close(); + app.angularFullstack.close(); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..477a02a --- /dev/null +++ b/package.json @@ -0,0 +1,151 @@ +{ + "name": "app2", + "version": "0.0.0", + "main": "server/index.js", + "dependencies": { + "ace-builds": "^1.2.6", + "angular": "~1.6.0", + "angular-animate": "~1.6.0", + "angular-aria": "~1.6.0", + "angular-cookies": "~1.6.0", + "angular-markdown-directive": "^0.3.1", + "angular-resource": "~1.6.0", + "angular-sanitize": "^1.6.4", + "angular-tree-control": "^0.2.28", + "angular-ui-ace": "^0.2.3", + "angular-ui-bootstrap": "^2.0.1", + "angular-ui-router": "~0.3.1", + "angular-validation-match": "^1.9.0", + "ansi-to-html": "^0.6.2", + "ansi2html": "0.0.1", + "babel-polyfill": "^6.7.2", + "babel-runtime": "^6.6.1", + "bluebird": "^3.3.3", + "body-parser": "^1.13.3", + "bootstrap": "~3.3.7", + "bootstrap-social": "^5.0.0", + "composable-middleware": "^0.3.0", + "connect-mongo": "^1.2.1", + "cookie-parser": "^1.3.5", + "core-js": "^2.2.1", + "ejs": "^2.5.3", + "errorhandler": "^1.4.2", + "express": "^4.13.3", + "express-jwt": "^5.0.0", + "express-session": "^1.11.3", + "fast-json-patch": "^1.0.0", + "filendir": "^1.0.0", + "font-awesome": ">=4.1.0", + "json-loader": "^0.5.4", + "jsonwebtoken": "^7.0.0", + "lodash": "^4.6.1", + "lusca": "^1.3.0", + "method-override": "^2.3.5", + "mongoose": "^4.1.2", + "morgan": "^1.8.0", + "passport": "~0.3.0", + "passport-facebook": "^2.0.0", + "passport-google-oauth20": "^1.0.0", + "passport-local": "^1.0.0", + "scp2": "^0.5.0", + "serve-favicon": "^2.3.0", + "showdown": "^0.3.1", + "shrink-ray": "^0.1.3", + "sprint-js": "~0.1.0", + "ssh2": "^0.5.0", + "underscore": "^1.8.3", + "yamljs": "^0.2.10" + }, + "devDependencies": { + "angular-mocks": "~1.6.0", + "autoprefixer": "^6.0.0", + "babel-core": "^6.6.5", + "babel-eslint": "^6.0.4", + "babel-register": "^6.6.5", + "browser-sync": "^2.8.0", + "bs-fullscreen-message": "^1.0.0", + "babel-plugin-transform-class-properties": "^6.6.0", + "babel-plugin-transform-runtime": "^6.6.0", + "babel-preset-es2015": "^6.6.0", + "eslint": "^2.12.0", + "del": "^2.0.2", + "gulp": "^3.9.1", + "gulp-babel": "^6.1.2", + "gulp-env": "^0.4.0", + "gulp-eslint": "^2.0.0", + "gulp-imagemin": "^3.0.1", + "gulp-inject": "^4.0.0", + "gulp-istanbul": "^1.1.1", + "gulp-istanbul-enforcer": "^1.0.3", + "gulp-load-plugins": "^1.0.0-rc.1", + "gulp-mocha": "^2.1.3", + "gulp-plumber": "^1.0.1", + "gulp-protractor": "^3.0.0", + "gulp-rev": "^7.0.0", + "gulp-rev-replace": "^0.4.2", + "gulp-sort": "^2.0.0", + "gulp-sourcemaps": "^1.5.2", + "gulp-util": "^3.0.5", + "gulp-watch": "^4.3.5", + "gulp-stylint": "^3.0.0", + "grunt": "^1.0.1", + "grunt-build-control": "^0.7.0", + "isparta": "^4.0.0", + "nodemon": "^1.3.7", + "run-sequence": "^1.1.0", + "lazypipe": "^1.0.1", + "webpack": "^1.12.14", + "webpack-dev-middleware": "^1.5.1", + "webpack-stream": "^3.2.0", + "extract-text-webpack-plugin": "^1.0.1", + "html-webpack-plugin": "^2.16.0", + "html-webpack-harddisk-plugin": "~0.0.2", + "awesome-typescript-loader": "^1.1.1", + "ng-annotate-loader": "~0.1.0", + "babel-loader": "^6.2.4", + "css-loader": "^0.24.0", + "file-loader": "^0.9.0", + "imports-loader": "^0.6.5", + "isparta-instrumenter-loader": "^1.0.0", + "isparta-loader": "^2.0.0", + "istanbul-instrumenter-loader": "^0.2.0", + "null-loader": "^0.1.1", + "postcss-loader": "^0.11.1", + "raw-loader": "^0.5.1", + "style-loader": "^0.13.0", + "karma-webpack": "^1.7.0", + "through2": "^2.0.1", + "open": "~0.0.4", + "istanbul": "1.1.0-alpha.1", + "chai": "^3.2.0", + "sinon": "^1.16.1", + "chai-as-promised": "^5.1.0", + "chai-things": "^0.2.0", + "karma": "~0.13.3", + "karma-firefox-launcher": "^1.0.0", + "karma-script-launcher": "^1.0.0", + "karma-coverage": "^1.0.0", + "karma-chrome-launcher": "^2.0.0", + "karma-phantomjs-launcher": "~1.0.0", + "karma-spec-reporter": "~0.0.20", + "karma-sourcemap-loader": "~0.3.7", + "sinon-chai": "^2.8.0", + "mocha": "^3.0.2", + "karma-mocha": "^1.0.1", + "karma-chai-plugins": "~0.7.0", + "phantomjs-prebuilt": "^2.1.4", + "proxyquire": "^1.0.1", + "strip-ansi": "^3.0.1", + "supertest": "^1.1.0" + }, + "engines": { + "node": "^6.2.2", + "npm": "^3.9.5" + }, + "scripts": { + "test": "gulp test", + "update-webdriver": "node node_modules/protractor/bin/webdriver-manager update", + "start": "node server" + }, + "private": true +} diff --git a/protractor.conf.js b/protractor.conf.js new file mode 100644 index 0000000..a9669d9 --- /dev/null +++ b/protractor.conf.js @@ -0,0 +1,81 @@ +// Protractor configuration +// https://github.com/angular/protractor/blob/master/referenceConf.js + +'use strict'; + +var config = { + // The timeout for each script run on the browser. This should be longer + // than the maximum time your application needs to stabilize between tasks. + allScriptsTimeout: 110000, + + // A base URL for your application under test. Calls to protractor.get() + // with relative paths will be prepended with this. + baseUrl: 'http://localhost:' + (process.env.PORT || '9000'), + + // Credientials for Saucelabs + sauceUser: process.env.SAUCE_USERNAME, + + sauceKey: process.env.SAUCE_ACCESS_KEY, + + // list of files / patterns to load in the browser + specs: [ + 'e2e/**/*.spec.js' + ], + + // Patterns to exclude. + exclude: [], + + // ----- Capabilities to be passed to the webdriver instance ---- + // + // For a full list of available capabilities, see + // https://code.google.com/p/selenium/wiki/DesiredCapabilities + // and + // https://code.google.com/p/selenium/source/browse/javascript/webdriver/capabilities.js + capabilities: { + 'browserName': 'chrome', + 'name': 'Fullstack E2E', + 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER, + 'build': process.env.TRAVIS_BUILD_NUMBER + }, + + // ----- The test framework ----- + // + // Jasmine and Cucumber are fully supported as a test and assertion framework. + // Mocha has limited beta support. You will need to include your own + // assertion framework if working with mocha. + framework: 'mocha', + + // ----- Options to be passed to mocha ----- + mochaOpts: { + reporter: 'spec', + timeout: 30000, + defaultTimeoutInterval: 30000 + }, + + // Prepare environment for tests + params: { + serverConfig: require('./server/config/environment') + }, + + onPrepare: function() { + require('babel-register'); + // Load Mocha and Chai + plugins + require('./mocha.conf'); + + // Expose should assertions (see https://github.com/angular/protractor/issues/633) + Object.defineProperty( + protractor.promise.Promise.prototype, + 'should', + Object.getOwnPropertyDescriptor(Object.prototype, 'should') + ); + + var serverConfig = config.params.serverConfig; + + // Setup mongo for tests + var mongoose = require('mongoose'); + mongoose.connect(serverConfig.mongo.uri, serverConfig.mongo.options); // Connect to database + } +}; + +config.params.baseUrl = config.baseUrl; +exports.config = config; diff --git a/server/.eslintrc b/server/.eslintrc new file mode 100644 index 0000000..849296d --- /dev/null +++ b/server/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": "../.eslintrc", + "env": { + "node": true + } +} diff --git a/server/api/ansible/ansible.controller.js b/server/api/ansible/ansible.controller.js new file mode 100644 index 0000000..074a00b --- /dev/null +++ b/server/api/ansible/ansible.controller.js @@ -0,0 +1,814 @@ +/** + * Using Rails-like standard naming convention for endpoints. + * GET /api/ansible -> index + * POST /api/ansible -> create + * GET /api/ansible/:id -> show + * PUT /api/ansible/:id -> upsert + * PATCH /api/ansible/:id -> patch + * DELETE /api/ansible/:id -> destroy + */ + +'use strict'; + +import jsonpatch from 'fast-json-patch'; +import Ansible from './ansible.model'; +var ssh2_exec = require('../../components/ssh/ssh2_exec'); +var ansibleTool = require('../../components/ansible/ansible_tool'); + + +function respondWithResult(res, statusCode) { + statusCode = statusCode || 200; + return function(entity) { + if(entity) { + return res.status(statusCode).json(entity); + } + return null; + }; +} + +function patchUpdates(patches) { + return function(entity) { + try { + // eslint-disable-next-line prefer-reflect + jsonpatch.apply(entity, patches, /*validate*/ true); + } catch(err) { + return Promise.reject(err); + } + + return entity.save(); + }; +} + +function removeEntity(res) { + return function(entity) { + if(entity) { + return entity.remove() + .then(() => { + res.status(204).end(); + }); + } + }; +} + +function handleEntityNotFound(res) { + return function(entity) { + if(!entity) { + res.status(404).end(); + return null; + } + return entity; + }; +} + +function handleError(res, statusCode) { + statusCode = statusCode || 500; + return function(err) { + res.status(statusCode).send(err); + }; +} + +// Creates a new Ansible in the DB +export function command(req, res) { + + var command = req.body.command; + + var ansibleEngine = req.body.ansibleEngine; + + ssh2_exec.executeCommand(command, + null, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ) +} + +// Creates a new Ansible in the DB +export function modules(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + + ansibleTool.getModules(function(data){ + res.write(data) + }, + function(data){ + res.write(data); + res.end() + }, + function(data){ + res.write(data) + }, + ansibleEngine + ); + +} + +// Gets a single Deploy from the DB +export function getLogs(req, res) { + return Ansible.findById(req.params.id).exec() + .then(handleEntityNotFound(res)) + .then(function(entity){ + console.log("Getting logs " + entity.logfile); + ansibleTool.getLogs(entity.logfile, + function(successData){ + return res.send(successData); + }, + function(errorData){ + return res.status(500).send(errorData) + } + ); + return null; + }) + .catch(handleError(res)); +} + +// Executes Ansible Play book in the backend +export function execute(req, res) { + + //var inventory_file_contents = req.body.inventory_file_contents; + //var playbook_file_contents = req.body.playbook_file_contents; + + var playbook_name = req.body.selectedPlaybook; + var inventory_file_name = req.body.inventory_file_name; + + var tags = req.body.tags; + var limit_to_hosts = req.body.limit_to_hosts; + var verbose = req.body.verbose; + var check_mode = req.body.check_mode; + + var ansibleEngine = req.body.ansibleEngine; + var project_folder = ansibleEngine.projectFolder; + + console.log("Check_Mode=" + check_mode); + + var time = new Date().getTime(); + var logfilename = 'execution_' + time; + + var tags_joined = tags; + if(typeof tags === 'object')tags_joined = tags.join(','); + + var limit_to_hosts_joined = limit_to_hosts; + if(typeof limit_to_hosts === 'object')limit_to_hosts_joined = limit_to_hosts.join(','); + + var ansibleObject = { + logfile: logfilename, + tags: tags_joined, + limit_to_hosts: limit_to_hosts, + verbose: verbose, + host: req.body.host, + check_mode: check_mode, + selectedPlaybook: req.body.selectedPlaybook, + selectedPlay: req.body.selectedPlay, + executionType: req.body.executionType, + executionName: req.body.executionName, + executionTime: time + }; + + var resultSent = false; + + ansibleTool.executeAnsible(logfilename, project_folder, playbook_name, inventory_file_name, tags_joined, limit_to_hosts_joined, verbose,check_mode, + function(data){ + //res.write(data) + if(!resultSent){ + resultSent = true; + return Ansible.create(ansibleObject) + .then(respondWithResult(res, 201)) + .catch(handleError(res)); + } + }, + function(data){ + //res.write(data); + //res.end() + if(!resultSent){ + resultSent = true; + return Ansible.create(ansibleObject) + .then(respondWithResult(res, 201)) + .catch(handleError(res)); + } + }, + function(data){ + //res.write(data) + if(!resultSent){ + resultSent = true; + res.status(500).send(data) + } + }, + ansibleEngine + ); + +} + + +/** + * List playbook tags + * ansible-playbook playbook.yml -i inventory --list-tags + * @param req + * @param res + */ +export function playbook_tags_list(req, res) { + + var playbook_name = req.body.selectedPlaybook; + var inventory_file_name = req.body.inventory_file_name; + + var ansibleEngine = req.body.ansibleEngine; + var project_folder = ansibleEngine.projectFolder; + + ansibleTool.getTagList(project_folder, playbook_name, inventory_file_name, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + + +export function playbook_create(req, res) { + + var playbook_file_contents = req.body.playbookFileContents; + var ansibleEngine = req.body.ansibleEngine; + var play_book_name = req.body.playbookName; + var project_folder = ansibleEngine.projectFolder; + + play_book_name = play_book_name.replace(project_folder,''); + console.log("Playbook name = " + play_book_name); + + var resultSent = false; + ansibleTool.writePlaybook(project_folder,play_book_name,playbook_file_contents, + function(data){ + //res.write(data); + //res.end() + if(!resultSent){ + resultSent = true; + res.send(data) + } + }, + function(data){ + //res.write(data) + if(!resultSent){ + resultSent = true; + res.status(500).send(data) + } + }, + ansibleEngine + ); + +} + +export function playbook_delete(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var play_book_name = req.body.playbookName; + var project_folder = ansibleEngine.projectFolder; + + var resultSent = false; + ansibleTool.deletePlaybook(project_folder,play_book_name, + function(data){ + res.write(data) + }, + function(data){ + if(!resultSent){ + resultSent = true; + res.write(data); + res.end(); + } + }, + function(data){ + if(!resultSent){ + resultSent = true; + res.status(500); + res.write(data); + res.end(); + } + }, + ansibleEngine + ); + +} + +export function playbook_get(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var play_book_name = req.body.playbookName; + var project_folder = ansibleEngine.projectFolder; + + var resultSent = false; + ansibleTool.readPlaybook(project_folder,play_book_name, + function(data){ + res.write(data) + }, + function(data){ + if(!resultSent){ + resultSent = true; + res.write(data); + res.end(); + } + }, + function(data){ + if(!resultSent){ + resultSent = true; + res.status(500); + res.write(data); + res.end(); + } + }, + ansibleEngine + ); + +} + + +export function playbook_list(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var project_folder = ansibleEngine.projectFolder; + + ansibleTool.getPlaybookList(project_folder, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + + +export function roles_list(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var project_folder = ansibleEngine.projectFolder; + + ansibleTool.getRolesList(project_folder, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + +export function inventory_list(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var project_folder = ansibleEngine.projectFolder; + + ansibleTool.getInventoryList(project_folder, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + +export function inventory_get(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var project_folder = ansibleEngine.projectFolder; + var inventoryName = req.body.inventoryName; + + ansibleTool.readInventoryFile(project_folder,inventoryName, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + + +export function inventory_create(req, res) { + + var inventoryFileContents = req.body.inventoryFileContents; + var ansibleEngine = req.body.ansibleEngine; + var inventoryName = req.body.inventoryName; + var project_folder = ansibleEngine.projectFolder; + + var file_path = project_folder + '/' + inventoryName; + + ansibleTool.writeFile(file_path,inventoryFileContents, + function(data){ + res.send(data); + }, + function(data){ + res.status(500).send(data) + }, + ansibleEngine + ); + +} + +export function inventory_delete(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var inventoryName = req.body.inventoryName; + var project_folder = ansibleEngine.projectFolder; + + var file_path = project_folder + '/' + inventoryName; + + ansibleTool.deleteFile(file_path, + function(data){ + res.send(data); + }, + function(data){ + res.status(500).send(data) + }, + ansibleEngine + ); + +} + +export function update_groups_vars_file(req, res) { + + var groupVarsContents = req.body.groupVarsContents; + var ansibleEngine = req.body.ansibleEngine; + var groupName = req.body.groupName; + var project_folder = ansibleEngine.projectFolder; + + var file_path = project_folder + '/group_vars/' + groupName; + + ansibleTool.writeFile(file_path, groupVarsContents, + function(data){ + res.send(data); + }, + function(data){ + res.status(500).send(data) + }, + ansibleEngine + ); + +} + +export function get_groups_vars_file(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var groupName = req.body.groupName; + var project_folder = ansibleEngine.projectFolder; + + var file_path = project_folder + '/group_vars/' + groupName; + + ansibleTool.readFile(file_path, + null, + function(data){ + res.send(data); + }, + function(data){ + res.status(500).send(data) + }, + ansibleEngine + ); + +} + + +export function update_hosts_vars_file(req, res) { + + var hostVarsContents = req.body.hostVarsContents; + var ansibleEngine = req.body.ansibleEngine; + var hostName = req.body.hostName; + var project_folder = ansibleEngine.projectFolder; + + var file_path = project_folder + '/host_vars/' + hostName; + + ansibleTool.writeFile(file_path, hostVarsContents, + function(data){ + res.send(data); + }, + function(data){ + res.status(500).send(data) + }, + ansibleEngine + ); + +} + +export function get_hosts_vars_file(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var hostName = req.body.hostName; + var project_folder = ansibleEngine.projectFolder; + + var file_path = project_folder + '/host_vars/' + hostName; + + ansibleTool.readFile(file_path, + null, + function(data){ + res.send(data); + }, + function(data){ + res.status(500).send(data) + }, + ansibleEngine + ); + +} + + +/** + * Get variables for a host using Python AnsibleAPI + * @param req + * @param res + */ +export function get_hosts_vars(req,res){ + + var ansibleEngine = req.body.ansibleEngine; + var host_name = req.body.hostName; + var project_folder = ansibleEngine.projectFolder; + var inventory_file_name = req.body.inventoryFileName; + + console.log('hostName=' + host_name) + + ansibleTool.getVars(project_folder,inventory_file_name,host_name, + null, + function(data){ + res.send(data); + }, + function(data){ + res.status(500).send(data) + }, + ansibleEngine) + +} + + +/** + * Get variables for a role using Python AnsibleAPI + * @param req + * @param res + */ +export function get_roles_vars(req,res){ + + var ansibleEngine = req.body.ansibleEngine; + var role_name = req.body.roleName; + var project_folder = ansibleEngine.projectFolder; + + console.log('roleName=' + role_name); + + ansibleTool.getRolesVars(project_folder,role_name, + null, + function(data){ + res.send(data); + }, + function(data){ + res.status(500).send(data) + + }, + ansibleEngine) + +} + +export function roles_search_galaxy(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var searchText = req.body.searchText; + + ansibleTool.searchRolesGalaxy(searchText, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + +export function roles_search_github(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var searchText = req.body.searchText; + + ansibleTool.searchRolesGithub(searchText, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + +/** + * Create/Copy Role + * Create a new role if selectedRoleName is null + * Copy existing role if selectedRoleName is not null + * @param req + * @param res + */ +export function roles_create(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var roleName = req.body.roleName; + var selectedRoleName = req.body.selectedRoleName; + + var createRoleFunction = ansibleTool.createRole; + + if(selectedRoleName) + createRoleFunction = ansibleTool.copyRole; + + createRoleFunction(roleName, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine, + selectedRoleName + ); + +} + +export function roles_import(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var roleType = req.body.roleType; + var roleNameUri = req.body.roleNameUri; + + ansibleTool.importRole(roleType,roleNameUri, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + +export function roles_delete(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var roleName = req.body.roleName; + + ansibleTool.deleteRole(roleName, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + +export function roles_files(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var roleName = req.body.roleName; + + ansibleTool.getRoleFiles(roleName, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + +export function project_files(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + + ansibleTool.getProjectFiles( + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + +export function file_create(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var fileAbsolutePath = req.body.fileAbsolutePath; + + ansibleTool.createFile(fileAbsolutePath, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + +export function file_update(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var fileAbsolutePath = req.body.fileAbsolutePath; + var fileContents = req.body.fileContents; + + ansibleTool.writeFile(fileAbsolutePath,fileContents, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + +export function file_delete(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + var fileAbsolutePath = req.body.fileAbsolutePath; + + ansibleTool.deleteFile(fileAbsolutePath, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data); + }, + ansibleEngine + ); + +} + +// Gets a list of Ansibles +export function index(req, res) { + return Ansible.find().exec() + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Gets a single Ansible from the DB +export function show(req, res) { + return Ansible.findById(req.params.id).exec() + .then(handleEntityNotFound(res)) + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Creates a new Ansible in the DB +export function create(req, res) { + return Ansible.create(req.body) + .then(respondWithResult(res, 201)) + .catch(handleError(res)); +} + +// Upserts the given Ansible in the DB at the specified ID +export function upsert(req, res) { + if(req.body._id) { + Reflect.deleteProperty(req.body, '_id'); + } + return Ansible.findOneAndUpdate({_id: req.params.id}, req.body, {new: true, upsert: true, setDefaultsOnInsert: true, runValidators: true}).exec() + + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Updates an existing Ansible in the DB +export function patch(req, res) { + if(req.body._id) { + Reflect.deleteProperty(req.body, '_id'); + } + return Ansible.findById(req.params.id).exec() + .then(handleEntityNotFound(res)) + .then(patchUpdates(req.body)) + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Deletes a Ansible from the DB +export function destroy(req, res) { + return Ansible.findById(req.params.id).exec() + .then(handleEntityNotFound(res)) + .then(removeEntity(res)) + .catch(handleError(res)); +} diff --git a/server/api/ansible/ansible.events.js b/server/api/ansible/ansible.events.js new file mode 100644 index 0000000..5337401 --- /dev/null +++ b/server/api/ansible/ansible.events.js @@ -0,0 +1,35 @@ +/** + * Ansible model events + */ + +'use strict'; + +import {EventEmitter} from 'events'; +var AnsibleEvents = new EventEmitter(); + +// Set max event listeners (0 == unlimited) +AnsibleEvents.setMaxListeners(0); + +// Model events +var events = { + save: 'save', + remove: 'remove' +}; + +// Register the event emitter to the model events +function registerEvents(Ansible) { + for(var e in events) { + let event = events[e]; + Ansible.post(e, emitEvent(event)); + } +} + +function emitEvent(event) { + return function(doc) { + AnsibleEvents.emit(event + ':' + doc._id, doc); + AnsibleEvents.emit(event, doc); + }; +} + +export {registerEvents}; +export default AnsibleEvents; diff --git a/server/api/ansible/ansible.integration.js b/server/api/ansible/ansible.integration.js new file mode 100644 index 0000000..71105f6 --- /dev/null +++ b/server/api/ansible/ansible.integration.js @@ -0,0 +1,190 @@ +'use strict'; + +/* globals describe, expect, it, beforeEach, afterEach */ + +var app = require('../..'); +import request from 'supertest'; + +var newAnsible; + +describe('Ansible API:', function() { + describe('GET /api/ansible', function() { + var ansibles; + + beforeEach(function(done) { + request(app) + .get('/api/ansible') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + ansibles = res.body; + done(); + }); + }); + + it('should respond with JSON array', function() { + expect(ansibles).to.be.instanceOf(Array); + }); + }); + + describe('POST /api/ansible', function() { + beforeEach(function(done) { + request(app) + .post('/api/ansible') + .send({ + name: 'New Ansible', + info: 'This is the brand new ansible!!!' + }) + .expect(201) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + newAnsible = res.body; + done(); + }); + }); + + it('should respond with the newly created ansible', function() { + expect(newAnsible.name).to.equal('New Ansible'); + expect(newAnsible.info).to.equal('This is the brand new ansible!!!'); + }); + }); + + describe('GET /api/ansible/:id', function() { + var ansible; + + beforeEach(function(done) { + request(app) + .get(`/api/ansible/${newAnsible._id}`) + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + ansible = res.body; + done(); + }); + }); + + afterEach(function() { + ansible = {}; + }); + + it('should respond with the requested ansible', function() { + expect(ansible.name).to.equal('New Ansible'); + expect(ansible.info).to.equal('This is the brand new ansible!!!'); + }); + }); + + describe('PUT /api/ansible/:id', function() { + var updatedAnsible; + + beforeEach(function(done) { + request(app) + .put(`/api/ansible/${newAnsible._id}`) + .send({ + name: 'Updated Ansible', + info: 'This is the updated ansible!!!' + }) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if(err) { + return done(err); + } + updatedAnsible = res.body; + done(); + }); + }); + + afterEach(function() { + updatedAnsible = {}; + }); + + it('should respond with the updated ansible', function() { + expect(updatedAnsible.name).to.equal('Updated Ansible'); + expect(updatedAnsible.info).to.equal('This is the updated ansible!!!'); + }); + + it('should respond with the updated ansible on a subsequent GET', function(done) { + request(app) + .get(`/api/ansible/${newAnsible._id}`) + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + let ansible = res.body; + + expect(ansible.name).to.equal('Updated Ansible'); + expect(ansible.info).to.equal('This is the updated ansible!!!'); + + done(); + }); + }); + }); + + describe('PATCH /api/ansible/:id', function() { + var patchedAnsible; + + beforeEach(function(done) { + request(app) + .patch(`/api/ansible/${newAnsible._id}`) + .send([ + { op: 'replace', path: '/name', value: 'Patched Ansible' }, + { op: 'replace', path: '/info', value: 'This is the patched ansible!!!' } + ]) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if(err) { + return done(err); + } + patchedAnsible = res.body; + done(); + }); + }); + + afterEach(function() { + patchedAnsible = {}; + }); + + it('should respond with the patched ansible', function() { + expect(patchedAnsible.name).to.equal('Patched Ansible'); + expect(patchedAnsible.info).to.equal('This is the patched ansible!!!'); + }); + }); + + describe('DELETE /api/ansible/:id', function() { + it('should respond with 204 on successful removal', function(done) { + request(app) + .delete(`/api/ansible/${newAnsible._id}`) + .expect(204) + .end(err => { + if(err) { + return done(err); + } + done(); + }); + }); + + it('should respond with 404 when ansible does not exist', function(done) { + request(app) + .delete(`/api/ansible/${newAnsible._id}`) + .expect(404) + .end(err => { + if(err) { + return done(err); + } + done(); + }); + }); + }); +}); diff --git a/server/api/ansible/ansible.model.js b/server/api/ansible/ansible.model.js new file mode 100644 index 0000000..1d0c82d --- /dev/null +++ b/server/api/ansible/ansible.model.js @@ -0,0 +1,24 @@ +'use strict'; + +import mongoose from 'mongoose'; +import {registerEvents} from './ansible.events'; + +var AnsibleSchema = new mongoose.Schema({ + name: String, + info: String, + active: Boolean, + logfile: String, + tags: String, + limit_to_hosts: String, + host: String, + verbose: String, + check_mode: Boolean, + selectedPlaybook: String, + selectedPlay: String, + executionType: String, + executionName: String, + executionTime: Date +}); + +registerEvents(AnsibleSchema); +export default mongoose.model('Ansible', AnsibleSchema); diff --git a/server/api/ansible/index.js b/server/api/ansible/index.js new file mode 100644 index 0000000..bf06fc3 --- /dev/null +++ b/server/api/ansible/index.js @@ -0,0 +1,59 @@ +'use strict'; + +var express = require('express'); +var controller = require('./ansible.controller'); + +var router = express.Router(); + +router.get('/', controller.index); + +router.post('/modules', controller.modules); +router.post('/command', controller.command); +router.post('/execute', controller.execute); + +router.post('/project/files', controller.project_files); + +router.post('/playbook/get', controller.playbook_get); +router.post('/playbook/create', controller.playbook_create); +router.post('/playbook/delete', controller.playbook_delete); +router.post('/playbook/list', controller.playbook_list); + + +router.post('/roles/create', controller.roles_create); +router.post('/roles/list', controller.roles_list); +router.post('/roles/search/galaxy', controller.roles_search_galaxy); +router.post('/roles/search/github', controller.roles_search_github); +router.post('/roles/delete', controller.roles_delete); +router.post('/roles/files', controller.roles_files); +router.post('/roles/import', controller.roles_import); + +router.post('/tags/list', controller.playbook_tags_list); + +router.post('/files/create', controller.file_create); +router.post('/files/update', controller.file_update); +router.post('/files/delete', controller.file_delete); + + +router.post('/inventory/list', controller.inventory_list); +router.post('/inventory/get', controller.inventory_get); +router.post('/inventory/create', controller.inventory_create); +router.post('/inventory/delete', controller.inventory_delete); + +router.post('/vars_file/groups/update', controller.update_groups_vars_file); +router.post('/vars_file/groups/get', controller.get_groups_vars_file); + +router.post('/vars_file/hosts/update', controller.update_hosts_vars_file); +router.post('/vars_file/hosts/get', controller.get_hosts_vars_file); + +router.post('/vars/hosts/get', controller.get_hosts_vars); +router.post('/vars/roles/get', controller.get_roles_vars); + +router.get('/logs/:id', controller.getLogs); + +router.get('/:id', controller.show); +router.post('/', controller.create); +router.put('/:id', controller.upsert); +router.patch('/:id', controller.patch); +router.delete('/:id', controller.destroy); + +module.exports = router; diff --git a/server/api/ansible/index.spec.js b/server/api/ansible/index.spec.js new file mode 100644 index 0000000..86817fc --- /dev/null +++ b/server/api/ansible/index.spec.js @@ -0,0 +1,86 @@ +'use strict'; + +/* globals sinon, describe, expect, it */ + +var proxyquire = require('proxyquire').noPreserveCache(); + +var ansibleCtrlStub = { + index: 'ansibleCtrl.index', + show: 'ansibleCtrl.show', + create: 'ansibleCtrl.create', + upsert: 'ansibleCtrl.upsert', + patch: 'ansibleCtrl.patch', + destroy: 'ansibleCtrl.destroy' +}; + +var routerStub = { + get: sinon.spy(), + put: sinon.spy(), + patch: sinon.spy(), + post: sinon.spy(), + delete: sinon.spy() +}; + +// require the index with our stubbed out modules +var ansibleIndex = proxyquire('./index.js', { + express: { + Router() { + return routerStub; + } + }, + './ansible.controller': ansibleCtrlStub +}); + +describe('Ansible API Router:', function() { + it('should return an express router instance', function() { + expect(ansibleIndex).to.equal(routerStub); + }); + + describe('GET /api/ansible', function() { + it('should route to ansible.controller.index', function() { + expect(routerStub.get + .withArgs('/', 'ansibleCtrl.index') + ).to.have.been.calledOnce; + }); + }); + + describe('GET /api/ansible/:id', function() { + it('should route to ansible.controller.show', function() { + expect(routerStub.get + .withArgs('/:id', 'ansibleCtrl.show') + ).to.have.been.calledOnce; + }); + }); + + describe('POST /api/ansible', function() { + it('should route to ansible.controller.create', function() { + expect(routerStub.post + .withArgs('/', 'ansibleCtrl.create') + ).to.have.been.calledOnce; + }); + }); + + describe('PUT /api/ansible/:id', function() { + it('should route to ansible.controller.upsert', function() { + expect(routerStub.put + .withArgs('/:id', 'ansibleCtrl.upsert') + ).to.have.been.calledOnce; + }); + }); + + describe('PATCH /api/ansible/:id', function() { + it('should route to ansible.controller.patch', function() { + expect(routerStub.patch + .withArgs('/:id', 'ansibleCtrl.patch') + ).to.have.been.calledOnce; + }); + }); + + describe('DELETE /api/ansible/:id', function() { + it('should route to ansible.controller.destroy', function() { + expect(routerStub.delete + .withArgs('/:id', 'ansibleCtrl.destroy') + ).to.have.been.calledOnce; + }); + }); +}); diff --git a/server/api/custom_module/custom_module.controller.js b/server/api/custom_module/custom_module.controller.js new file mode 100644 index 0000000..d4019f9 --- /dev/null +++ b/server/api/custom_module/custom_module.controller.js @@ -0,0 +1,220 @@ +/** + * Using Rails-like standard naming convention for endpoints. + * GET /api/custom_modules -> index + * POST /api/custom_modules -> create + * GET /api/custom_modules/:id -> show + * PUT /api/custom_modules/:id -> upsert + * PATCH /api/custom_modules/:id -> patch + * DELETE /api/custom_modules/:id -> destroy + */ + +'use strict'; + +import jsonpatch from 'fast-json-patch'; +import CustomModule from './custom_module.model'; +var ssh2_exec = require('../../components/ssh/ssh2_exec'); +var scp2_exec = require('../../components/scp/scp_exec'); + +function respondWithResult(res, statusCode) { + statusCode = statusCode || 200; + return function(entity) { + if(entity) { + return res.status(statusCode).json(entity); + } + return null; + }; +} + +function patchUpdates(patches) { + return function(entity) { + try { + // eslint-disable-next-line prefer-reflect + jsonpatch.apply(entity, patches, /*validate*/ true); + } catch(err) { + return Promise.reject(err); + } + + return entity.save(); + }; +} + +function removeEntity(res) { + return function(entity) { + if(entity) { + return entity.remove() + .then(() => { + res.status(204).end(); + }); + } + }; +} + +function handleEntityNotFound(res) { + return function(entity) { + if(!entity) { + res.status(404).end(); + return null; + } + return entity; + }; +} + +function handleError(res, statusCode) { + statusCode = statusCode || 500; + return function(err) { + res.status(statusCode).send(err); + }; +} + +// Gets a list of CustomModules +export function index(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + + if(!ansibleEngine.customModules){ + return res.status(500).send("Custom Modules Folder not defined in Ansible Engine") + } + + var command = 'ls "' + ansibleEngine.customModules + '"'; + + ssh2_exec.executeCommand(command, + null, + function(data){ + res.send(data) + }, + function(data){ + res.status(500).send(data) + }, + ansibleEngine + ); + + /*return CustomModule.find().exec() + .then(respondWithResult(res)) + .catch(handleError(res));*/ +} + +// Gets a single CustomModule from the DB +export function show(req, res) { + console.log("Show " + req.params.custom_module); + var ansibleEngine = req.body.ansibleEngine; + + if(!ansibleEngine.customModules){ + res.status(500).send("Custom Modules Folder not defined in Ansible Engine") + } + + var command = 'cat "' + ansibleEngine.customModules + '"/' + req.params.custom_module; + + if(req.params.custom_module === 'template.py'){ + command = 'cat ' + '/opt/ehc-builder-scripts/ansible_modules/template.py'; + } + + + ssh2_exec.executeCommand(command, + null, + function(data){ + res.send(data); + }, + function(data){ + res.status(500).send(data) + }, + ansibleEngine + ); + + /*return CustomModule.findById(req.params.custom_module).exec() + .then(handleEntityNotFound(res)) + .then(respondWithResult(res)) + .catch(handleError(res));*/ +} + +// Test Module +export function testModule(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + + var moduleArgs = req.body.moduleArgs; + + if(!ansibleEngine.customModules){ + res.status(500).send("Custom Modules Folder not defined in Ansible Engine") + } + + var command = '/opt/ansible/ansible-devel/hacking/test-module -m "' + ansibleEngine.customModules + '/' + req.params.custom_module + "\" -a '" + JSON.stringify(moduleArgs) + "'"; + + console.log("Command=" + command); + + ssh2_exec.executeCommand(command, + null, + function(data){ + res.send(data); + }, + function(data){ + res.status(500).send(data) + }, + ansibleEngine + ); + + /*return CustomModule.findById(req.params.custom_module).exec() + .then(handleEntityNotFound(res)) + .then(respondWithResult(res)) + .catch(handleError(res));*/ +} + +// Creates a new CustomModule in the DB +export function create(req, res) { + + console.log("Create"); + + var custom_module_name = req.params.custom_module; + var custom_module_code = req.body.custom_module_code; + + var ansibleEngine = req.body.ansibleEngine; + + if(!ansibleEngine.customModules){ + res.status(500).send("Custom Modules Folder not defined in Ansible Engine") + } + + console.log("Custom module name " + "\"" + ansibleEngine.customModules + '/' + custom_module_name + "\"") + + scp2_exec.createFileOnScriptEngine(custom_module_code,ansibleEngine.customModules + '/' + custom_module_name, + function(){ + res.send("Saved") + },function(err){ + res.status(500).send("Failed to create file on target") + }, + ansibleEngine + ); + + /*return CustomModule.create(req.body) + .then(respondWithResult(res, 201)) + .catch(handleError(res));*/ +} + +// Upserts the given CustomModule in the DB at the specified ID +export function upsert(req, res) { + if(req.body._id) { + Reflect.deleteProperty(req.body, '_id'); + } + return CustomModule.findOneAndUpdate({_id: req.params.id}, req.body, {new: true, upsert: true, setDefaultsOnInsert: true, runValidators: true}).exec() + + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Updates an existing CustomModule in the DB +export function patch(req, res) { + if(req.body._id) { + Reflect.deleteProperty(req.body, '_id'); + } + return CustomModule.findById(req.params.id).exec() + .then(handleEntityNotFound(res)) + .then(patchUpdates(req.body)) + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Deletes a CustomModule from the DB +export function destroy(req, res) { + return CustomModule.findById(req.params.id).exec() + .then(handleEntityNotFound(res)) + .then(removeEntity(res)) + .catch(handleError(res)); +} diff --git a/server/api/custom_module/custom_module.events.js b/server/api/custom_module/custom_module.events.js new file mode 100644 index 0000000..dda994b --- /dev/null +++ b/server/api/custom_module/custom_module.events.js @@ -0,0 +1,35 @@ +/** + * CustomModule model events + */ + +'use strict'; + +import {EventEmitter} from 'events'; +var CustomModuleEvents = new EventEmitter(); + +// Set max event listeners (0 == unlimited) +CustomModuleEvents.setMaxListeners(0); + +// Model events +var events = { + save: 'save', + remove: 'remove' +}; + +// Register the event emitter to the model events +function registerEvents(CustomModule) { + for(var e in events) { + let event = events[e]; + CustomModule.post(e, emitEvent(event)); + } +} + +function emitEvent(event) { + return function(doc) { + CustomModuleEvents.emit(event + ':' + doc._id, doc); + CustomModuleEvents.emit(event, doc); + }; +} + +export {registerEvents}; +export default CustomModuleEvents; diff --git a/server/api/custom_module/custom_module.integration.js b/server/api/custom_module/custom_module.integration.js new file mode 100644 index 0000000..5454c26 --- /dev/null +++ b/server/api/custom_module/custom_module.integration.js @@ -0,0 +1,190 @@ +'use strict'; + +/* globals describe, expect, it, beforeEach, afterEach */ + +var app = require('../..'); +import request from 'supertest'; + +var newCustomModule; + +describe('CustomModule API:', function() { + describe('GET /api/custom_modules', function() { + var customModules; + + beforeEach(function(done) { + request(app) + .get('/api/custom_modules') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + customModules = res.body; + done(); + }); + }); + + it('should respond with JSON array', function() { + expect(customModules).to.be.instanceOf(Array); + }); + }); + + describe('POST /api/custom_modules', function() { + beforeEach(function(done) { + request(app) + .post('/api/custom_modules') + .send({ + name: 'New CustomModule', + info: 'This is the brand new customModule!!!' + }) + .expect(201) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + newCustomModule = res.body; + done(); + }); + }); + + it('should respond with the newly created customModule', function() { + expect(newCustomModule.name).to.equal('New CustomModule'); + expect(newCustomModule.info).to.equal('This is the brand new customModule!!!'); + }); + }); + + describe('GET /api/custom_modules/:id', function() { + var customModule; + + beforeEach(function(done) { + request(app) + .get(`/api/custom_modules/${newCustomModule._id}`) + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + customModule = res.body; + done(); + }); + }); + + afterEach(function() { + customModule = {}; + }); + + it('should respond with the requested customModule', function() { + expect(customModule.name).to.equal('New CustomModule'); + expect(customModule.info).to.equal('This is the brand new customModule!!!'); + }); + }); + + describe('PUT /api/custom_modules/:id', function() { + var updatedCustomModule; + + beforeEach(function(done) { + request(app) + .put(`/api/custom_modules/${newCustomModule._id}`) + .send({ + name: 'Updated CustomModule', + info: 'This is the updated customModule!!!' + }) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if(err) { + return done(err); + } + updatedCustomModule = res.body; + done(); + }); + }); + + afterEach(function() { + updatedCustomModule = {}; + }); + + it('should respond with the updated customModule', function() { + expect(updatedCustomModule.name).to.equal('Updated CustomModule'); + expect(updatedCustomModule.info).to.equal('This is the updated customModule!!!'); + }); + + it('should respond with the updated customModule on a subsequent GET', function(done) { + request(app) + .get(`/api/custom_modules/${newCustomModule._id}`) + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + let customModule = res.body; + + expect(customModule.name).to.equal('Updated CustomModule'); + expect(customModule.info).to.equal('This is the updated customModule!!!'); + + done(); + }); + }); + }); + + describe('PATCH /api/custom_modules/:id', function() { + var patchedCustomModule; + + beforeEach(function(done) { + request(app) + .patch(`/api/custom_modules/${newCustomModule._id}`) + .send([ + { op: 'replace', path: '/name', value: 'Patched CustomModule' }, + { op: 'replace', path: '/info', value: 'This is the patched customModule!!!' } + ]) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if(err) { + return done(err); + } + patchedCustomModule = res.body; + done(); + }); + }); + + afterEach(function() { + patchedCustomModule = {}; + }); + + it('should respond with the patched customModule', function() { + expect(patchedCustomModule.name).to.equal('Patched CustomModule'); + expect(patchedCustomModule.info).to.equal('This is the patched customModule!!!'); + }); + }); + + describe('DELETE /api/custom_modules/:id', function() { + it('should respond with 204 on successful removal', function(done) { + request(app) + .delete(`/api/custom_modules/${newCustomModule._id}`) + .expect(204) + .end(err => { + if(err) { + return done(err); + } + done(); + }); + }); + + it('should respond with 404 when customModule does not exist', function(done) { + request(app) + .delete(`/api/custom_modules/${newCustomModule._id}`) + .expect(404) + .end(err => { + if(err) { + return done(err); + } + done(); + }); + }); + }); +}); diff --git a/server/api/custom_module/custom_module.model.js b/server/api/custom_module/custom_module.model.js new file mode 100644 index 0000000..1f18055 --- /dev/null +++ b/server/api/custom_module/custom_module.model.js @@ -0,0 +1,13 @@ +'use strict'; + +import mongoose from 'mongoose'; +import {registerEvents} from './custom_module.events'; + +var CustomModuleSchema = new mongoose.Schema({ + name: String, + info: String, + active: Boolean +}); + +registerEvents(CustomModuleSchema); +export default mongoose.model('CustomModule', CustomModuleSchema); diff --git a/server/api/custom_module/index.js b/server/api/custom_module/index.js new file mode 100644 index 0000000..6bdc2d1 --- /dev/null +++ b/server/api/custom_module/index.js @@ -0,0 +1,16 @@ +'use strict'; + +var express = require('express'); +var controller = require('./custom_module.controller'); + +var router = express.Router(); + +router.post('/query', controller.index); +router.post('/:custom_module/test', controller.testModule); +router.post('/:custom_module/get', controller.show); +router.post('/:custom_module', controller.create); +router.put('/:id', controller.upsert); +router.patch('/:id', controller.patch); +router.delete('/:id', controller.destroy); + +module.exports = router; diff --git a/server/api/custom_module/index.spec.js b/server/api/custom_module/index.spec.js new file mode 100644 index 0000000..04b1873 --- /dev/null +++ b/server/api/custom_module/index.spec.js @@ -0,0 +1,86 @@ +'use strict'; + +/* globals sinon, describe, expect, it */ + +var proxyquire = require('proxyquire').noPreserveCache(); + +var customModuleCtrlStub = { + index: 'customModuleCtrl.index', + show: 'customModuleCtrl.show', + create: 'customModuleCtrl.create', + upsert: 'customModuleCtrl.upsert', + patch: 'customModuleCtrl.patch', + destroy: 'customModuleCtrl.destroy' +}; + +var routerStub = { + get: sinon.spy(), + put: sinon.spy(), + patch: sinon.spy(), + post: sinon.spy(), + delete: sinon.spy() +}; + +// require the index with our stubbed out modules +var customModuleIndex = proxyquire('./index.js', { + express: { + Router() { + return routerStub; + } + }, + './custom_module.controller': customModuleCtrlStub +}); + +describe('CustomModule API Router:', function() { + it('should return an express router instance', function() { + expect(customModuleIndex).to.equal(routerStub); + }); + + describe('GET /api/custom_modules', function() { + it('should route to customModule.controller.index', function() { + expect(routerStub.get + .withArgs('/', 'customModuleCtrl.index') + ).to.have.been.calledOnce; + }); + }); + + describe('GET /api/custom_modules/:id', function() { + it('should route to customModule.controller.show', function() { + expect(routerStub.get + .withArgs('/:id', 'customModuleCtrl.show') + ).to.have.been.calledOnce; + }); + }); + + describe('POST /api/custom_modules', function() { + it('should route to customModule.controller.create', function() { + expect(routerStub.post + .withArgs('/', 'customModuleCtrl.create') + ).to.have.been.calledOnce; + }); + }); + + describe('PUT /api/custom_modules/:id', function() { + it('should route to customModule.controller.upsert', function() { + expect(routerStub.put + .withArgs('/:id', 'customModuleCtrl.upsert') + ).to.have.been.calledOnce; + }); + }); + + describe('PATCH /api/custom_modules/:id', function() { + it('should route to customModule.controller.patch', function() { + expect(routerStub.patch + .withArgs('/:id', 'customModuleCtrl.patch') + ).to.have.been.calledOnce; + }); + }); + + describe('DELETE /api/custom_modules/:id', function() { + it('should route to customModule.controller.destroy', function() { + expect(routerStub.delete + .withArgs('/:id', 'customModuleCtrl.destroy') + ).to.have.been.calledOnce; + }); + }); +}); diff --git a/server/api/project/index.js b/server/api/project/index.js new file mode 100644 index 0000000..bd6a8bc --- /dev/null +++ b/server/api/project/index.js @@ -0,0 +1,15 @@ +'use strict'; + +var express = require('express'); +var controller = require('./project.controller'); + +var router = express.Router(); + +router.get('/', controller.index); +router.get('/:id', controller.show); +router.post('/', controller.create); +router.put('/:id', controller.upsert); +router.patch('/:id', controller.patch); +router.delete('/:id', controller.destroy); + +module.exports = router; diff --git a/server/api/project/index.spec.js b/server/api/project/index.spec.js new file mode 100644 index 0000000..6801df5 --- /dev/null +++ b/server/api/project/index.spec.js @@ -0,0 +1,86 @@ +'use strict'; + +/* globals sinon, describe, expect, it */ + +var proxyquire = require('proxyquire').noPreserveCache(); + +var projectCtrlStub = { + index: 'projectCtrl.index', + show: 'projectCtrl.show', + create: 'projectCtrl.create', + upsert: 'projectCtrl.upsert', + patch: 'projectCtrl.patch', + destroy: 'projectCtrl.destroy' +}; + +var routerStub = { + get: sinon.spy(), + put: sinon.spy(), + patch: sinon.spy(), + post: sinon.spy(), + delete: sinon.spy() +}; + +// require the index with our stubbed out modules +var projectIndex = proxyquire('./index.js', { + express: { + Router() { + return routerStub; + } + }, + './project.controller': projectCtrlStub +}); + +describe('Project API Router:', function() { + it('should return an express router instance', function() { + expect(projectIndex).to.equal(routerStub); + }); + + describe('GET /api/projects', function() { + it('should route to project.controller.index', function() { + expect(routerStub.get + .withArgs('/', 'projectCtrl.index') + ).to.have.been.calledOnce; + }); + }); + + describe('GET /api/projects/:id', function() { + it('should route to project.controller.show', function() { + expect(routerStub.get + .withArgs('/:id', 'projectCtrl.show') + ).to.have.been.calledOnce; + }); + }); + + describe('POST /api/projects', function() { + it('should route to project.controller.create', function() { + expect(routerStub.post + .withArgs('/', 'projectCtrl.create') + ).to.have.been.calledOnce; + }); + }); + + describe('PUT /api/projects/:id', function() { + it('should route to project.controller.upsert', function() { + expect(routerStub.put + .withArgs('/:id', 'projectCtrl.upsert') + ).to.have.been.calledOnce; + }); + }); + + describe('PATCH /api/projects/:id', function() { + it('should route to project.controller.patch', function() { + expect(routerStub.patch + .withArgs('/:id', 'projectCtrl.patch') + ).to.have.been.calledOnce; + }); + }); + + describe('DELETE /api/projects/:id', function() { + it('should route to project.controller.destroy', function() { + expect(routerStub.delete + .withArgs('/:id', 'projectCtrl.destroy') + ).to.have.been.calledOnce; + }); + }); +}); diff --git a/server/api/project/project.controller.js b/server/api/project/project.controller.js new file mode 100644 index 0000000..45f3eaa --- /dev/null +++ b/server/api/project/project.controller.js @@ -0,0 +1,152 @@ +/** + * Using Rails-like standard naming convention for endpoints. + * GET /api/projects -> index + * POST /api/projects -> create + * GET /api/projects/:id -> show + * PUT /api/projects/:id -> upsert + * PATCH /api/projects/:id -> patch + * DELETE /api/projects/:id -> destroy + */ + +'use strict'; + +import jsonpatch from 'fast-json-patch'; +import Project from './project.model'; +var ansibleTool = require('../../components/ansible/ansible_tool'); + +function respondWithResult(res, statusCode) { + statusCode = statusCode || 200; + return function(entity) { + if(entity) { + return res.status(statusCode).json(entity); + } + return null; + }; +} + +function patchUpdates(patches) { + return function(entity) { + try { + // eslint-disable-next-line prefer-reflect + jsonpatch.apply(entity, patches, /*validate*/ true); + } catch(err) { + return Promise.reject(err); + } + + return entity.save(); + }; +} + +function removeEntity(res) { + return function(entity) { + if(entity) { + return entity.remove() + .then(() => { + res.status(204).end(); + }); + } + }; +} + +function handleEntityNotFound(res) { + return function(entity) { + if(!entity) { + res.status(404).end(); + return null; + } + return entity; + }; +} + +function handleError(res, statusCode) { + statusCode = statusCode || 500; + return function(err) { + res.status(statusCode).send(err); + }; +} + +// Gets a list of Projects +export function index(req, res) { + console.log("Getting projects list"); + return Project.find().exec() + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Gets a single Project from the DB +export function show(req, res) { + return Project.findById(req.params.id).exec() + .then(handleEntityNotFound(res)) + .then(respondWithResult(res)) + .catch(handleError(res)); +} + + +// Creates a new Project in the DB +export function create(req, res) { + + var ansibleEngine = req.body.ansibleEngine; + + console.log("Ansible Engine " + JSON.stringify(ansibleEngine)); + + if(ansibleEngine.ansibleHost){ + ansibleTool.getAnsibleVersion( + function(version){ + + req.body.ansibleVersion = version; + + ansibleTool.createProjectFolder(ansibleEngine.projectFolder, + function(){ + return Project.create(req.body) + .then(respondWithResult(res, 201)) + .catch(handleError(res)); + }, + function(data){ + res.status(500).send(data) + }, ansibleEngine); + + //res.write(data); + //res.end() + }, + function(data){ + res.status(500).send("" + data); + },ansibleEngine + ) + }else{ + return Project.create(req.body) + .then(respondWithResult(res, 201)) + .catch(handleError(res)); + } + +} + +// Upserts the given Project in the DB at the specified ID +export function upsert(req, res) { + if(req.body._id) { + Reflect.deleteProperty(req.body, '_id'); + } + return Project.findOneAndUpdate({_id: req.params.id}, req.body, {new: true, upsert: true, setDefaultsOnInsert: true, runValidators: true}).exec() + + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Updates an existing Project in the DB +export function patch(req, res) { + if(req.body._id) { + Reflect.deleteProperty(req.body, '_id'); + } + return Project.findById(req.params.id).exec() + .then(handleEntityNotFound(res)) + .then(patchUpdates(req.body)) + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Deletes a Project from the DB +export function destroy(req, res) { + return Project.findById(req.params.id).exec() + .then(handleEntityNotFound(res)) + .then(removeEntity(res)) + .catch(handleError(res)); +} diff --git a/server/api/project/project.events.js b/server/api/project/project.events.js new file mode 100644 index 0000000..376ed52 --- /dev/null +++ b/server/api/project/project.events.js @@ -0,0 +1,35 @@ +/** + * Project model events + */ + +'use strict'; + +import {EventEmitter} from 'events'; +var ProjectEvents = new EventEmitter(); + +// Set max event listeners (0 == unlimited) +ProjectEvents.setMaxListeners(0); + +// Model events +var events = { + save: 'save', + remove: 'remove' +}; + +// Register the event emitter to the model events +function registerEvents(Project) { + for(var e in events) { + let event = events[e]; + Project.post(e, emitEvent(event)); + } +} + +function emitEvent(event) { + return function(doc) { + ProjectEvents.emit(event + ':' + doc._id, doc); + ProjectEvents.emit(event, doc); + }; +} + +export {registerEvents}; +export default ProjectEvents; diff --git a/server/api/project/project.integration.js b/server/api/project/project.integration.js new file mode 100644 index 0000000..5616f23 --- /dev/null +++ b/server/api/project/project.integration.js @@ -0,0 +1,190 @@ +'use strict'; + +/* globals describe, expect, it, beforeEach, afterEach */ + +var app = require('../..'); +import request from 'supertest'; + +var newProject; + +describe('Project API:', function() { + describe('GET /api/projects', function() { + var projects; + + beforeEach(function(done) { + request(app) + .get('/api/projects') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + projects = res.body; + done(); + }); + }); + + it('should respond with JSON array', function() { + expect(projects).to.be.instanceOf(Array); + }); + }); + + describe('POST /api/projects', function() { + beforeEach(function(done) { + request(app) + .post('/api/projects') + .send({ + name: 'New Project', + info: 'This is the brand new project!!!' + }) + .expect(201) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + newProject = res.body; + done(); + }); + }); + + it('should respond with the newly created project', function() { + expect(newProject.name).to.equal('New Project'); + expect(newProject.info).to.equal('This is the brand new project!!!'); + }); + }); + + describe('GET /api/projects/:id', function() { + var project; + + beforeEach(function(done) { + request(app) + .get(`/api/projects/${newProject._id}`) + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + project = res.body; + done(); + }); + }); + + afterEach(function() { + project = {}; + }); + + it('should respond with the requested project', function() { + expect(project.name).to.equal('New Project'); + expect(project.info).to.equal('This is the brand new project!!!'); + }); + }); + + describe('PUT /api/projects/:id', function() { + var updatedProject; + + beforeEach(function(done) { + request(app) + .put(`/api/projects/${newProject._id}`) + .send({ + name: 'Updated Project', + info: 'This is the updated project!!!' + }) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if(err) { + return done(err); + } + updatedProject = res.body; + done(); + }); + }); + + afterEach(function() { + updatedProject = {}; + }); + + it('should respond with the updated project', function() { + expect(updatedProject.name).to.equal('Updated Project'); + expect(updatedProject.info).to.equal('This is the updated project!!!'); + }); + + it('should respond with the updated project on a subsequent GET', function(done) { + request(app) + .get(`/api/projects/${newProject._id}`) + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + let project = res.body; + + expect(project.name).to.equal('Updated Project'); + expect(project.info).to.equal('This is the updated project!!!'); + + done(); + }); + }); + }); + + describe('PATCH /api/projects/:id', function() { + var patchedProject; + + beforeEach(function(done) { + request(app) + .patch(`/api/projects/${newProject._id}`) + .send([ + { op: 'replace', path: '/name', value: 'Patched Project' }, + { op: 'replace', path: '/info', value: 'This is the patched project!!!' } + ]) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if(err) { + return done(err); + } + patchedProject = res.body; + done(); + }); + }); + + afterEach(function() { + patchedProject = {}; + }); + + it('should respond with the patched project', function() { + expect(patchedProject.name).to.equal('Patched Project'); + expect(patchedProject.info).to.equal('This is the patched project!!!'); + }); + }); + + describe('DELETE /api/projects/:id', function() { + it('should respond with 204 on successful removal', function(done) { + request(app) + .delete(`/api/projects/${newProject._id}`) + .expect(204) + .end(err => { + if(err) { + return done(err); + } + done(); + }); + }); + + it('should respond with 404 when project does not exist', function(done) { + request(app) + .delete(`/api/projects/${newProject._id}`) + .expect(404) + .end(err => { + if(err) { + return done(err); + } + done(); + }); + }); + }); +}); diff --git a/server/api/project/project.model.js b/server/api/project/project.model.js new file mode 100644 index 0000000..e629e48 --- /dev/null +++ b/server/api/project/project.model.js @@ -0,0 +1,22 @@ +'use strict'; + +import mongoose from 'mongoose'; +import {registerEvents} from './project.events'; + +var ProjectSchema = new mongoose.Schema({ + name: String, + ansibleEngine: {}, + ansibleVersion : String, + creationTime: Date, + info: String, + active: Boolean, + ansible_data: String, //YAML Format + ansible_data_json: {}, //JSON Format + inventory_data: String, //YAML Format + inventory_data_json: {}, //JSON Format + roles_data: String, //YAML Format + roles_data_json: {} //JSON Format +}); + +registerEvents(ProjectSchema); +export default mongoose.model('Project', ProjectSchema); diff --git a/server/api/thing/index.js b/server/api/thing/index.js new file mode 100644 index 0000000..bd1da07 --- /dev/null +++ b/server/api/thing/index.js @@ -0,0 +1,15 @@ +'use strict'; + +var express = require('express'); +var controller = require('./thing.controller'); + +var router = express.Router(); + +router.get('/', controller.index); +router.get('/:id', controller.show); +router.post('/', controller.create); +router.put('/:id', controller.upsert); +router.patch('/:id', controller.patch); +router.delete('/:id', controller.destroy); + +module.exports = router; diff --git a/server/api/thing/index.spec.js b/server/api/thing/index.spec.js new file mode 100644 index 0000000..af463fa --- /dev/null +++ b/server/api/thing/index.spec.js @@ -0,0 +1,86 @@ +'use strict'; + +/* globals sinon, describe, expect, it */ + +var proxyquire = require('proxyquire').noPreserveCache(); + +var thingCtrlStub = { + index: 'thingCtrl.index', + show: 'thingCtrl.show', + create: 'thingCtrl.create', + upsert: 'thingCtrl.upsert', + patch: 'thingCtrl.patch', + destroy: 'thingCtrl.destroy' +}; + +var routerStub = { + get: sinon.spy(), + put: sinon.spy(), + patch: sinon.spy(), + post: sinon.spy(), + delete: sinon.spy() +}; + +// require the index with our stubbed out modules +var thingIndex = proxyquire('./index.js', { + express: { + Router() { + return routerStub; + } + }, + './thing.controller': thingCtrlStub +}); + +describe('Thing API Router:', function() { + it('should return an express router instance', function() { + expect(thingIndex).to.equal(routerStub); + }); + + describe('GET /api/things', function() { + it('should route to thing.controller.index', function() { + expect(routerStub.get + .withArgs('/', 'thingCtrl.index') + ).to.have.been.calledOnce; + }); + }); + + describe('GET /api/things/:id', function() { + it('should route to thing.controller.show', function() { + expect(routerStub.get + .withArgs('/:id', 'thingCtrl.show') + ).to.have.been.calledOnce; + }); + }); + + describe('POST /api/things', function() { + it('should route to thing.controller.create', function() { + expect(routerStub.post + .withArgs('/', 'thingCtrl.create') + ).to.have.been.calledOnce; + }); + }); + + describe('PUT /api/things/:id', function() { + it('should route to thing.controller.upsert', function() { + expect(routerStub.put + .withArgs('/:id', 'thingCtrl.upsert') + ).to.have.been.calledOnce; + }); + }); + + describe('PATCH /api/things/:id', function() { + it('should route to thing.controller.patch', function() { + expect(routerStub.patch + .withArgs('/:id', 'thingCtrl.patch') + ).to.have.been.calledOnce; + }); + }); + + describe('DELETE /api/things/:id', function() { + it('should route to thing.controller.destroy', function() { + expect(routerStub.delete + .withArgs('/:id', 'thingCtrl.destroy') + ).to.have.been.calledOnce; + }); + }); +}); diff --git a/server/api/thing/thing.controller.js b/server/api/thing/thing.controller.js new file mode 100644 index 0000000..f8ddd9c --- /dev/null +++ b/server/api/thing/thing.controller.js @@ -0,0 +1,118 @@ +/** + * Using Rails-like standard naming convention for endpoints. + * GET /api/things -> index + * POST /api/things -> create + * GET /api/things/:id -> show + * PUT /api/things/:id -> upsert + * PATCH /api/things/:id -> patch + * DELETE /api/things/:id -> destroy + */ + +'use strict'; + +import jsonpatch from 'fast-json-patch'; +import Thing from './thing.model'; + +function respondWithResult(res, statusCode) { + statusCode = statusCode || 200; + return function(entity) { + if(entity) { + return res.status(statusCode).json(entity); + } + return null; + }; +} + +function patchUpdates(patches) { + return function(entity) { + try { + // eslint-disable-next-line prefer-reflect + jsonpatch.apply(entity, patches, /*validate*/ true); + } catch(err) { + return Promise.reject(err); + } + + return entity.save(); + }; +} + +function removeEntity(res) { + return function(entity) { + if(entity) { + return entity.remove() + .then(() => { + res.status(204).end(); + }); + } + }; +} + +function handleEntityNotFound(res) { + return function(entity) { + if(!entity) { + res.status(404).end(); + return null; + } + return entity; + }; +} + +function handleError(res, statusCode) { + statusCode = statusCode || 500; + return function(err) { + res.status(statusCode).send(err); + }; +} + +// Gets a list of Things +export function index(req, res) { + return Thing.find().exec() + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Gets a single Thing from the DB +export function show(req, res) { + return Thing.findById(req.params.id).exec() + .then(handleEntityNotFound(res)) + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Creates a new Thing in the DB +export function create(req, res) { + return Thing.create(req.body) + .then(respondWithResult(res, 201)) + .catch(handleError(res)); +} + +// Upserts the given Thing in the DB at the specified ID +export function upsert(req, res) { + if(req.body._id) { + Reflect.deleteProperty(req.body, '_id'); + } + return Thing.findOneAndUpdate({_id: req.params.id}, req.body, {new: true, upsert: true, setDefaultsOnInsert: true, runValidators: true}).exec() + + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Updates an existing Thing in the DB +export function patch(req, res) { + if(req.body._id) { + Reflect.deleteProperty(req.body, '_id'); + } + return Thing.findById(req.params.id).exec() + .then(handleEntityNotFound(res)) + .then(patchUpdates(req.body)) + .then(respondWithResult(res)) + .catch(handleError(res)); +} + +// Deletes a Thing from the DB +export function destroy(req, res) { + return Thing.findById(req.params.id).exec() + .then(handleEntityNotFound(res)) + .then(removeEntity(res)) + .catch(handleError(res)); +} diff --git a/server/api/thing/thing.events.js b/server/api/thing/thing.events.js new file mode 100644 index 0000000..3add968 --- /dev/null +++ b/server/api/thing/thing.events.js @@ -0,0 +1,35 @@ +/** + * Thing model events + */ + +'use strict'; + +import {EventEmitter} from 'events'; +var ThingEvents = new EventEmitter(); + +// Set max event listeners (0 == unlimited) +ThingEvents.setMaxListeners(0); + +// Model events +var events = { + save: 'save', + remove: 'remove' +}; + +// Register the event emitter to the model events +function registerEvents(Thing) { + for(var e in events) { + let event = events[e]; + Thing.post(e, emitEvent(event)); + } +} + +function emitEvent(event) { + return function(doc) { + ThingEvents.emit(`${event}:${doc._id}`, doc); + ThingEvents.emit(event, doc); + }; +} + +export {registerEvents}; +export default ThingEvents; diff --git a/server/api/thing/thing.integration.js b/server/api/thing/thing.integration.js new file mode 100644 index 0000000..85e0d16 --- /dev/null +++ b/server/api/thing/thing.integration.js @@ -0,0 +1,190 @@ +'use strict'; + +/* globals describe, expect, it, beforeEach, afterEach */ + +var app = require('../..'); +import request from 'supertest'; + +var newThing; + +describe('Thing API:', function() { + describe('GET /api/things', function() { + var things; + + beforeEach(function(done) { + request(app) + .get('/api/things') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + things = res.body; + done(); + }); + }); + + it('should respond with JSON array', function() { + expect(things).to.be.instanceOf(Array); + }); + }); + + describe('POST /api/things', function() { + beforeEach(function(done) { + request(app) + .post('/api/things') + .send({ + name: 'New Thing', + info: 'This is the brand new thing!!!' + }) + .expect(201) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + newThing = res.body; + done(); + }); + }); + + it('should respond with the newly created thing', function() { + expect(newThing.name).to.equal('New Thing'); + expect(newThing.info).to.equal('This is the brand new thing!!!'); + }); + }); + + describe('GET /api/things/:id', function() { + var thing; + + beforeEach(function(done) { + request(app) + .get(`/api/things/${newThing._id}`) + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + thing = res.body; + done(); + }); + }); + + afterEach(function() { + thing = {}; + }); + + it('should respond with the requested thing', function() { + expect(thing.name).to.equal('New Thing'); + expect(thing.info).to.equal('This is the brand new thing!!!'); + }); + }); + + describe('PUT /api/things/:id', function() { + var updatedThing; + + beforeEach(function(done) { + request(app) + .put(`/api/things/${newThing._id}`) + .send({ + name: 'Updated Thing', + info: 'This is the updated thing!!!' + }) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if(err) { + return done(err); + } + updatedThing = res.body; + done(); + }); + }); + + afterEach(function() { + updatedThing = {}; + }); + + it('should respond with the updated thing', function() { + expect(updatedThing.name).to.equal('Updated Thing'); + expect(updatedThing.info).to.equal('This is the updated thing!!!'); + }); + + it('should respond with the updated thing on a subsequent GET', function(done) { + request(app) + .get(`/api/things/${newThing._id}`) + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if(err) { + return done(err); + } + let thing = res.body; + + expect(thing.name).to.equal('Updated Thing'); + expect(thing.info).to.equal('This is the updated thing!!!'); + + done(); + }); + }); + }); + + describe('PATCH /api/things/:id', function() { + var patchedThing; + + beforeEach(function(done) { + request(app) + .patch(`/api/things/${newThing._id}`) + .send([ + { op: 'replace', path: '/name', value: 'Patched Thing' }, + { op: 'replace', path: '/info', value: 'This is the patched thing!!!' } + ]) + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if(err) { + return done(err); + } + patchedThing = res.body; + done(); + }); + }); + + afterEach(function() { + patchedThing = {}; + }); + + it('should respond with the patched thing', function() { + expect(patchedThing.name).to.equal('Patched Thing'); + expect(patchedThing.info).to.equal('This is the patched thing!!!'); + }); + }); + + describe('DELETE /api/things/:id', function() { + it('should respond with 204 on successful removal', function(done) { + request(app) + .delete(`/api/things/${newThing._id}`) + .expect(204) + .end(err => { + if(err) { + return done(err); + } + done(); + }); + }); + + it('should respond with 404 when thing does not exist', function(done) { + request(app) + .delete(`/api/things/${newThing._id}`) + .expect(404) + .end(err => { + if(err) { + return done(err); + } + done(); + }); + }); + }); +}); diff --git a/server/api/thing/thing.model.js b/server/api/thing/thing.model.js new file mode 100644 index 0000000..061a817 --- /dev/null +++ b/server/api/thing/thing.model.js @@ -0,0 +1,13 @@ +'use strict'; + +import mongoose from 'mongoose'; +import {registerEvents} from './thing.events'; + +var ThingSchema = new mongoose.Schema({ + name: String, + info: String, + active: Boolean +}); + +registerEvents(ThingSchema); +export default mongoose.model('Thing', ThingSchema); diff --git a/server/api/user/index.js b/server/api/user/index.js new file mode 100644 index 0000000..fad00f6 --- /dev/null +++ b/server/api/user/index.js @@ -0,0 +1,16 @@ +'use strict'; + +import {Router} from 'express'; +import * as controller from './user.controller'; +import * as auth from '../../auth/auth.service'; + +var router = new Router(); + +router.get('/', auth.hasRole('admin'), controller.index); +router.delete('/:id', auth.hasRole('admin'), controller.destroy); +router.get('/me', auth.isAuthenticated(), controller.me); +router.put('/:id/password', auth.isAuthenticated(), controller.changePassword); +router.get('/:id', auth.isAuthenticated(), controller.show); +router.post('/', controller.create); + +module.exports = router; diff --git a/server/api/user/index.spec.js b/server/api/user/index.spec.js new file mode 100644 index 0000000..773a546 --- /dev/null +++ b/server/api/user/index.spec.js @@ -0,0 +1,95 @@ +'use strict'; + +/* globals sinon, describe, expect, it */ + +var proxyquire = require('proxyquire').noPreserveCache(); + +var userCtrlStub = { + index: 'userCtrl.index', + destroy: 'userCtrl.destroy', + me: 'userCtrl.me', + changePassword: 'userCtrl.changePassword', + show: 'userCtrl.show', + create: 'userCtrl.create' +}; + +var authServiceStub = { + isAuthenticated() { + return 'authService.isAuthenticated'; + }, + hasRole(role) { + return `authService.hasRole.${role}`; + } +}; + +var routerStub = { + get: sinon.spy(), + put: sinon.spy(), + post: sinon.spy(), + delete: sinon.spy() +}; + +// require the index with our stubbed out modules +var userIndex = proxyquire('./index', { + express: { + Router() { + return routerStub; + } + }, + './user.controller': userCtrlStub, + '../../auth/auth.service': authServiceStub +}); + +describe('User API Router:', function() { + it('should return an express router instance', function() { + expect(userIndex).to.equal(routerStub); + }); + + describe('GET /api/users', function() { + it('should verify admin role and route to user.controller.index', function() { + expect(routerStub.get + .withArgs('/', 'authService.hasRole.admin', 'userCtrl.index') + ).to.have.been.calledOnce; + }); + }); + + describe('DELETE /api/users/:id', function() { + it('should verify admin role and route to user.controller.destroy', function() { + expect(routerStub.delete + .withArgs('/:id', 'authService.hasRole.admin', 'userCtrl.destroy') + ).to.have.been.calledOnce; + }); + }); + + describe('GET /api/users/me', function() { + it('should be authenticated and route to user.controller.me', function() { + expect(routerStub.get + .withArgs('/me', 'authService.isAuthenticated', 'userCtrl.me') + ).to.have.been.calledOnce; + }); + }); + + describe('PUT /api/users/:id/password', function() { + it('should be authenticated and route to user.controller.changePassword', function() { + expect(routerStub.put + .withArgs('/:id/password', 'authService.isAuthenticated', 'userCtrl.changePassword') + ).to.have.been.calledOnce; + }); + }); + + describe('GET /api/users/:id', function() { + it('should be authenticated and route to user.controller.show', function() { + expect(routerStub.get + .withArgs('/:id', 'authService.isAuthenticated', 'userCtrl.show') + ).to.have.been.calledOnce; + }); + }); + + describe('POST /api/users', function() { + it('should route to user.controller.create', function() { + expect(routerStub.post + .withArgs('/', 'userCtrl.create') + ).to.have.been.calledOnce; + }); + }); +}); diff --git a/server/api/user/user.controller.js b/server/api/user/user.controller.js new file mode 100644 index 0000000..c1ab134 --- /dev/null +++ b/server/api/user/user.controller.js @@ -0,0 +1,122 @@ +'use strict'; + +import User from './user.model'; +import config from '../../config/environment'; +import jwt from 'jsonwebtoken'; + +function validationError(res, statusCode) { + statusCode = statusCode || 422; + return function(err) { + return res.status(statusCode).json(err); + }; +} + +function handleError(res, statusCode) { + statusCode = statusCode || 500; + return function(err) { + return res.status(statusCode).send(err); + }; +} + +/** + * Get list of users + * restriction: 'admin' + */ +export function index(req, res) { + return User.find({}, '-salt -password').exec() + .then(users => { + res.status(200).json(users); + }) + .catch(handleError(res)); +} + +/** + * Creates a new user + */ +export function create(req, res) { + var newUser = new User(req.body); + newUser.provider = 'local'; + newUser.role = 'user'; + newUser.save() + .then(function(user) { + var token = jwt.sign({ _id: user._id }, config.secrets.session, { + expiresIn: 60 * 60 * 5 + }); + res.json({ token }); + }) + .catch(validationError(res)); +} + +/** + * Get a single user + */ +export function show(req, res, next) { + var userId = req.params.id; + + return User.findById(userId).exec() + .then(user => { + if(!user) { + return res.status(404).end(); + } + res.json(user.profile); + }) + .catch(err => next(err)); +} + +/** + * Deletes a user + * restriction: 'admin' + */ +export function destroy(req, res) { + return User.findByIdAndRemove(req.params.id).exec() + .then(function() { + res.status(204).end(); + }) + .catch(handleError(res)); +} + +/** + * Change a users password + */ +export function changePassword(req, res) { + var userId = req.user._id; + var oldPass = String(req.body.oldPassword); + var newPass = String(req.body.newPassword); + + return User.findById(userId).exec() + .then(user => { + if(user.authenticate(oldPass)) { + user.password = newPass; + return user.save() + .then(() => { + res.status(204).end(); + }) + .catch(validationError(res)); + } else { + return res.status(403).end(); + } + }); +} + +/** + * Get my info + */ +export function me(req, res, next) { + var userId = req.user._id; + + return User.findOne({ _id: userId }, '-salt -password').exec() + .then(user => { // don't ever give out the password or salt + if(!user) { + return res.status(401).end(); + } + res.json(user); + }) + .catch(err => next(err)); +} + +/** + * Authentication callback + */ +export function authCallback(req, res) { + res.redirect('/'); +} diff --git a/server/api/user/user.events.js b/server/api/user/user.events.js new file mode 100644 index 0000000..bf98a57 --- /dev/null +++ b/server/api/user/user.events.js @@ -0,0 +1,35 @@ +/** + * User model events + */ + +'use strict'; + +import {EventEmitter} from 'events'; +var UserEvents = new EventEmitter(); + +// Set max event listeners (0 == unlimited) +UserEvents.setMaxListeners(0); + +// Model events +var events = { + save: 'save', + remove: 'remove' +}; + +// Register the event emitter to the model events +function registerEvents(User) { + for(var e in events) { + let event = events[e]; + User.post(e, emitEvent(event)); + } +} + +function emitEvent(event) { + return function(doc) { + UserEvents.emit(`${event}:${doc._id}`, doc); + UserEvents.emit(event, doc); + }; +} + +export {registerEvents}; +export default UserEvents; diff --git a/server/api/user/user.integration.js b/server/api/user/user.integration.js new file mode 100644 index 0000000..9ef0176 --- /dev/null +++ b/server/api/user/user.integration.js @@ -0,0 +1,67 @@ +'use strict'; + +/* globals describe, expect, it, before, after, beforeEach, afterEach */ + +import app from '../..'; +import User from './user.model'; +import request from 'supertest'; + +describe('User API:', function() { + var user; + + // Clear users before testing + before(function() { + return User.remove().then(function() { + user = new User({ + name: 'Fake User', + email: 'test@example.com', + password: 'password' + }); + + return user.save(); + }); + }); + + // Clear users after testing + after(function() { + return User.remove(); + }); + + describe('GET /api/users/me', function() { + var token; + + before(function(done) { + request(app) + .post('/auth/local') + .send({ + email: 'test@example.com', + password: 'password' + }) + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + token = res.body.token; + done(); + }); + }); + + it('should respond with a user profile when authenticated', function(done) { + request(app) + .get('/api/users/me') + .set('authorization', `Bearer ${token}`) + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + expect(res.body._id.toString()).to.equal(user._id.toString()); + done(); + }); + }); + + it('should respond with a 401 when not authenticated', function(done) { + request(app) + .get('/api/users/me') + .expect(401) + .end(done); + }); + }); +}); diff --git a/server/api/user/user.model.js b/server/api/user/user.model.js new file mode 100644 index 0000000..6ac0415 --- /dev/null +++ b/server/api/user/user.model.js @@ -0,0 +1,258 @@ +'use strict'; +/*eslint no-invalid-this:0*/ +import crypto from 'crypto'; +mongoose.Promise = require('bluebird'); +import mongoose, {Schema} from 'mongoose'; +import {registerEvents} from './user.events'; + +const authTypes = ['github', 'twitter', 'facebook', 'google']; + +var UserSchema = new Schema({ + name: String, + email: { + type: String, + lowercase: true, + required() { + if(authTypes.indexOf(this.provider) === -1) { + return true; + } else { + return false; + } + } + }, + role: { + type: String, + default: 'user' + }, + password: { + type: String, + required() { + if(authTypes.indexOf(this.provider) === -1) { + return true; + } else { + return false; + } + } + }, + provider: String, + salt: String, + facebook: {}, + google: {}, + github: {} +}); + +/** + * Virtuals + */ + +// Public profile information +UserSchema + .virtual('profile') + .get(function() { + return { + name: this.name, + role: this.role + }; + }); + +// Non-sensitive info we'll be putting in the token +UserSchema + .virtual('token') + .get(function() { + return { + _id: this._id, + role: this.role + }; + }); + +/** + * Validations + */ + +// Validate empty email +UserSchema + .path('email') + .validate(function(email) { + if(authTypes.indexOf(this.provider) !== -1) { + return true; + } + return email.length; + }, 'Email cannot be blank'); + +// Validate empty password +UserSchema + .path('password') + .validate(function(password) { + if(authTypes.indexOf(this.provider) !== -1) { + return true; + } + return password.length; + }, 'Password cannot be blank'); + +// Validate email is not taken +UserSchema + .path('email') + .validate(function(value) { + if(authTypes.indexOf(this.provider) !== -1) { + return true; + } + + return this.constructor.findOne({ email: value }).exec() + .then(user => { + if(user) { + if(this.id === user.id) { + return true; + } + return false; + } + return true; + }) + .catch(function(err) { + throw err; + }); + }, 'The specified email address is already in use.'); + +var validatePresenceOf = function(value) { + return value && value.length; +}; + +/** + * Pre-save hook + */ +UserSchema + .pre('save', function(next) { + // Handle new/update passwords + if(!this.isModified('password')) { + return next(); + } + + if(!validatePresenceOf(this.password)) { + if(authTypes.indexOf(this.provider) === -1) { + return next(new Error('Invalid password')); + } else { + return next(); + } + } + + // Make salt with a callback + this.makeSalt((saltErr, salt) => { + if(saltErr) { + return next(saltErr); + } + this.salt = salt; + this.encryptPassword(this.password, (encryptErr, hashedPassword) => { + if(encryptErr) { + return next(encryptErr); + } + this.password = hashedPassword; + return next(); + }); + }); + }); + +/** + * Methods + */ +UserSchema.methods = { + /** + * Authenticate - check if the passwords are the same + * + * @param {String} password + * @param {Function} callback + * @return {Boolean} + * @api public + */ + authenticate(password, callback) { + if(!callback) { + return this.password === this.encryptPassword(password); + } + + this.encryptPassword(password, (err, pwdGen) => { + if(err) { + return callback(err); + } + + if(this.password === pwdGen) { + return callback(null, true); + } else { + return callback(null, false); + } + }); + }, + + /** + * Make salt + * + * @param {Number} [byteSize] - Optional salt byte size, default to 16 + * @param {Function} callback + * @return {String} + * @api public + */ + makeSalt(...args) { + let byteSize; + let callback; + let defaultByteSize = 16; + + if(typeof args[0] === 'function') { + callback = args[0]; + byteSize = defaultByteSize; + } else if(typeof args[1] === 'function') { + callback = args[1]; + } else { + throw new Error('Missing Callback'); + } + + if(!byteSize) { + byteSize = defaultByteSize; + } + + return crypto.randomBytes(byteSize, (err, salt) => { + if(err) { + return callback(err); + } else { + return callback(null, salt.toString('base64')); + } + }); + }, + + /** + * Encrypt password + * + * @param {String} password + * @param {Function} callback + * @return {String} + * @api public + */ + encryptPassword(password, callback) { + if(!password || !this.salt) { + if(!callback) { + return null; + } else { + return callback('Missing password or salt'); + } + } + + var defaultIterations = 10000; + var defaultKeyLength = 64; + var salt = new Buffer(this.salt, 'base64'); + + if(!callback) { + // eslint-disable-next-line no-sync + return crypto.pbkdf2Sync(password, salt, defaultIterations, + defaultKeyLength, 'sha1') + .toString('base64'); + } + + return crypto.pbkdf2(password, salt, defaultIterations, defaultKeyLength, + 'sha1', (err, key) => { + if(err) { + return callback(err); + } else { + return callback(null, key.toString('base64')); + } + }); + } +}; + +registerEvents(UserSchema); +export default mongoose.model('User', UserSchema); diff --git a/server/api/user/user.model.spec.js b/server/api/user/user.model.spec.js new file mode 100644 index 0000000..e4d0d8b --- /dev/null +++ b/server/api/user/user.model.spec.js @@ -0,0 +1,164 @@ +'use strict'; + +import app from '../..'; +import User from './user.model'; +var user; +var genUser = function() { + user = new User({ + provider: 'local', + name: 'Fake User', + email: 'test@example.com', + password: 'password' + }); + return user; +}; + +describe('User Model', function() { + before(function() { + // Clear users before testing + return User.remove(); + }); + + beforeEach(function() { + genUser(); + }); + + afterEach(function() { + return User.remove(); + }); + + it('should begin with no users', function() { + return expect(User.find({}).exec()).to + .eventually.have.length(0); + }); + + it('should fail when saving a duplicate user', function() { + return expect(user.save() + .then(function() { + var userDup = genUser(); + return userDup.save(); + })).to.be.rejected; + }); + + describe('#email', function() { + it('should fail when saving with a blank email', function() { + user.email = ''; + return expect(user.save()).to.be.rejected; + }); + + it('should fail when saving with a null email', function() { + user.email = null; + return expect(user.save()).to.be.rejected; + }); + + it('should fail when saving without an email', function() { + user.email = undefined; + return expect(user.save()).to.be.rejected; + }); + + describe('given user provider is google', function() { + beforeEach(function() { + user.provider = 'google'; + }); + + it('should succeed when saving without an email', function() { + user.email = null; + return expect(user.save()).to.be.fulfilled; + }); + }); + + describe('given user provider is facebook', function() { + beforeEach(function() { + user.provider = 'facebook'; + }); + + it('should succeed when saving without an email', function() { + user.email = null; + return expect(user.save()).to.be.fulfilled; + }); + }); + + describe('given user provider is github', function() { + beforeEach(function() { + user.provider = 'github'; + }); + + it('should succeed when saving without an email', function() { + user.email = null; + return expect(user.save()).to.be.fulfilled; + }); + }); + }); + + describe('#password', function() { + it('should fail when saving with a blank password', function() { + user.password = ''; + return expect(user.save()).to.be.rejected; + }); + + it('should fail when saving with a null password', function() { + user.password = null; + return expect(user.save()).to.be.rejected; + }); + + it('should fail when saving without a password', function() { + user.password = undefined; + return expect(user.save()).to.be.rejected; + }); + + describe('given the user has been previously saved', function() { + beforeEach(function() { + return user.save(); + }); + + it('should authenticate user if valid', function() { + expect(user.authenticate('password')).to.be.true; + }); + + it('should not authenticate user if invalid', function() { + expect(user.authenticate('blah')).to.not.be.true; + }); + + it('should remain the same hash unless the password is updated', function() { + user.name = 'Test User'; + return expect(user.save() + .then(function(u) { + return u.authenticate('password'); + })).to.eventually.be.true; + }); + }); + + describe('given user provider is google', function() { + beforeEach(function() { + user.provider = 'google'; + }); + + it('should succeed when saving without a password', function() { + user.password = null; + return expect(user.save()).to.be.fulfilled; + }); + }); + + describe('given user provider is facebook', function() { + beforeEach(function() { + user.provider = 'facebook'; + }); + + it('should succeed when saving without a password', function() { + user.password = null; + return expect(user.save()).to.be.fulfilled; + }); + }); + + describe('given user provider is github', function() { + beforeEach(function() { + user.provider = 'github'; + }); + + it('should succeed when saving without a password', function() { + user.password = null; + return expect(user.save()).to.be.fulfilled; + }); + }); + }); +}); diff --git a/server/app.js b/server/app.js new file mode 100644 index 0000000..d743048 --- /dev/null +++ b/server/app.js @@ -0,0 +1,38 @@ +/** + * Main application file + */ + +'use strict'; + +import express from 'express'; +import mongoose from 'mongoose'; +mongoose.Promise = require('bluebird'); +import config from './config/environment'; +import http from 'http'; +import seedDatabaseIfNeeded from './config/seed'; + +// Connect to MongoDB +mongoose.connect(config.mongo.uri, config.mongo.options); +mongoose.connection.on('error', function(err) { + console.error(`MongoDB connection error: ${err}`); + process.exit(-1); // eslint-disable-line no-process-exit +}); + +// Setup server +var app = express(); +var server = http.createServer(app); +require('./config/express').default(app); +require('./routes').default(app); + +// Start server +function startServer() { + app.angularFullstack = server.listen(config.port, config.ip, function() { + console.log('Express server listening on %d, in %s mode', config.port, app.get('env')); + }); +} + +seedDatabaseIfNeeded(); +setImmediate(startServer); + +// Expose app +exports = module.exports = app; diff --git a/server/auth/auth.service.js b/server/auth/auth.service.js new file mode 100644 index 0000000..4a42130 --- /dev/null +++ b/server/auth/auth.service.js @@ -0,0 +1,82 @@ +'use strict'; +import config from '../config/environment'; +import jwt from 'jsonwebtoken'; +import expressJwt from 'express-jwt'; +import compose from 'composable-middleware'; +import User from '../api/user/user.model'; + +var validateJwt = expressJwt({ + secret: config.secrets.session +}); + +/** + * Attaches the user object to the request if authenticated + * Otherwise returns 403 + */ +export function isAuthenticated() { + return compose() + // Validate jwt + .use(function(req, res, next) { + // allow access_token to be passed through query parameter as well + if(req.query && req.query.hasOwnProperty('access_token')) { + req.headers.authorization = `Bearer ${req.query.access_token}`; + } + // IE11 forgets to set Authorization header sometimes. Pull from cookie instead. + if(req.query && typeof req.headers.authorization === 'undefined') { + req.headers.authorization = `Bearer ${req.cookies.token}`; + } + validateJwt(req, res, next); + }) + // Attach user to request + .use(function(req, res, next) { + User.findById(req.user._id).exec() + .then(user => { + if(!user) { + return res.status(401).end(); + } + req.user = user; + next(); + }) + .catch(err => next(err)); + }); +} + +/** + * Checks if the user role meets the minimum requirements of the route + */ +export function hasRole(roleRequired) { + if(!roleRequired) { + throw new Error('Required role needs to be set'); + } + + return compose() + .use(isAuthenticated()) + .use(function meetsRequirements(req, res, next) { + if(config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) { + return next(); + } else { + return res.status(403).send('Forbidden'); + } + }); +} + +/** + * Returns a jwt token signed by the app secret + */ +export function signToken(id, role) { + return jwt.sign({ _id: id, role }, config.secrets.session, { + expiresIn: 60 * 60 * 5 + }); +} + +/** + * Set token cookie directly for oAuth strategies + */ +export function setTokenCookie(req, res) { + if(!req.user) { + return res.status(404).send('It looks like you aren\'t logged in, please try again.'); + } + var token = signToken(req.user._id, req.user.role); + res.cookie('token', token); + res.redirect('/'); +} diff --git a/server/auth/facebook/index.js b/server/auth/facebook/index.js new file mode 100644 index 0000000..f0c98ef --- /dev/null +++ b/server/auth/facebook/index.js @@ -0,0 +1,20 @@ +'use strict'; + +import express from 'express'; +import passport from 'passport'; +import {setTokenCookie} from '../auth.service'; + +var router = express.Router(); + +router + .get('/', passport.authenticate('facebook', { + scope: ['email', 'user_about_me'], + failureRedirect: '/signup', + session: false + })) + .get('/callback', passport.authenticate('facebook', { + failureRedirect: '/signup', + session: false + }), setTokenCookie); + +export default router; diff --git a/server/auth/facebook/passport.js b/server/auth/facebook/passport.js new file mode 100644 index 0000000..7046e0a --- /dev/null +++ b/server/auth/facebook/passport.js @@ -0,0 +1,34 @@ +import passport from 'passport'; +import {Strategy as FacebookStrategy} from 'passport-facebook'; + +export function setup(User, config) { + passport.use(new FacebookStrategy({ + clientID: config.facebook.clientID, + clientSecret: config.facebook.clientSecret, + callbackURL: config.facebook.callbackURL, + profileFields: [ + 'displayName', + 'emails' + ] + }, + function(accessToken, refreshToken, profile, done) { + User.findOne({'facebook.id': profile.id}).exec() + .then(user => { + if(user) { + return done(null, user); + } + + user = new User({ + name: profile.displayName, + email: profile.emails[0].value, + role: 'user', + provider: 'facebook', + facebook: profile._json + }); + user.save() + .then(savedUser => done(null, savedUser)) + .catch(err => done(err)); + }) + .catch(err => done(err)); + })); +} diff --git a/server/auth/google/index.js b/server/auth/google/index.js new file mode 100644 index 0000000..25753de --- /dev/null +++ b/server/auth/google/index.js @@ -0,0 +1,23 @@ +'use strict'; + +import express from 'express'; +import passport from 'passport'; +import {setTokenCookie} from '../auth.service'; + +var router = express.Router(); + +router + .get('/', passport.authenticate('google', { + failureRedirect: '/signup', + scope: [ + 'profile', + 'email' + ], + session: false + })) + .get('/callback', passport.authenticate('google', { + failureRedirect: '/signup', + session: false + }), setTokenCookie); + +export default router; diff --git a/server/auth/google/passport.js b/server/auth/google/passport.js new file mode 100644 index 0000000..a0f8347 --- /dev/null +++ b/server/auth/google/passport.js @@ -0,0 +1,31 @@ +import passport from 'passport'; +import {Strategy as GoogleStrategy} from 'passport-google-oauth20'; + +export function setup(User, config) { + passport.use(new GoogleStrategy({ + clientID: config.google.clientID, + clientSecret: config.google.clientSecret, + callbackURL: config.google.callbackURL + }, + function(accessToken, refreshToken, profile, done) { + User.findOne({'google.id': profile.id}).exec() + .then(user => { + if(user) { + return done(null, user); + } + + user = new User({ + name: profile.displayName, + email: profile.emails[0].value, + role: 'user', + username: profile.emails[0].value.split('@')[0], + provider: 'google', + google: profile._json + }); + user.save() + .then(savedUser => done(null, savedUser)) + .catch(err => done(err)); + }) + .catch(err => done(err)); + })); +} diff --git a/server/auth/index.js b/server/auth/index.js new file mode 100644 index 0000000..9b04d8b --- /dev/null +++ b/server/auth/index.js @@ -0,0 +1,17 @@ +'use strict'; +import express from 'express'; +import config from '../config/environment'; +import User from '../api/user/user.model'; + +// Passport Configuration +require('./local/passport').setup(User, config); +require('./facebook/passport').setup(User, config); +require('./google/passport').setup(User, config); + +var router = express.Router(); + +router.use('/local', require('./local').default); +router.use('/facebook', require('./facebook').default); +router.use('/google', require('./google').default); + +export default router; diff --git a/server/auth/local/index.js b/server/auth/local/index.js new file mode 100644 index 0000000..08ebf69 --- /dev/null +++ b/server/auth/local/index.js @@ -0,0 +1,24 @@ +'use strict'; + +import express from 'express'; +import passport from 'passport'; +import {signToken} from '../auth.service'; + +var router = express.Router(); + +router.post('/', function(req, res, next) { + passport.authenticate('local', function(err, user, info) { + var error = err || info; + if(error) { + return res.status(401).json(error); + } + if(!user) { + return res.status(404).json({message: 'Something went wrong, please try again.'}); + } + + var token = signToken(user._id, user.role); + res.json({ token }); + })(req, res, next); +}); + +export default router; diff --git a/server/auth/local/passport.js b/server/auth/local/passport.js new file mode 100644 index 0000000..dc52228 --- /dev/null +++ b/server/auth/local/passport.js @@ -0,0 +1,35 @@ +import passport from 'passport'; +import {Strategy as LocalStrategy} from 'passport-local'; + +function localAuthenticate(User, email, password, done) { + User.findOne({ + email: email.toLowerCase() + }).exec() + .then(user => { + if(!user) { + return done(null, false, { + message: 'This email is not registered.' + }); + } + user.authenticate(password, function(authError, authenticated) { + if(authError) { + return done(authError); + } + if(!authenticated) { + return done(null, false, { message: 'This password is not correct.' }); + } else { + return done(null, user); + } + }); + }) + .catch(err => done(err)); +} + +export function setup(User/*, config*/) { + passport.use(new LocalStrategy({ + usernameField: 'email', + passwordField: 'password' // this is the virtual field on the model + }, function(email, password, done) { + return localAuthenticate(User, email, password, done); + })); +} diff --git a/server/components/ansible/ansible_tool.js b/server/components/ansible/ansible_tool.js new file mode 100644 index 0000000..5e866c5 --- /dev/null +++ b/server/components/ansible/ansible_tool.js @@ -0,0 +1,658 @@ +var ssh2_exec = require('../ssh/ssh2_exec'); +var scp2_exec = require('../scp/scp_exec'); +var config = require('../../config/environment'); + +var local_logPath = 'logs/ansible/execute/' + +exports.getLogs = function(logfilename,successCallback,errorCallback){ + var logFile = local_logPath + logfilename; + var fs = require('fs'); + fs.readFile(logFile, function(err, data){ + if(err){ + errorCallback(err); + }else{ + successCallback(data); + } + + }); +}; + + +exports.getModules = function(dataCallback, successCallback,errorCallback, ansibleEngine){ + + var command = 'ansible-doc -l'; + + if(ansibleEngine.customModules){ + command = 'export ANSIBLE_LIBRARY="' + ansibleEngine.customModules + '"; ' + command; + } + + ssh2_exec.executeCommand(command, + null, + function(data){ + successCallback(data); + }, + function(data){ + errorCallback(data) + }, + ansibleEngine + ) +}; + +exports.getAnsibleVersion = function(successCallback,errorCallback, ansibleEngine){ + var command = 'ansible --version'; + var ansibleVersionResult = ""; + + ssh2_exec.executeCommand(command, + null, + function(data){ + ansibleVersionResult=data; + console.log("Ansible Verison =" + ansibleVersionResult); + ansibleVersionResult = "" + ansibleVersionResult; + var version = ansibleVersionResult.replace(/ansible (.*)[^]+/,"$1"); + console.log("Version=" + version); + successCallback(version || ansibleVersionResult); + }, + function(data){ + errorCallback(data) + }, + ansibleEngine + ) +}; + + +exports.executeAnsible = function(logfilename,project_folder, playbook_name, inventory_file_name, tags_joined, limit_to_hosts_joined, verbose,check_mode,dataCallback, successCallback,errorCallback,ansibleEngine){ + + var fs = require('filendir'); + var time = new Date().getTime(); + var logFile = local_logPath + logfilename; + + fs.writeFileSync(logFile,"Executing Ansible Playbook \n\n",{'flag':'a'}); + fs.writeFileSync(logFile," Completed \n",{'flag':'a'}); + + // export ANSIBLE_GATHERING=FALSE; + var command= 'export ANSIBLE_FORCE_COLOR=true; export ANSIBLE_HOST_KEY_CHECKING=False; cd "' + project_folder + '"; ansible-playbook --vault-password-file ~/.vault_pass.txt "' + playbook_name + '" -i "' + inventory_file_name + '"'; + + if(ansibleEngine.customModules){ + command = 'export ANSIBLE_LIBRARY="' + ansibleEngine.customModules + '"; ' + command; + } + + if(tags_joined) + command += ' --tags "' + tags_joined + '"'; + //command += ' --tags "' + tags.join(",") + '"'; + + if(limit_to_hosts_joined) + command += ' --limit "' + limit_to_hosts_joined + '"'; + + if(verbose === 'verbose_detail'){ + command += ' -vvv '; + } + else if(verbose === 'verbose'){ + command += ' -v '; + } + + if(check_mode !== 'No_Check'){ + command += ' --check '; + } + + console.log("Command= " + command); + + //fs.writeFileSync(logFile,"\n Executing Command =" + command + "\n",{'flag':'a'}); + fs.writeFile(logFile,"\n Executing Command =" + command + "\n"); + + ssh2_exec.executeCommand(command,function(response){ + //Calling datacallbcak back as the call is Asynchronous + //Logs are queried to check status + dataCallback(response); + console.log(response); + //fs.writeFile(logFile,response,{'flag':'a'}); + fs.writeFile(logFile,"\n Executing Command =" + command + "\n" + response); + },function(response){ + successCallback(response); + console.log(response); + //fs.writeFile(logFile,response,{'flag':'a'}); + },function(response){ + errorCallback(response); + console.log(response); + fs.writeFile(logFile,response,{'flag':'a'}); + },ansibleEngine, true //addScriptEndString + ) + +}; + + + +exports.getVars = function(project_folder, inventory_file_name, host_name, dataCallback, successCallback,errorCallback,ansibleEngine){ + + var fs = require('filendir'); + + var AnsibleAPILocation = '/opt/ehc-builder-scripts/ansible_modules/AnsibleAPI.py'; + + var command= 'cd "' + project_folder + '"; python "' + AnsibleAPILocation + '" host_vars --inventory_file="' + inventory_file_name + '"'; + + if(host_name){ + command += ' --host_name ' + host_name; + } + + if(ansibleEngine.customModules){ + command = 'export ANSIBLE_LIBRARY="' + ansibleEngine.customModules + '"; ' + command; + } + + console.log("Command= " + command); + + ssh2_exec.executeCommand(command,null,successCallback,errorCallback,ansibleEngine); + +}; + + +exports.getRolesVars = function(project_folder, role_name, dataCallback, successCallback,errorCallback,ansibleEngine){ + + var AnsibleAPILocation = '/opt/ehc-builder-scripts/ansible_modules/AnsibleAPI.py'; + + var project_roles_folder = project_folder + '/roles'; + var playbook_path = role_name + '/tests/test.yml'; + var command= 'cd "' + project_roles_folder + '"; python "' + AnsibleAPILocation + '" role_vars --playbook_path="' + playbook_path + '" --vault_password_file ~/.vault_pass.txt'; + + if(ansibleEngine.customModules){ + command = 'export ANSIBLE_LIBRARY="' + ansibleEngine.customModules + '"; ' + command; + } + + console.log("Command= " + command); + + ssh2_exec.executeCommand(command,null,successCallback,errorCallback,ansibleEngine); + +}; + +/* +exports.executeAnsible = function(logfilename,inventory_file_contents,playbook_file_contents,tags,verbose,check_mode,dataCallback, successCallback,errorCallback,ansibleEngine){ + + var fs = require('filendir'); + var time = new Date().getTime(); + var logFile = local_logPath + logfilename; + + fs.writeFileSync(logFile,"Executing Ansible Playbook \n\n",{'flag':'a'}); + + var inventory_file_name = 'inventory_file_' + time + '.ini'; + var playbook_file_name = 'playbook_file_' + time + '.yml'; + + fs.writeFileSync(logFile,"inventory_file_location - " + inventory_file_name +" \n",{'flag':'a'}); + fs.writeFileSync(logFile,"playbook_file_location - " + playbook_file_name +" \n\n",{'flag':'a'}); + + console.log('inventory_file_name=' + inventory_file_name); + + var inputFilePathOnScriptEngine = config.scriptEngine.inputDirectory + '/inventory/' + inventory_file_name; + var playbookFilePathOnScriptEngine = config.scriptEngine.inputDirectory + '/playbooks/' + playbook_file_name; + + fs.writeFileSync(logFile,"Writing inventory file to Script Engine - " + inputFilePathOnScriptEngine +" - ",{'flag':'a'}); + + scp2_exec.createFileOnScriptEngine(inventory_file_contents,inputFilePathOnScriptEngine, + function(){ + console.log("Inventory file written"); + fs.writeFileSync(logFile," Completed \n",{'flag':'a'}); + fs.writeFileSync(logFile,"Writing playbook file to Script Engine - " + playbookFilePathOnScriptEngine +" - ",{'flag':'a'}); + scp2_exec.createFileOnScriptEngine(playbook_file_contents,playbookFilePathOnScriptEngine, + function(){ + console.log("Playbook file written"); + fs.writeFileSync(logFile," Completed \n",{'flag':'a'}); + + var command= "export ANSIBLE_HOST_KEY_CHECKING=False; ansible-playbook --vault-password-file ~/.vault_pass.txt " + playbookFilePathOnScriptEngine + " -i " + inputFilePathOnScriptEngine; + + if(ansibleEngine.customModules){ + command = 'export ANSIBLE_LIBRARY=' + ansibleEngine.customModules + '; ' + command; + } + + if(tags) + command += ' --tags "' + tags.join(",") + '"'; + + if(verbose === 'verbose_detail'){ + command += ' -vvv '; + } + else if(verbose === 'verbose'){ + command += ' -v '; + } + + if(check_mode !== 'No_Check'){ + command += ' --check '; + } + + console.log("Command= " + command); + + fs.writeFileSync(logFile,"\n Executing Command =" + command + "\n",{'flag':'a'}); + + ssh2_exec.executeCommand(command,function(response){ + dataCallback(response); + console.log(response); + fs.writeFile(logFile,response,{'flag':'a'}); + },function(response){ + successCallback(response); + console.log(response); + fs.writeFile(logFile,response,{'flag':'a'}); + },function(response){ + errorCallback(response); + console.log(response); + fs.writeFile(logFile,response,{'flag':'a'}); + },ansibleEngine) + + },function(err){ + errorCallback(err); + fs.writeFile(logFile," Failed \n",{'flag':'a'}); + },ansibleEngine); + + },function(err){ + errorCallback(err); + fs.writeFile(logFile," Failed \n",{'flag':'a'}); + },ansibleEngine); + +}; +*/ + + + +exports.writeFile = function(file_path,file_contents, successCallback,errorCallback,ansibleEngine){ + + scp2_exec.createFileOnScriptEngine(file_contents, file_path, + function(){ + successCallback('file written'); + },function(err){ + errorCallback(err); + },ansibleEngine); + +}; + +exports.deleteFile = function(file_path,successCallback,errorCallback,ansibleEngine){ + + var command = 'rm -rf "' + file_path + '"'; + + ssh2_exec.executeCommand(command,null,successCallback,errorCallback,ansibleEngine) + +}; + +exports.readFile = function(file_path, dataCallback, successCallback,errorCallback,ansibleEngine){ + + var command = 'cat "' + file_path + '"'; + + ssh2_exec.executeCommand(command,null,successCallback,errorCallback,ansibleEngine) + +}; + +exports.writePlaybook = function(project_folder,playbook_file_name,playbook_file_contents, successCallback,errorCallback,ansibleEngine){ + + var playbook_file_path = '' + project_folder + '/' + playbook_file_name + ''; + + console.log('playbook_file_path=' + playbook_file_path); + + scp2_exec.createFileOnScriptEngine(playbook_file_contents, playbook_file_path, + function(){ + console.log('playbook_file written'); + successCallback('playbook_file written'); + },function(err){ + errorCallback(err); + },ansibleEngine); + +}; + + +exports.readPlaybook = function(project_folder,playbook_file_name, dataCallback, successCallback,errorCallback,ansibleEngine){ + + var playbook_file_path = project_folder + '/' + playbook_file_name; + var command = 'cat "' + playbook_file_path + '"'; + + ssh2_exec.executeCommand(command,null,successCallback,errorCallback,ansibleEngine) + +}; + + +exports.deletePlaybook = function(project_folder,playbook_file_name, dataCallback, successCallback,errorCallback,ansibleEngine){ + + var playbook_file_path = project_folder + '/' + playbook_file_name; + var command = 'rm -f "' + playbook_file_path + '"'; + + ssh2_exec.executeCommand(command,null,successCallback,errorCallback,ansibleEngine) + +}; + +exports.getPlaybookList = function(project_folder, successCallback, errorCallback, ansibleEngine){ + + var playbook_file_path = project_folder + '/'; + var command = 'ls "' + playbook_file_path + '" | grep .yml'; + var ansiblePlaybookListResults = ""; + + ssh2_exec.executeCommand(command, + null, + function(data){ + ansiblePlaybookListResults=data; + var files = []; + if(ansiblePlaybookListResults) + files = ansiblePlaybookListResults.trim().split('\n'); + successCallback(files); + }, + function(data){ + errorCallback(data) + }, + ansibleEngine + ) + +}; + +/** + * Get list of roles from project + * @param project_folder - Project root folder + * @param successCallback - Success Callback method + * @param errorCallback - Error Callback method + * @param ansibleEngine - Remote Ansible Engine details + */ +exports.getRolesList = function(project_folder, successCallback, errorCallback, ansibleEngine){ + + var playbook_file_path = project_folder + '/roles'; + var command = 'ls "' + playbook_file_path + '"'; + var ansiblePlaybookListResults = ""; + + ssh2_exec.executeCommand(command, + null, + function(data){ + ansiblePlaybookListResults=data; + var roles = []; + if(ansiblePlaybookListResults) + roles = ansiblePlaybookListResults.trim().split('\n'); + successCallback(roles); + }, + function(data){ + errorCallback(data) + }, + ansibleEngine + ) + +}; + + +/** + * Create Project Folder + * @param project_folder - Project folder to create + * @param successCallback + * @param errorCallback + * @param ansibleEngine - Remote Ansible Engine details + */ +exports.createProjectFolder = function(project_folder, successCallback, errorCallback, ansibleEngine){ + + var librarypath = project_folder + '/library'; + var rolespath = project_folder + '/roles'; + var command = 'mkdir -p "' + librarypath + '"; mkdir -p "' + rolespath + '"'; + + var check_dir_command = '[ ! -d ' + project_folder + ' ]'; + + ssh2_exec.executeCommand(check_dir_command,null,function(data){ + ssh2_exec.executeCommand(command,null,successCallback,errorCallback,ansibleEngine); + },function(){ + errorCallback("Directory - " + project_folder +" already exists. Try a different Project Folder path.") + },ansibleEngine); + +}; + +/** + * Search roles in ansible-galaxy + * Use ansible-galaxy search command + * @param searchText - Text to search + * @param successCallback + * @param errorCallback + * @param ansibleEngine - Remote Ansible Engine details + */ +exports.searchRolesGalaxy = function(searchText, successCallback, errorCallback, ansibleEngine){ + + var command = 'ansible-galaxy search ' + searchText; + console.log('Command = ' + command); + ssh2_exec.executeCommand(command,null,function(response){ + + console.log("Galaxy Response =" + response); + + if(response.indexOf('No roles match your search.') > -1){ + return errorCallback('No roles match your search.') + }else{ + var str = response.replace(/[^]+--\n([^]+)/g,'$1'); + + var re = /\s+(.*?)\s+(.*)/gm; + var m; + var results = []; + while ((m = re.exec(str)) !== null) { + if (m.index === re.lastIndex) { + re.lastIndex++; + } + // View your result using the m-variable. + // eg m[0] etc. + + results.push({'name':m[1],'description':m[2],'type':'galaxy'}) + + } + + successCallback(results); + + } + + }, errorCallback,ansibleEngine); + +}; + +/** + * Search Roles in GitHub + * Uses uri https://api.github.com/search/repositories?q=ansible-role- + * @param searchText + * @param successCallback + * @param errorCallback + * @param ansibleEngine + */ +exports.searchRolesGithub = function(searchText, successCallback, errorCallback, ansibleEngine){ + + var https = require('https'); + var options = { + host: 'api.github.com', + path: '/search/repositories?q=ansible-role-' + searchText, + headers: {'user-agent': 'node.js'} + }; + + console.log("path " + '/search/repositories?q=ansible-role' + searchText) + + var req = https.get(options, function(res) { + console.log('STATUS: ' + res.statusCode); + console.log('HEADERS: ' + JSON.stringify(res.headers)); + console.log('DATA: ' + JSON.stringify(res.data)); + + // Buffer the body entirely for processing as a whole. + var bodyChunks = []; + res.on('data', function(chunk) { + // You can process streamed parts here... + bodyChunks.push(chunk); + }).on('end', function() { + var body = Buffer.concat(bodyChunks); + console.log('BODY: ' + body); + + var json_results = JSON.parse(body); + var results = []; + console.log("Search Results = " + json_results.total_count); + for(var i=0;i { + // console.log(`Found match, group ${groupIndex}: ${match}`); + // + // }); + + var methodName = m[1]; + var docStringComments = '"""' + m[2] + '"""'; + + var regex2 = /"""[^]+(Description[^]+?)(Parameters[^]+?)?(Return[^]+?)?(Raise[^]+?)?"""/gm; + var description = docStringComments.replace(regex2, '$1'); + var Parameters = docStringComments.replace(regex2, '$2'); + var Return = docStringComments.replace(regex2, '$3'); + var Raise = docStringComments.replace(regex2, '$4'); + + var moduleName = path.parse(filename).name + var packageName = path.parse(path.parse(filename).dir).name + + // console.log('Package: %s', packageName); + // console.log('Module: %s', moduleName); + // console.log('Method: %s', methodName); + // console.log('Description: %s', description); + + var method = { + moduleName: moduleName, + methodName: methodName, + packageName: packageName, + description: description + } + + results.push(method) + // console.log('Parameters: %s', Parameters); + // console.log('Return: %s', Return); + // console.log('Raise: %s', Raise); + + } + + next(); + }, function(){ + + console.log(results); + +}); + + diff --git a/server/components/scp/scp_exec.js b/server/components/scp/scp_exec.js new file mode 100644 index 0000000..18eac86 --- /dev/null +++ b/server/components/scp/scp_exec.js @@ -0,0 +1,123 @@ +import config from '../../config/environment'; +//var config = require('../../config/environment/development.js'); +var client = require('scp2'); + + +exports.copyFileToScriptEngine = function(sourcePath,destinationPath,ansibleEngine){ + + var connHost = ansibleEngine.ansibleHost || config.scriptEngine.host; + var connUser = ansibleEngine.ansibleHostUser || config.scriptEngine.user; + var connHostPassword = ansibleEngine.ansibleHostPassword || config.scriptEngine.password; + + var scriptEngineConfig = { + host: connHost, + port: 22, + username: connUser, + tryKeyboard: true + }; + + if(connHostPassword){ + scriptEngineConfig.password = connHostPassword; + }else{ + scriptEngineConfig.privateKey = require('fs').readFileSync(config.scriptEngine.privateKey); + } + + scriptEngineConfig.destinationPath = destinationPath; + var Client = require('scp2').Client; + var cl = new Client(scriptEngineConfig); + + cl.on('keyboard-interactive', function(name, instr, lang, prompts, cb) { + cb([connHostPassword]); + }); + + cl.on('error', function(error) { + console.log("SCP Connect Error" + error); + return error + }); + + cl.upload(sourcePath,destinationPath,function(err) { + if(err){ + console.error(err) + }else{ + console.log("Successfully uploaded file") + cl.close() + } + }) +}; + +exports.createFileOnScriptEngine = function(contents,destinationPath,successCallback,errorCallback,ansibleEngine){ + var Client = require('scp2').Client; + var buffer = new Buffer(contents, "utf-8"); + + if(!ansibleEngine) ansibleEngine = {}; + + var connHost = ansibleEngine.ansibleHost || config.scriptEngine.host; + var connUser = ansibleEngine.ansibleHostUser || config.scriptEngine.user; + var connHostPassword = ansibleEngine.ansibleHostPassword || config.scriptEngine.password; + + var scriptEngineConfig = { + host: connHost, + port: 22, + username: connUser, + tryKeyboard: true + }; + + if(connHostPassword){ + scriptEngineConfig.password = connHostPassword; + }else{ + scriptEngineConfig.privateKey = require('fs').readFileSync(config.scriptEngine.privateKey); + } + + + var cl = new Client(scriptEngineConfig); + + cl.on('keyboard-interactive', function(name, instr, lang, prompts, cb) { + cb([connHostPassword]); + }); + + cl.on('error', function(error) { + console.log("SCP Connect Error" + error); + errorCallback(error); + }); + + //cl.connect(scriptEngineConfig); + + var dirname = destinationPath.match(/(.*)[\/\\]/)[1]||''; + + console.log("direcname = " + dirname); + + cl.mkdir(dirname,function(err){ + if(err){ + errorCallback('Failed to create directory - ' + dirname + ' -' + err) + return cl.close() + } + + cl.write({ + destination: destinationPath, + content: buffer + }, function(err){ + if(err){ + console.error(err); + errorCallback(err); + + }else{ + console.log("Success "); + successCallback() + } + cl.close() + }); + + }); + + + +}; + +//exports.copyFileToScriptEngine('scp_exec.js','/tmp/ssh_tezt.js'); +/* +exports.createFileOnScriptEngine('sdfdddddddddsfd','/tmp/testfile.txt', function(response){ + console.log("Success" + response) +}, function(response){ + console.log("Error" + response) +}); +*/ diff --git a/server/components/ssh/ssh2_exec.js b/server/components/ssh/ssh2_exec.js new file mode 100644 index 0000000..9894c4c --- /dev/null +++ b/server/components/ssh/ssh2_exec.js @@ -0,0 +1,108 @@ +var Client = require('ssh2').Client; + +//var exec = require('ssh-exec'); + +var config = require('../../config/environment'); + +exports.executeCommand = function(command, dataCallback,completeCallback,errorCallback, ansibleEngine, addScriptEndString){ + + /*var fs = require('filendir'); + var time = new Date().getTime(); + //var logFile = 'logs/deploy/' + logfilename; + var logFile = logfilelocation;*/ + + var conn = new Client(); + + if(!ansibleEngine) ansibleEngine = {}; + + var connHost = ansibleEngine.ansibleHost || config.scriptEngine.host; + var connUser = ansibleEngine.ansibleHostUser || config.scriptEngine.user; + var connHostPassword = ansibleEngine.ansibleHostPassword || config.scriptEngine.password; + + var scriptEngineConfig = { + host: connHost, + port: 22, + username: connUser, + tryKeyboard: true + }; + + if(connHostPassword){ + scriptEngineConfig.password = connHostPassword; + }else{ + scriptEngineConfig.privateKey = require('fs').readFileSync(config.scriptEngine.privateKey); + } + + //fs.appendFile(logFile,command); + //console.log("Writing Command to log file =" + command) + /*fs.writeFile(logFile,"\n",{'flag':'a'});*/ + + conn.on('keyboard-interactive', function(name, instr, lang, prompts, cb) { + cb([connHostPassword]); + }); + + conn.on('error', function(error) { + console.log("SSH Connect Error" + error); + errorCallback(error); + }); + + conn.on('ready', function() { + console.log('Client :: ready'); + console.log('Command :: ' + command); + conn.exec(command, function(err, stream) { + var callBackSent = false; + + var result_data = ""; + var error_data = ""; + var error = false; + + if (err) { + console.log("Error=" + err); + errorCallback(err); + + } + stream.on('close', function(code, signal) { + console.log('Stream :: close :: code: ' + code + ', signal: ' + signal); + //completeCallback('Stream :: close :: code: ' + code + ', signal: ' + signal + '\nSCRIPT_FINISHED'); + if(addScriptEndString){ + //dataCallback call is what writes to logfile + result_data += '\nSCRIPT_FINISHED'; + dataCallback(result_data); + } + + if(code !== 0){ + errorCallback(error_data) + }else{ + completeCallback(result_data) + } + conn.end(); + }).on('data', function(data) { + console.log('STDOUT: ' + data); + result_data += data; + if(dataCallback){ + //dataCallback(data); + dataCallback(result_data); + } + + }).stderr.on('data', function(data) { + console.log('STDERR: ' + data); + error_data += data; + error = true; + //errorCallback(data); + + }); + }); + }).connect(scriptEngineConfig); +}; + + +//exports.executeCommand(null,'python3.4 /data/ehc-builder/scripts/vipr/python/ehc-builder/scripts/bin/main.py vro all --inputfile="configure_vmware_vro_Mumshad_Mannambeth_1468092975124.in" --logfile="configure_vmware_vro_Mumshad_Mannambeth_1468092975124"', 'logs/deploy/configure_vmware_vro_Mumshad_Mannambeth_1468092975124.log' ) +/* + +exports.executeCommand(null,'date','testfile.log',function(response){ + console.log(response) +},function(response){ + console.log(response) +},function(response){ + console.log(response) +}) +*/ diff --git a/server/components/upgrade/upgradetool.js b/server/components/upgrade/upgradetool.js new file mode 100644 index 0000000..f3562e2 --- /dev/null +++ b/server/components/upgrade/upgradetool.js @@ -0,0 +1,116 @@ +/** + * Created by mannam4 on 7/31/2016. + */ +var ssh2_exec = require('../ssh/ssh2_exec'); +var config = require('../../config/environment'); + +exports.getLogs = function(logfilename,successCallback,errorCallback){ + + var logFile = '/opt/ehc-builder-scripts/logs/' + logfilename; + var command = 'cat ' +logFile; + + var logFileData = ''; + + console.log("Command = " + command); + + var localLogFile = 'logs/upgrade/upgrade.log'; + + ssh2_exec.executeCommand(command, + function(data){ + //Partial Data + //console.log("Data = "+ data) + //logFileData+=data + }, + function(data){ + //Complete Data + //console.log("Data =" + data) + if(data) + logFileData = (data.toString().replace('Stream :: close :: code: 0, signal: undefined','')); + console.log("Success Callback =" + logFileData); + successCallback(logFileData) + }, + function(error){ + //Error Data + //console.log("Error =" + error) + if(error) + logFileData+=error; + console.log("Error Callback =" + logFileData); + errorCallback(logFileData) + } + ); + + /*var logFile = 'logs/upgrade/' + logfilename; + var fs = require('fs'); + fs.readFile(logFile, function(err, data){ + if(err){ + errorCallback(err); + }else{ + successCallback(data); + } + + });*/ +}; + + +exports.upgrade = function(user,upgradeData,logfilename,dataCallback,completeCallback,errorCallback){ + var command = '/opt/ehc-builder-scripts/bin/ozone_upgrade.sh --force --restart > /opt/ehc-builder-scripts/logs/' + logfilename + " 2> >(sed $'s,.*,\\e[31m&\\e[m,'>&1)"; + + var logFile = 'logs/upgrade/' + logfilename; + + var fs = require('filendir'); + + fs.writeFile(logFile,command,{'flag':'a'}); + //return completeCallback(command); + + ssh2_exec.executeCommand(command, + function(data){ + //Partial Data + //console.log("Data = "+ data) + completeCallback(data) + }, + function(data){ + //Complete Data + //console.log("Data =" + data) + completeCallback(data) + }, + function(error){ + //Error Data + //console.log("Error =" + error) + errorCallback(error) + } + ) + +}; + + +exports.checkUpdates = function(user,dataCallback,completeCallback,errorCallback){ + var command = '/opt/ehc-builder-scripts/bin/check_updates.sh'; + + var logFile = 'logs/upgrade/check_updates.log'; + + var fs = require('filendir'); + + fs.writeFile(logFile,command,{'flag':'a'}); + //return completeCallback(command); + + console.log("Updates command " + command); + + ssh2_exec.executeCommand(command, + function(data){ + //Partial Data + //console.log("Data = "+ data) + dataCallback(data) + }, + function(data){ + //Complete Data + //console.log("Data =" + data) + completeCallback(data) + }, + function(error){ + //Error Data + //console.log("Error =" + error) + errorCallback(error) + } + ) + +}; diff --git a/server/components/utils/dbutility.js b/server/components/utils/dbutility.js new file mode 100644 index 0000000..1e6be03 --- /dev/null +++ b/server/components/utils/dbutility.js @@ -0,0 +1,31 @@ +var _ = require('underscore'); + +exports.updateDocument = function(doc, SchemaTarget, data) { + for (var field in SchemaTarget.schema.paths) { + if ((field !== '_id') && (field !== '__v')) { + var newValue = getObjValue(field, data); + if (newValue !== undefined) { + setObjValue(field, doc, newValue); + } + } + } + return doc; +}; + +function getObjValue(field, data) { + return _.reduce(field.split("."), function(obj, f) { + if(obj) return obj[f]; + }, data); +} + +function setObjValue(field, data, value) { + var fieldArr = field.split('.'); + return _.reduce(fieldArr, function(o, f, i) { + if(i == fieldArr.length-1) { + o[f] = value; + } else { + if(!o[f]) o[f] = {}; + } + return o[f]; + }, data); +} diff --git a/server/config/environment/development.js b/server/config/environment/development.js new file mode 100644 index 0000000..cb08353 --- /dev/null +++ b/server/config/environment/development.js @@ -0,0 +1,16 @@ +'use strict'; +/*eslint no-process-env:0*/ + +// Development specific configuration +// ================================== +module.exports = { + + // MongoDB connection options + mongo: { + uri: 'mongodb://db/app2-dev' + }, + + // Seed database on startup + seedDB: true + +}; diff --git a/server/config/environment/index.js b/server/config/environment/index.js new file mode 100644 index 0000000..189861c --- /dev/null +++ b/server/config/environment/index.js @@ -0,0 +1,66 @@ +'use strict'; +/*eslint no-process-env:0*/ + +import path from 'path'; +import _ from 'lodash'; + +/*function requiredProcessEnv(name) { + if(!process.env[name]) { + throw new Error('You must set the ' + name + ' environment variable'); + } + return process.env[name]; +}*/ + +// All configurations will extend these options +// ============================================ +var all = { + env: process.env.NODE_ENV, + + // Root path of server + root: path.normalize(`${__dirname}/../../..`), + + // Browser-sync port + browserSyncPort: process.env.BROWSER_SYNC_PORT || 3000, + + // Server port + port: process.env.PORT || 9000, + + // Server IP + ip: process.env.IP || '0.0.0.0', + + // Should we populate the DB with sample data? + seedDB: false, + + // Secret for session, you will want to change this and make it an environment variable + secrets: { + session: 'app2-secret' + }, + + // MongoDB connection options + mongo: { + options: { + db: { + safe: true + } + } + }, + + facebook: { + clientID: process.env.FACEBOOK_ID || 'id', + clientSecret: process.env.FACEBOOK_SECRET || 'secret', + callbackURL: `${process.env.DOMAIN || ''}/auth/facebook/callback` + }, + + google: { + clientID: process.env.GOOGLE_ID || 'id', + clientSecret: process.env.GOOGLE_SECRET || 'secret', + callbackURL: `${process.env.DOMAIN || ''}/auth/google/callback` + } +}; + +// Export the config object based on the NODE_ENV +// ============================================== +module.exports = _.merge( + all, + require('./shared'), + require(`./${process.env.NODE_ENV}.js`) || {}); diff --git a/server/config/environment/production.js b/server/config/environment/production.js new file mode 100644 index 0000000..6f85c5d --- /dev/null +++ b/server/config/environment/production.js @@ -0,0 +1,24 @@ +'use strict'; +/*eslint no-process-env:0*/ + +// Production specific configuration +// ================================= +module.exports = { + // Server IP + ip: process.env.OPENSHIFT_NODEJS_IP + || process.env.ip + || undefined, + + // Server port + port: process.env.OPENSHIFT_NODEJS_PORT + || process.env.PORT + || 8080, + + // MongoDB connection options + mongo: { + uri: process.env.MONGODB_URI + || process.env.MONGOHQ_URL + || process.env.OPENSHIFT_MONGODB_DB_URL + process.env.OPENSHIFT_APP_NAME + || 'mongodb://localhost/app2' + } +}; diff --git a/server/config/environment/shared.js b/server/config/environment/shared.js new file mode 100644 index 0000000..777032e --- /dev/null +++ b/server/config/environment/shared.js @@ -0,0 +1,11 @@ +'use strict'; + +exports = module.exports = { + // List of user roles + userRoles: ['guest', 'user', 'admin'], + 'scriptEngine' : { + 'host' : 'localhost', + 'user' : 'root', + 'password' : 'P@ssw0rd@123' + } +}; diff --git a/server/config/environment/test.js b/server/config/environment/test.js new file mode 100644 index 0000000..b748f8e --- /dev/null +++ b/server/config/environment/test.js @@ -0,0 +1,21 @@ +'use strict'; +/*eslint no-process-env:0*/ + +// Test specific configuration +// =========================== +module.exports = { + // MongoDB connection options + mongo: { + uri: 'mongodb://localhost/app2-test' + }, + sequelize: { + uri: 'sqlite://', + options: { + logging: false, + storage: 'test.sqlite', + define: { + timestamps: false + } + } + } +}; diff --git a/server/config/express.js b/server/config/express.js new file mode 100644 index 0000000..c4f938a --- /dev/null +++ b/server/config/express.js @@ -0,0 +1,133 @@ +/** + * Express configuration + */ + +'use strict'; + +import express from 'express'; +import favicon from 'serve-favicon'; +import morgan from 'morgan'; +import shrinkRay from 'shrink-ray'; +import bodyParser from 'body-parser'; +import methodOverride from 'method-override'; +import cookieParser from 'cookie-parser'; +import errorHandler from 'errorhandler'; +import path from 'path'; +import lusca from 'lusca'; +import config from './environment'; +import passport from 'passport'; +import session from 'express-session'; +import connectMongo from 'connect-mongo'; +import mongoose from 'mongoose'; +var MongoStore = connectMongo(session); + +export default function(app) { + var env = app.get('env'); + + if(env === 'development' || env === 'test') { + app.use(express.static(path.join(config.root, '.tmp'))); + } + + if(env === 'production') { + app.use(favicon(path.join(config.root, 'client', 'favicon.ico'))); + } + + app.set('appPath', path.join(config.root, 'client')); + app.use(express.static(app.get('appPath'))); + app.use(morgan('dev')); + + app.set('views', `${config.root}/server/views`); + app.engine('html', require('ejs').renderFile); + app.set('view engine', 'html'); + app.use(shrinkRay()); + app.use(bodyParser.urlencoded({ extended: false })); + app.use(bodyParser.json()); + app.use(methodOverride()); + app.use(cookieParser()); + app.use(passport.initialize()); + + + // Persist sessions with MongoStore / sequelizeStore + // We need to enable sessions for passport-twitter because it's an + // oauth 1.0 strategy, and Lusca depends on sessions + app.use(session({ + secret: config.secrets.session, + saveUninitialized: true, + resave: false, + store: new MongoStore({ + mongooseConnection: mongoose.connection, + db: 'app2' + }) + })); + + /** + * Lusca - express server security + * https://github.com/krakenjs/lusca + */ + if(env !== 'test' && !process.env.SAUCE_USERNAME) { + app.use(lusca({ + csrf: { + angular: true + }, + xframe: 'SAMEORIGIN', + hsts: { + maxAge: 31536000, //1 year, in seconds + includeSubDomains: true, + preload: true + }, + xssProtection: true + })); + } + + if(env === 'development') { + const webpackDevMiddleware = require('webpack-dev-middleware'); + const stripAnsi = require('strip-ansi'); + const webpack = require('webpack'); + const makeWebpackConfig = require('../../webpack.make'); + const webpackConfig = makeWebpackConfig({ DEV: true }); + const compiler = webpack(webpackConfig); + const browserSync = require('browser-sync').create(); + + /** + * Run Browsersync and use middleware for Hot Module Replacement + */ + browserSync.init({ + open: false, + logFileChanges: false, + proxy: `localhost:${config.port}`, + ws: true, + middleware: [ + webpackDevMiddleware(compiler, { + noInfo: false, + stats: { + colors: true, + timings: true, + chunks: false + } + }) + ], + port: config.browserSyncPort, + plugins: ['bs-fullscreen-message'] + }); + + /** + * Reload all devices when bundle is complete + * or send a fullscreen error message to the browser instead + */ + compiler.plugin('done', function(stats) { + console.log('webpack done hook'); + if(stats.hasErrors() || stats.hasWarnings()) { + return browserSync.sockets.emit('fullscreen:message', { + title: 'Webpack Error:', + body: stripAnsi(stats.toString()), + timeout: 100000 + }); + } + browserSync.reload(); + }); + } + + if(env === 'development' || env === 'test') { + app.use(errorHandler()); // Error handler - has to be last + } +} diff --git a/server/config/local.env.sample.js b/server/config/local.env.sample.js new file mode 100644 index 0000000..c929c39 --- /dev/null +++ b/server/config/local.env.sample.js @@ -0,0 +1,20 @@ +'use strict'; + +// Use local.env.js for environment variables that will be set when the server starts locally. +// Use for your api keys, secrets, etc. This file should not be tracked by git. +// +// You will need to set these on the server you deploy to. + +module.exports = { + DOMAIN: 'http://localhost:9000', + SESSION_SECRET: 'app2-secret', + + FACEBOOK_ID: 'app-id', + FACEBOOK_SECRET: 'secret', + + GOOGLE_ID: 'app-id', + GOOGLE_SECRET: 'secret', + + // Control debug level for modules using visionmedia/debug + DEBUG: '' +}; diff --git a/server/config/seed.js b/server/config/seed.js new file mode 100644 index 0000000..02a5b7f --- /dev/null +++ b/server/config/seed.js @@ -0,0 +1,66 @@ +/** + * Populate DB with sample data on server start + * to disable, edit config/environment/index.js, and set `seedDB: false` + */ + +'use strict'; +import Thing from '../api/thing/thing.model'; +import User from '../api/user/user.model'; +import config from './environment/'; + +export default function seedDatabaseIfNeeded() { + if(config.seedDB) { + Thing.find({}).remove() + .then(() => { + let thing = Thing.create({ + name: 'Development Tools', + info: 'Integration with popular tools such as Webpack, Gulp, Babel, TypeScript, Karma, ' + + 'Mocha, ESLint, Node Inspector, Livereload, Protractor, Pug, ' + + 'Stylus, Sass, and Less.' + }, { + name: 'Server and Client integration', + info: 'Built with a powerful and fun stack: MongoDB, Express, ' + + 'AngularJS, and Node.' + }, { + name: 'Smart Build System', + info: 'Build system ignores `spec` files, allowing you to keep ' + + 'tests alongside code. Automatic injection of scripts and ' + + 'styles into your index.html' + }, { + name: 'Modular Structure', + info: 'Best practice client and server structures allow for more ' + + 'code reusability and maximum scalability' + }, { + name: 'Optimized Build', + info: 'Build process packs up your templates as a single JavaScript ' + + 'payload, minifies your scripts/css/images, and rewrites asset ' + + 'names for caching.' + }, { + name: 'Deployment Ready', + info: 'Easily deploy your app to Heroku or Openshift with the heroku ' + + 'and openshift subgenerators' + }); + return thing; + }) + .then(() => console.log('finished populating things')) + .catch(err => console.log('error populating things', err)); + + User.find({}).remove() + .then(() => { + User.create({ + provider: 'local', + name: 'Test User', + email: 'test@example.com', + password: 'test' + }, { + provider: 'local', + role: 'admin', + name: 'Admin', + email: 'admin@example.com', + password: 'admin' + }) + .then(() => console.log('finished populating users')) + .catch(err => console.log('error populating users', err)); + }); + } +} diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..cc279f5 --- /dev/null +++ b/server/index.js @@ -0,0 +1,12 @@ +'use strict'; + +// Set default node environment to development +var env = process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + +if(env === 'development' || env === 'test') { + // Register the Babel require hook + require('babel-register'); +} + +// Export the application +exports = module.exports = require('./app'); diff --git a/server/routes.js b/server/routes.js new file mode 100644 index 0000000..6cf7f0a --- /dev/null +++ b/server/routes.js @@ -0,0 +1,29 @@ +/** + * Main application routes + */ + +'use strict'; + +import errors from './components/errors'; +import path from 'path'; + +export default function(app) { + // Insert routes below + app.use('/api/custom_modules', require('./api/custom_module')); + app.use('/api/ansible', require('./api/ansible')); + app.use('/api/projects', require('./api/project')); + app.use('/api/things', require('./api/thing')); + app.use('/api/users', require('./api/user')); + + app.use('/auth', require('./auth').default); + + // All undefined asset or api routes should return a 404 + app.route('/:url(api|auth|components|app|bower_components|assets)/*') + .get(errors[404]); + + // All other routes should redirect to the index.html + app.route('/*') + .get((req, res) => { + res.sendFile(path.resolve(`${app.get('appPath')}/index.html`)); + }); +} diff --git a/server/views/404.html b/server/views/404.html new file mode 100644 index 0000000..ec98e3c --- /dev/null +++ b/server/views/404.html @@ -0,0 +1,157 @@ + + + + + Page Not Found :( + + + +
+

Not found :(

+

Sorry, but the page you were trying to view does not exist.

+

It looks like this was the result of either:

+
    +
  • a mistyped address
  • +
  • an out-of-date link
  • +
+ + +
+ + diff --git a/spec.js b/spec.js new file mode 100644 index 0000000..5b64e0c --- /dev/null +++ b/spec.js @@ -0,0 +1,12 @@ +'use strict'; +/*eslint-env node*/ +var testsContext; + +require('babel-polyfill'); +require('angular'); +require('angular-mocks'); +require('./client/components/ui-router/ui-router.mock'); + + +testsContext = require.context('./client', true, /\.spec\.js$/); +testsContext.keys().forEach(testsContext); diff --git a/webpack.build.js b/webpack.build.js new file mode 100644 index 0000000..2bf43c0 --- /dev/null +++ b/webpack.build.js @@ -0,0 +1,8 @@ +/** + * Webpack config for builds + */ +module.exports = require('./webpack.make')({ + BUILD: true, + TEST: false, + DEV: false +}); diff --git a/webpack.dev.js b/webpack.dev.js new file mode 100644 index 0000000..491f6e9 --- /dev/null +++ b/webpack.dev.js @@ -0,0 +1,8 @@ +/** + * Webpack config for development + */ +module.exports = require('./webpack.make')({ + BUILD: false, + TEST: false, + DEV: true +}); diff --git a/webpack.make.js b/webpack.make.js new file mode 100644 index 0000000..d5e603b --- /dev/null +++ b/webpack.make.js @@ -0,0 +1,363 @@ +'use strict'; +/*eslint-env node*/ +var webpack = require('webpack'); +var autoprefixer = require('autoprefixer'); +var HtmlWebpackPlugin = require('html-webpack-plugin'); +var HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); +var ExtractTextPlugin = require('extract-text-webpack-plugin'); +var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin; +var fs = require('fs'); +var path = require('path'); +var ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin; + +module.exports = function makeWebpackConfig(options) { + /** + * Environment type + * BUILD is for generating minified builds + * TEST is for generating test builds + */ + var BUILD = !!options.BUILD; + var TEST = !!options.TEST; + var E2E = !!options.E2E; + var DEV = !!options.DEV; + + /** + * Config + * Reference: http://webpack.github.io/docs/configuration.html + * This is the object where all configuration gets set + */ + var config = {}; + + /** + * Entry + * Reference: http://webpack.github.io/docs/configuration.html#entry + * Should be an empty object if it's generating a test build + * Karma will set this when it's a test build + */ + if(TEST) { + config.entry = {}; + } else { + config.entry = { + app: './client/app/app.js', + polyfills: './client/polyfills.js', + vendor: [ + 'angular', + 'angular-animate', + 'angular-aria', + 'angular-cookies', + 'angular-resource', + + 'angular-sanitize', + + 'angular-ui-bootstrap', + 'angular-ui-router', + 'lodash' + ] + }; + } + + /** + * Output + * Reference: http://webpack.github.io/docs/configuration.html#output + * Should be an empty object if it's generating a test build + * Karma will handle setting it up for you when it's a test build + */ + if(TEST) { + config.output = {}; + } else { + config.output = { + // Absolute output directory + path: BUILD ? path.join(__dirname, '/dist/client/') : path.join(__dirname, '/.tmp/'), + + // Output path from the view of the page + // Uses webpack-dev-server in development + publicPath: BUILD || DEV || E2E ? '/' : `http://localhost:${8080}/`, + //publicPath: BUILD ? '/' : 'http://localhost:' + env.port + '/', + + // Filename for entry points + // Only adds hash in build mode + filename: BUILD ? '[name].[hash].js' : '[name].bundle.js', + + // Filename for non-entry points + // Only adds hash in build mode + chunkFilename: BUILD ? '[name].[hash].js' : '[name].bundle.js' + }; + } + + + + if(TEST) { + config.resolve = { + modulesDirectories: [ + 'node_modules' + ], + extensions: ['', '.js', '.ts'] + }; + } + + /** + * Devtool + * Reference: http://webpack.github.io/docs/configuration.html#devtool + * Type of sourcemap to use per build type + */ + if(TEST) { + config.devtool = 'inline-source-map'; + } else if(BUILD || DEV) { + config.devtool = 'source-map'; + } else { + config.devtool = 'eval'; + } + + /** + * Loaders + * Reference: http://webpack.github.io/docs/configuration.html#module-loaders + * List: http://webpack.github.io/docs/list-of-loaders.html + * This handles most of the magic responsible for converting modules + */ + + + config.babel = { + shouldPrintComment(commentContents) { + // keep `/*@ngInject*/` + return /@ngInject/.test(commentContents); + } + } + + // Initialize module + config.module = { + noParse: /.*yaml\.min\.js/, + preLoaders: [ + { test: /\.json$/, loader: "json" } + ], + loaders: [{ + // JS LOADER + // Reference: https://github.com/babel/babel-loader + // Transpile .js files using babel-loader + // Compiles ES6 and ES7 into ES5 code + test: /\.js$/, + loader: 'babel', + include: [ + path.resolve(__dirname, 'client/'), + path.resolve(__dirname, 'node_modules/lodash-es/') + ] + }, { + // TS LOADER + // Reference: https://github.com/s-panferov/awesome-typescript-loader + // Transpile .ts files using awesome-typescript-loader + test: /\.ts$/, + loader: 'awesome-typescript-loader', + query: { + tsconfig: path.resolve(__dirname, 'tsconfig.client.json') + }, + include: [ + path.resolve(__dirname, 'client/') + ] + }, { + // ASSET LOADER + // Reference: https://github.com/webpack/file-loader + // Copy png, jpg, jpeg, gif, svg, woff, woff2, ttf, eot files to output + // Rename the file using the asset hash + // Pass along the updated reference to your code + // You can add here any file extension you want to get copied to your output + test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)([\?]?.*)$/, + loader: 'file' + }, { + + // HTML LOADER + // Reference: https://github.com/webpack/raw-loader + // Allow loading html through js + test: /\.html$/, + loader: 'raw' + }, { + // CSS LOADER + // Reference: https://github.com/webpack/css-loader + // Allow loading css through js + // + // Reference: https://github.com/postcss/postcss-loader + // Postprocess your css with PostCSS plugins + test: /\.css$/, + loader: !TEST + // Reference: https://github.com/webpack/extract-text-webpack-plugin + // Extract css files in production builds + // + // Reference: https://github.com/webpack/style-loader + // Use style-loader in development for hot-loading + ? ExtractTextPlugin.extract('style', 'css!postcss') + // Reference: https://github.com/webpack/null-loader + // Skip loading css in test mode + : 'null' + }] + }; + + config.module.postLoaders = [{ + test: /\.js$/, + loader: 'ng-annotate?single_quotes' + }]; + + // ISPARTA INSTRUMENTER LOADER + // Reference: https://github.com/ColCh/isparta-instrumenter-loader + // Instrument JS files with Isparta for subsequent code coverage reporting + // Skips node_modules and spec files + if(TEST) { + config.module.preLoaders.push({ + //delays coverage til after tests are run, fixing transpiled source coverage error + test: /\.js$/, + exclude: /(node_modules|spec\.js|mock\.js)/, + loader: 'isparta-instrumenter', + query: { + babel: { + // optional: ['runtime', 'es7.classProperties', 'es7.decorators'] + } + } + }); + } + + + /** + * PostCSS + * Reference: https://github.com/postcss/autoprefixer-core + * Add vendor prefixes to your css + */ + config.postcss = [ + autoprefixer({ + browsers: ['last 2 version'] + }) + ]; + + /** + * Plugins + * Reference: http://webpack.github.io/docs/configuration.html#plugins + * List: http://webpack.github.io/docs/list-of-plugins.html + */ + config.plugins = [ + /* + * Plugin: ForkCheckerPlugin + * Description: Do type checking in a separate process, so webpack don't need to wait. + * + * See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse + */ + new ForkCheckerPlugin(), + + // Reference: https://github.com/webpack/extract-text-webpack-plugin + // Extract css files + // Disabled when in test mode or not in build mode + new ExtractTextPlugin('[name].[hash].css', { + disable: !BUILD || TEST + }) + ]; + + if(!TEST) { + config.plugins.push(new CommonsChunkPlugin({ + name: 'vendor', + + // filename: "vendor.js" + // (Give the chunk a different name) + + minChunks: Infinity + // (with more entries, this ensures that no other module + // goes into the vendor chunk) + })); + } + + // Skip rendering index.html in test mode + // Reference: https://github.com/ampedandwired/html-webpack-plugin + // Render index.html + if(!TEST) { + let htmlConfig = { + template: 'client/_index.html', + filename: '../client/index.html', + alwaysWriteToDisk: true + } + config.plugins.push( + new HtmlWebpackPlugin(htmlConfig), + new HtmlWebpackHarddiskPlugin() + ); + } + + // Add build specific plugins + if(BUILD) { + config.plugins.push( + // Reference: http://webpack.github.io/docs/list-of-plugins.html#noerrorsplugin + // Only emit files when there are no errors + new webpack.NoErrorsPlugin(), + + // Reference: http://webpack.github.io/docs/list-of-plugins.html#dedupeplugin + // Dedupe modules in the output + new webpack.optimize.DedupePlugin(), + + // Reference: http://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin + // Minify all javascript, switch loaders to minimizing mode + new webpack.optimize.UglifyJsPlugin({ + mangle: false, + output: { + comments: false + }, + compress: { + warnings: false + } + }), + + // Reference: https://webpack.github.io/docs/list-of-plugins.html#defineplugin + // Define free global variables + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: '"production"' + } + }) + ); + } + + if(DEV) { + config.plugins.push( + // Reference: https://webpack.github.io/docs/list-of-plugins.html#defineplugin + // Define free global variables + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: '"development"' + } + }) + ); + } + + config.cache = DEV; + + if(TEST) { + config.stats = { + colors: true, + reasons: true + }; + config.debug = false; + } + + /** + * Dev server configuration + * Reference: http://webpack.github.io/docs/configuration.html#devserver + * Reference: http://webpack.github.io/docs/webpack-dev-server.html + */ + config.devServer = { + contentBase: './client/', + stats: { + modules: false, + cached: false, + colors: true, + chunk: false + } + }; + + config.node = { + global: 'window', + process: true, + crypto: 'empty', + clearImmediate: false, + setImmediate: false, + fs: "empty" + }; + + config.resolve = { + alias: { + "ace-builds": "../../node_modules/ace-builds/src-min-noconflict/ace.js" + } + }; + + return config; +}; diff --git a/webpack.test.js b/webpack.test.js new file mode 100644 index 0000000..9175a1b --- /dev/null +++ b/webpack.test.js @@ -0,0 +1,8 @@ +/** + * Webpack config for tests + */ +module.exports = require('./webpack.make')({ + BUILD: false, + TEST: true, + DEV: false +});