├── responses ├── .gitkeep ├── .gitignore └── diffs │ └── .gitkeep ├── assets ├── templates │ └── .gitkeep ├── favicon.ico ├── styles │ ├── pages │ │ ├── legal │ │ │ ├── terms.less │ │ │ └── privacy.less │ │ ├── account │ │ │ ├── edit-password.less │ │ │ ├── edit-profile.less │ │ │ └── account-overview.less │ │ ├── entrance │ │ │ ├── login.less │ │ │ ├── new-password.less │ │ │ ├── confirmed-email.less │ │ │ ├── signup.less │ │ │ └── forgot-password.less │ │ ├── faq.less │ │ ├── contact.less │ │ ├── 404.less │ │ ├── 498.less │ │ ├── 500.less │ │ ├── dashboard │ │ │ └── welcome.less │ │ └── homepage.less │ ├── styleguide │ │ ├── truncate.less │ │ ├── typography.less │ │ ├── index.less │ │ ├── containers.less │ │ ├── buttons.less │ │ └── colors.less │ ├── components │ │ └── ajax-button.component.less │ ├── importer.less │ ├── layout.less │ └── bootstrap-overrides.less ├── fonts │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── js │ ├── pages │ │ ├── account │ │ │ ├── account-overview.page.js │ │ │ ├── edit-password.page.js │ │ │ └── edit-profile.page.js │ │ ├── entrance │ │ │ └── login.page.js │ │ └── updateSettings.page.js │ ├── cloud.setup.js │ ├── components │ │ ├── ajax-button.component.js │ │ └── js-timestamp.component.js │ └── utilities │ │ └── open-stripe-checkout.js └── .eslintrc ├── .dockerignore ├── .foreverignore ├── .travis.yml ├── .eslintignore ├── .env ├── img ├── ss1.png ├── ss2.png ├── ss3.png └── ss4.jpg ├── config ├── locales │ └── en.json ├── policies.js ├── crontab.js ├── log.js ├── blueprints.js ├── views.js ├── session.js ├── i18n.js ├── security.js ├── routes.js ├── http.js ├── globals.js ├── datastores.js ├── sockets.js ├── env │ └── staging.js ├── custom.js └── bootstrap.js ├── views ├── 498.ejs ├── 404.ejs ├── 500.ejs ├── .eslintrc └── pages │ ├── account │ ├── account-overview.ejs │ ├── edit-password.ejs │ └── edit-profile.ejs │ ├── entrance │ └── login.ejs │ └── edit-settings.ejs ├── .sailsrc ├── dockerfile ├── .npmrc ├── tasks ├── register │ ├── syncAssets.js │ ├── linkAssets.js │ ├── compileAssets.js │ ├── linkAssetsBuild.js │ ├── linkAssetsBuildProd.js │ ├── build.js │ ├── prod.js │ ├── default.js │ ├── buildProd.js │ └── polyfill.js └── config │ ├── sync.js │ ├── less.js │ ├── clean.js │ ├── cssmin.js │ ├── babel.js │ ├── watch.js │ ├── concat.js │ ├── copy.js │ ├── uglify.js │ ├── hash.js │ └── sails-linker.js ├── api ├── controllers │ ├── account │ │ ├── view-edit-profile.js │ │ ├── view-edit-password.js │ │ ├── update-password.js │ │ ├── view-account-overview.js │ │ ├── logout.js │ │ └── update-profile.js │ ├── entrance │ │ ├── view-login.js │ │ └── login.js │ ├── settings │ │ ├── view-update-settings.js │ │ └── update-settings.js │ ├── link │ │ ├── get-links.js │ │ ├── delete-link.js │ │ ├── add-link.js │ │ └── update-link.js │ └── dashboard │ │ └── view-main.js ├── models │ ├── Setting.js │ ├── Target.js │ └── User.js ├── helpers │ ├── diff-check.js │ ├── send-request.js │ ├── send-telegram.js │ └── diff-highlight.js ├── policies │ ├── is-super-admin.js │ └── is-logged-in.js └── responses │ ├── expired.js │ └── unauthorized.js ├── docker-compose.yml ├── Gruntfile.js ├── .htmlhintrc ├── .editorconfig ├── app.js ├── .lesshintrc ├── .gitignore ├── .eslintrc ├── scripts └── rebuild-cloud-sdk.js ├── crontab └── fetchResponse.js └── package.json /responses/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /responses/.gitignore: -------------------------------------------------------------------------------- 1 | *.txt -------------------------------------------------------------------------------- /responses/diffs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | nmp-debug.log -------------------------------------------------------------------------------- /.foreverignore: -------------------------------------------------------------------------------- 1 | **/.tmp/** 2 | **/views/** 3 | **/assets/** -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | assets/dependencies/**/*.js 2 | views/**/*.ejs 3 | 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | MONGO_URL=mongodb://127.0.0.1:27017/url-tracker # MongoDB connection URL -------------------------------------------------------------------------------- /img/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-sultani/url-tracker/HEAD/img/ss1.png -------------------------------------------------------------------------------- /img/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-sultani/url-tracker/HEAD/img/ss2.png -------------------------------------------------------------------------------- /img/ss3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-sultani/url-tracker/HEAD/img/ss3.png -------------------------------------------------------------------------------- /img/ss4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-sultani/url-tracker/HEAD/img/ss4.jpg -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-sultani/url-tracker/HEAD/assets/favicon.ico -------------------------------------------------------------------------------- /assets/styles/pages/legal/terms.less: -------------------------------------------------------------------------------- 1 | #terms { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | } 5 | -------------------------------------------------------------------------------- /config/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Welcome", 3 | "A brand new app.": "A brand new app." 4 | } 5 | -------------------------------------------------------------------------------- /assets/styles/pages/legal/privacy.less: -------------------------------------------------------------------------------- 1 | #privacy { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | } 5 | -------------------------------------------------------------------------------- /assets/styles/pages/account/edit-password.less: -------------------------------------------------------------------------------- 1 | #edit-password { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | } 5 | -------------------------------------------------------------------------------- /assets/styles/pages/account/edit-profile.less: -------------------------------------------------------------------------------- 1 | #edit-profile { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | } 5 | -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-sultani/url-tracker/HEAD/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-sultani/url-tracker/HEAD/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-sultani/url-tracker/HEAD/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-sultani/url-tracker/HEAD/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /assets/styles/styleguide/truncate.less: -------------------------------------------------------------------------------- 1 | .truncate() { 2 | overflow: hidden; 3 | text-overflow: ellipsis; 4 | white-space: nowrap; 5 | } 6 | -------------------------------------------------------------------------------- /views/498.ejs: -------------------------------------------------------------------------------- 1 |
2 |

Something went wrong

3 |
4 | <%- /* Expose locals as `window.SAILS_LOCALS` :: */ exposeLocalsToBrowser() %> 5 | -------------------------------------------------------------------------------- /.sailsrc: -------------------------------------------------------------------------------- 1 | { 2 | "generators": { 3 | "modules": {} 4 | }, 5 | "_generatedWith": { 6 | "sails": "1.2.4", 7 | "sails-generate": "1.17.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /assets/styles/pages/entrance/login.less: -------------------------------------------------------------------------------- 1 | #login { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | 5 | .login-form-container { 6 | .container-sm(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /assets/styles/pages/entrance/new-password.less: -------------------------------------------------------------------------------- 1 | #new-password { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | 5 | .new-password-form { 6 | .container-sm(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /assets/styles/styleguide/typography.less: -------------------------------------------------------------------------------- 1 | // Font families: 2 | @main-font: 'Lato', sans-serif; 3 | @header-font: 'Lato', sans-serif; 4 | 5 | // Font weights: 6 | @bold: 700; 7 | @normal: 400; 8 | -------------------------------------------------------------------------------- /assets/styles/styleguide/index.less: -------------------------------------------------------------------------------- 1 | @import 'colors.less'; 2 | @import 'typography.less'; 3 | @import 'buttons.less'; 4 | @import 'animations.less'; 5 | @import 'truncate.less'; 6 | @import 'containers.less'; 7 | -------------------------------------------------------------------------------- /assets/styles/pages/faq.less: -------------------------------------------------------------------------------- 1 | #faq { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | 5 | @media (max-width: 500px) { 6 | code { 7 | word-break: break-all; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:16 3 | 4 | WORKDIR /usr/src/app 5 | 6 | COPY package*.json ./ 7 | 8 | # Install dependencies 9 | RUN npm install 10 | 11 | COPY . . 12 | 13 | EXPOSE 1337 14 | 15 | CMD ["node", "app.js"] 16 | -------------------------------------------------------------------------------- /assets/styles/pages/entrance/confirmed-email.less: -------------------------------------------------------------------------------- 1 | #confirmed-email { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | 5 | .confirmation-message { 6 | .container-sm(); 7 | text-align: center; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /assets/styles/pages/entrance/signup.less: -------------------------------------------------------------------------------- 1 | #signup { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | 5 | .signup-form { 6 | .container-sm(); 7 | } 8 | 9 | .success-message { 10 | .container-sm(); 11 | text-align: center; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /views/404.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

404

4 |
5 |

The page you seek doesn't exist.

6 |
7 |

Page not found!

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /assets/styles/pages/entrance/forgot-password.less: -------------------------------------------------------------------------------- 1 | #forgot-password { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | 5 | .forgot-form { 6 | .container-sm(); 7 | } 8 | 9 | .success-message { 10 | .container-sm(); 11 | text-align: center; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /assets/styles/styleguide/containers.less: -------------------------------------------------------------------------------- 1 | .container-sm() { 2 | width: 100%; 3 | max-width: 450px; 4 | margin-left: auto; 5 | margin-right: auto; 6 | } 7 | 8 | .container-md() { 9 | width: 100%; 10 | max-width: 650px; 11 | margin-left: auto; 12 | margin-right: auto; 13 | } 14 | -------------------------------------------------------------------------------- /assets/styles/pages/contact.less: -------------------------------------------------------------------------------- 1 | #contact { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | 5 | .contact-form { 6 | .container-md(); 7 | textarea { 8 | height: 100px; 9 | } 10 | } 11 | 12 | .success-message { 13 | .container-sm(); 14 | text-align: center; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /assets/styles/styleguide/buttons.less: -------------------------------------------------------------------------------- 1 | .btn-reset() { 2 | border-top: none; 3 | border-bottom: none; 4 | border-left: none; 5 | border-right: none; 6 | background: transparent; 7 | font-family: inherit; 8 | cursor: pointer; 9 | &:focus { 10 | border-image: none; 11 | outline: none; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ###################### 2 | # ╔╗╔╔═╗╔╦╗┬─┐┌─┐ # 3 | # ║║║╠═╝║║║├┬┘│ # 4 | # o╝╚╝╩ ╩ ╩┴└─└─┘ # 5 | ###################### 6 | 7 | # Hide NPM log output unless it is related to an error of some kind: 8 | loglevel=error 9 | 10 | # Make "npm audit" an opt-in thing for subsequent installs within this app: 11 | audit=false 12 | -------------------------------------------------------------------------------- /assets/styles/pages/account/account-overview.less: -------------------------------------------------------------------------------- 1 | #account-overview { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | 5 | .account-settings-button { 6 | width: 150px; 7 | } 8 | 9 | .remove-button { 10 | color: @brand; 11 | text-decoration: underline; 12 | cursor: pointer; 13 | &:hover { 14 | color: @text-normal; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /assets/styles/pages/404.less: -------------------------------------------------------------------------------- 1 | [id='404'] { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | 5 | .container { 6 | .container-md(); 7 | } 8 | 9 | .mobile-spacer { 10 | display: none; 11 | } 12 | 13 | @media (max-width: 540px) { 14 | br { 15 | display: none; 16 | } 17 | .mobile-spacer { 18 | display: inline; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /assets/styles/pages/498.less: -------------------------------------------------------------------------------- 1 | [id='498'] { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | 5 | .container { 6 | .container-md(); 7 | } 8 | 9 | .mobile-spacer { 10 | display: none; 11 | } 12 | 13 | @media (max-width: 540px) { 14 | br { 15 | display: none; 16 | } 17 | .mobile-spacer { 18 | display: inline; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /assets/styles/pages/500.less: -------------------------------------------------------------------------------- 1 | [id='500'] { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | 5 | .container { 6 | .container-md(); 7 | } 8 | 9 | .mobile-spacer { 10 | display: none; 11 | } 12 | 13 | @media (max-width: 540px) { 14 | br { 15 | display: none; 16 | } 17 | .mobile-spacer { 18 | display: inline; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /assets/styles/styleguide/colors.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Color Variables 3 | */ 4 | 5 | @brand: #14acc2; 6 | 7 | @error: #B53A03; 8 | 9 | 10 | @text-normal: #000; 11 | @text-muted: lighten(@text-normal, 60%); 12 | 13 | @bg-lt-gray: #f1f1f1; 14 | @border-lt-gray: darken(@bg-lt-gray, 5%); 15 | @accent-lt-gray: darken(#fff, 5%); 16 | @accent-md-gray: darken(#fff, 25%); 17 | @accent-white: #fff; 18 | -------------------------------------------------------------------------------- /views/500.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Oh my.

4 |
5 |

We've encountered an unexpected error.

6 |
7 |

.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /tasks/register/syncAssets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/register/syncAssets.js` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * For more information see: 7 | * https://sailsjs.com/anatomy/tasks/register/sync-assets.js 8 | * 9 | */ 10 | module.exports = function(grunt) { 11 | grunt.registerTask('syncAssets', [ 12 | 'less:dev', 13 | 'sync:dev', 14 | ]); 15 | }; 16 | -------------------------------------------------------------------------------- /api/controllers/account/view-edit-profile.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 4 | friendlyName: 'View edit profile', 5 | 6 | 7 | description: 'Display "Edit profile" page.', 8 | 9 | 10 | exits: { 11 | 12 | success: { 13 | viewTemplatePath: 'pages/account/edit-profile', 14 | } 15 | 16 | }, 17 | 18 | 19 | fn: async function () { 20 | 21 | return {}; 22 | 23 | } 24 | 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /api/controllers/account/view-edit-password.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 4 | friendlyName: 'View edit password', 5 | 6 | 7 | description: 'Display "Edit password" page.', 8 | 9 | 10 | exits: { 11 | 12 | success: { 13 | viewTemplatePath: 'pages/account/edit-password' 14 | } 15 | 16 | }, 17 | 18 | 19 | fn: async function () { 20 | 21 | return {}; 22 | 23 | } 24 | 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /tasks/register/linkAssets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/register/linkAssets.js` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * For more information see: 7 | * https://sailsjs.com/anatomy/tasks/register/link-assets.js 8 | * 9 | */ 10 | module.exports = function(grunt) { 11 | grunt.registerTask('linkAssets', [ 12 | 'sails-linker:devJs', 13 | 'sails-linker:devStyles', 14 | ]); 15 | }; 16 | -------------------------------------------------------------------------------- /tasks/register/compileAssets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/register/compileAssets.js` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * For more information see: 7 | * https://sailsjs.com/anatomy/tasks/register/compile-assets.js 8 | * 9 | */ 10 | module.exports = function(grunt) { 11 | grunt.registerTask('compileAssets', [ 12 | 'clean:dev', 13 | 'less:dev', 14 | 'copy:dev', 15 | ]); 16 | }; 17 | -------------------------------------------------------------------------------- /tasks/register/linkAssetsBuild.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/register/linkAssetsBuild.js` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * For more information see: 7 | * https://sailsjs.com/anatomy/tasks/register/link-assets-build.js 8 | * 9 | */ 10 | module.exports = function(grunt) { 11 | grunt.registerTask('linkAssetsBuild', [ 12 | 'sails-linker:devJsBuild', 13 | 'sails-linker:devStylesBuild', 14 | ]); 15 | }; 16 | -------------------------------------------------------------------------------- /config/policies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Policy Mappings 3 | * (sails.config.policies) 4 | * 5 | * Policies are simple functions which run **before** your actions. 6 | * 7 | * For more information on configuring policies, check out: 8 | * https://sailsjs.com/docs/concepts/policies 9 | */ 10 | 11 | module.exports.policies = { 12 | 13 | '*': 'is-logged-in', 14 | 15 | // Bypass the `is-logged-in` policy for: 16 | 'entrance/*': true, 17 | 'account/logout': true, 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /tasks/register/linkAssetsBuildProd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/register/linkAssetsBuildProd.js` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * For more information see: 7 | * https://sailsjs.com/anatomy/tasks/register/link-assets-build-prod.js 8 | * 9 | */ 10 | module.exports = function(grunt) { 11 | grunt.registerTask('linkAssetsBuildProd', [ 12 | 'sails-linker:prodJsBuild', 13 | 'sails-linker:prodStylesBuild', 14 | ]); 15 | }; 16 | -------------------------------------------------------------------------------- /api/models/Setting.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Setting.js 3 | */ 4 | 5 | module.exports = { 6 | 7 | attributes: { 8 | 9 | app: { 10 | type: 'string', 11 | defaultsTo: 'this', 12 | }, 13 | reportToTelegram: { 14 | type: 'boolean', 15 | defaultsTo: false, 16 | }, 17 | 18 | telegramToken: { 19 | type: 'string', 20 | defaultsTo: '', 21 | }, 22 | 23 | telegramChatID: { 24 | type: 'string', 25 | defaultsTo: '', 26 | }, 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | mongo: 4 | image: mongo:3.6 5 | container_name: mongodb 6 | ports: 7 | - "27017:27017" 8 | networks: 9 | - app-network 10 | 11 | sails-app: 12 | build: . 13 | depends_on: 14 | - mongo 15 | environment: 16 | MONGO_URL: "mongodb://mongodb:27017/url-tracker" 17 | ports: 18 | - "1337:1337" 19 | networks: 20 | - app-network 21 | 22 | networks: 23 | app-network: 24 | driver: bridge 25 | -------------------------------------------------------------------------------- /assets/styles/pages/dashboard/welcome.less: -------------------------------------------------------------------------------- 1 | #main { 2 | padding-top: 75px; 3 | padding-bottom: 75px; 4 | } 5 | del { 6 | background-color: red; 7 | } 8 | ins { 9 | background-color: greenyellow; 10 | } 11 | 12 | pre { 13 | white-space: pre-wrap; /* Since CSS 2.1 */ 14 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 15 | } 16 | 17 | code { 18 | background-color: #eee; 19 | border: 1px solid #999; 20 | display: block; 21 | padding: 20px; 22 | word-break: break-all; 23 | } 24 | -------------------------------------------------------------------------------- /assets/styles/components/ajax-button.component.less: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 4 | * 5 | * App-wide styles for our ajax buttons. 6 | */ 7 | 8 | [parasails-component='ajax-button'] { 9 | .button-loader, .button-loading { 10 | .loader(); 11 | display: none; 12 | .loading-dot { 13 | width: 7px; 14 | height: 7px; 15 | margin: 0px 3px; 16 | display: inline; 17 | } 18 | } 19 | &.syncing { 20 | .button-loader, .button-loading { 21 | display: inline-block; 22 | } 23 | .button-text { 24 | display: none; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/controllers/entrance/view-login.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 4 | friendlyName: 'View login', 5 | 6 | 7 | description: 'Display "Login" page.', 8 | 9 | 10 | exits: { 11 | 12 | success: { 13 | viewTemplatePath: 'pages/entrance/login', 14 | }, 15 | 16 | redirect: { 17 | description: 'The requesting user is already logged in.', 18 | responseType: 'redirect' 19 | } 20 | 21 | }, 22 | 23 | 24 | fn: async function () { 25 | 26 | if (this.req.me) { 27 | throw { redirect: '/' }; 28 | } 29 | 30 | return {}; 31 | 32 | } 33 | 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /api/controllers/account/update-password.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 4 | friendlyName: 'Update password', 5 | 6 | 7 | description: 'Update the password for the logged-in user.', 8 | 9 | 10 | inputs: { 11 | 12 | password: { 13 | description: 'The new, unencrypted password.', 14 | example: 'abc123v2', 15 | required: true 16 | } 17 | 18 | }, 19 | 20 | 21 | fn: async function (inputs) { 22 | 23 | // Hash the new password. 24 | var hashed = await sails.helpers.passwords.hashPassword(inputs.password); 25 | 26 | // Update the record for the logged-in user. 27 | await User.updateOne({ id: this.req.me.id }) 28 | .set({ 29 | password: hashed 30 | }); 31 | 32 | } 33 | 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /api/helpers/diff-check.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 4 | friendlyName: 'Diff check', 5 | 6 | 7 | description: '', 8 | 9 | 10 | inputs: { 11 | 12 | res1: { 13 | description: 'First page', 14 | example: 'HTML page', 15 | type: 'string', 16 | }, 17 | res2: { 18 | description: 'Second page', 19 | example: 'HTML page', 20 | type: 'string', 21 | }, 22 | }, 23 | 24 | exits: { 25 | 26 | success: { 27 | 28 | }, 29 | 30 | }, 31 | 32 | 33 | fn: async function (inputs,exits) { 34 | 35 | let leven = require('leven'); 36 | 37 | let acceptedChange = leven(inputs.res1, inputs.res2); 38 | 39 | return exits.success(acceptedChange); 40 | 41 | 42 | } 43 | 44 | 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gruntfile 3 | * 4 | * This Node script is executed when you run `grunt`-- and also when 5 | * you run `sails lift` (provided the grunt hook is installed and 6 | * hasn't been disabled). 7 | * 8 | * WARNING: 9 | * Unless you know what you're doing, you shouldn't change this file. 10 | * Check out the `tasks/` directory instead. 11 | * 12 | * For more information see: 13 | * https://sailsjs.com/anatomy/Gruntfile.js 14 | */ 15 | module.exports = function(grunt) { 16 | 17 | var loadGruntTasks = require('sails-hook-grunt/accessible/load-grunt-tasks'); 18 | 19 | // Load Grunt task configurations (from `tasks/config/`) and Grunt 20 | // task registrations (from `tasks/register/`). 21 | loadGruntTasks(__dirname, grunt); 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /api/controllers/account/view-account-overview.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 4 | friendlyName: 'View account overview', 5 | 6 | 7 | description: 'Display "Account Overview" page.', 8 | 9 | 10 | exits: { 11 | 12 | success: { 13 | viewTemplatePath: 'pages/account/account-overview', 14 | } 15 | 16 | }, 17 | 18 | 19 | fn: async function () { 20 | 21 | // If billing features are enabled, include our configured Stripe.js 22 | // public key in the view locals. Otherwise, leave it as undefined. 23 | return { 24 | stripePublishableKey: sails.config.custom.enableBillingFeatures? sails.config.custom.stripePublishableKey : undefined, 25 | platformName: sails.config.custom.platformName, 26 | }; 27 | 28 | } 29 | 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /tasks/register/build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/register/build.js` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This Grunt tasklist will be executed if you run `sails www` or 7 | * `grunt build` in a development environment. 8 | * 9 | * For more information see: 10 | * https://sailsjs.com/anatomy/tasks/register/build.js 11 | * 12 | */ 13 | module.exports = function(grunt) { 14 | grunt.registerTask('build', [ 15 | // 'polyfill:dev', //« uncomment to ALSO transpile during development (for broader browser compat.) 16 | 'compileAssets', 17 | // 'babel', //« uncomment to ALSO transpile during development (for broader browser compat.) 18 | 'linkAssetsBuild', 19 | 'clean:build', 20 | 'copy:build' 21 | ]); 22 | }; 23 | -------------------------------------------------------------------------------- /api/controllers/settings/view-update-settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 4 | friendlyName: 'View edit profile', 5 | 6 | 7 | description: 'Display "Edit profile" page.', 8 | 9 | 10 | exits: { 11 | 12 | success: { 13 | responseType: 'view', 14 | viewTemplatePath: 'pages/edit-settings', 15 | 16 | } 17 | 18 | }, 19 | 20 | 21 | fn: async function () { 22 | 23 | let data = await Setting.findOne({app:'this'}); 24 | 25 | let reportToTelegram = data.reportToTelegram; 26 | let telegramToken = data.telegramToken; 27 | let telegramChatID = data.telegramChatID; 28 | 29 | 30 | return { 31 | reportToTelegram: reportToTelegram, 32 | telegramToken: telegramToken, 33 | telegramChatID:telegramChatID 34 | }; 35 | } 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "alt-require": true, 3 | "attr-lowercase": ["viewBox"], 4 | "attr-no-duplication": true, 5 | "attr-unsafe-chars": true, 6 | "attr-value-double-quotes": true, 7 | "attr-value-not-empty": false, 8 | "csslint": false, 9 | "doctype-first": false, 10 | "doctype-html5": true, 11 | "head-script-disabled": false, 12 | "href-abs-or-rel": false, 13 | "id-class-ad-disabled": true, 14 | "id-class-value": false, 15 | "id-unique": true, 16 | "inline-script-disabled": true, 17 | "inline-style-disabled": false, 18 | "jshint": false, 19 | "space-tab-mixed-disabled": "space", 20 | "spec-char-escape": false, 21 | "src-not-empty": true, 22 | "style-disabled": false, 23 | "tag-pair": true, 24 | "tag-self-close": false, 25 | "tagname-lowercase": true, 26 | "title-require": false 27 | } 28 | -------------------------------------------------------------------------------- /api/controllers/link/get-links.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | friendlyName: 'Get targets', 4 | 5 | description: 'Get an array of targets from MongoDB.', 6 | 7 | exits: { 8 | success: { 9 | description: 'Targets were retrieved successfully.', 10 | }, 11 | error: { 12 | description: 'An error occurred while retrieving targets.', 13 | } 14 | }, 15 | 16 | fn: async function () { 17 | try { 18 | var targets = await Target.find(); 19 | 20 | if (!targets || targets.length === 0) { 21 | sails.log.warn('No targets found.'); 22 | } 23 | 24 | return this.res.send(targets); 25 | } catch (error) { 26 | sails.log.error('Error retrieving targets:', error); 27 | return this.res.status(500).send('An error occurred while retrieving targets.'); 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /tasks/register/prod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/register/prod.js` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This Grunt tasklist will be executed instead of `default` when 7 | * your Sails app is lifted in a production environment (e.g. using 8 | * `NODE_ENV=production node app`). 9 | * 10 | * For more information see: 11 | * https://sailsjs.com/anatomy/tasks/register/prod.js 12 | * 13 | */ 14 | module.exports = function(grunt) { 15 | grunt.registerTask('prod', [ 16 | 'polyfill:prod', //« Remove this to skip transpilation in production (not recommended) 17 | 'compileAssets', 18 | 'babel', //« Remove this to skip transpilation in production (not recommended) 19 | 'concat', 20 | 'uglify', 21 | 'cssmin', 22 | 'sails-linker:prodJs', 23 | 'sails-linker:prodStyles', 24 | ]); 25 | }; 26 | 27 | -------------------------------------------------------------------------------- /views/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // ╔═╗╔═╗╦ ╦╔╗╔╔╦╗┬─┐┌─┐ ┌─┐┬ ┬┌─┐┬─┐┬─┐┬┌┬┐┌─┐ 3 | // ║╣ ╚═╗║ ║║║║ ║ ├┬┘│ │ │└┐┌┘├┤ ├┬┘├┬┘│ ││├┤ 4 | // o╚═╝╚═╝╩═╝╩╝╚╝ ╩ ┴└─└─┘ └─┘ └┘ └─┘┴└─┴└─┴─┴┘└─┘ 5 | // ┌─ ┌─┐┌─┐┬─┐ ┬┌┐┌┬ ┬┌┐┌┌─┐ ┌─┐┌─┐┬─┐┬┌─┐┌┬┐ ┌┬┐┌─┐┌─┐┌─┐ ─┐ 6 | // │ ├┤ │ │├┬┘ │││││ ││││├┤ └─┐│ ├┬┘│├─┘ │ │ ├─┤│ ┬└─┐ │ 7 | // └─ └ └─┘┴└─ ┴┘└┘┴─┘┴┘└┘└─┘ └─┘└─┘┴└─┴┴ ┴ ┴ ┴ ┴└─┘└─┘ ─┘ 8 | // > An .eslintrc configuration override for use in the `views/` directory. 9 | // 10 | // (This works just like assets/.eslintrc, with one minor addition) 11 | // 12 | // For more information see: 13 | // https://sailsjs.com/anatomy/views/.eslintrc 14 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 15 | "extends": [ 16 | "../assets/.eslintrc" 17 | ], 18 | "rules": { 19 | "eol-last": [0] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /api/policies/is-super-admin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * is-super-admin 3 | * 4 | * A simple policy that blocks requests from non-super-admins. 5 | * 6 | * For more about how to use policies, see: 7 | * https://sailsjs.com/config/policies 8 | * https://sailsjs.com/docs/concepts/policies 9 | * https://sailsjs.com/docs/concepts/policies/access-control-and-permissions 10 | */ 11 | module.exports = async function (req, res, proceed) { 12 | 13 | // First, check whether the request comes from a logged-in user. 14 | // > For more about where `req.me` comes from, check out this app's 15 | // > custom hook (`api/hooks/custom/index.js`). 16 | if (!req.me) { 17 | return res.unauthorized(); 18 | }//• 19 | 20 | // Then check that this user is a "super admin". 21 | if (!req.me.isSuperAdmin) { 22 | return res.forbidden(); 23 | }//• 24 | 25 | // IWMIH, we've got ourselves a "super admin". 26 | return proceed(); 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /assets/js/pages/account/account-overview.page.js: -------------------------------------------------------------------------------- 1 | parasails.registerPage('account-overview', { 2 | // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ 3 | // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ 4 | // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ 5 | data: { 6 | 7 | 8 | // Syncing/loading states for this page. 9 | 10 | // Form data 11 | formData: { /* … */ }, 12 | 13 | // Server error state for the form 14 | cloudError: '', 15 | 16 | checkoutHandler: undefined, 17 | 18 | }, 19 | 20 | // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ 21 | // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ 22 | // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ 23 | beforeMount: function (){ 24 | _.extend(this, window.SAILS_LOCALS); 25 | 26 | 27 | 28 | }, 29 | mounted: async function() { 30 | //… 31 | }, 32 | 33 | // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ 34 | // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ 35 | // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ 36 | methods: { 37 | 38 | 39 | 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /tasks/register/default.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/register/default.js` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This is the default Grunt tasklist that will be executed if you 7 | * run `grunt` in the top level directory of your app. It is also 8 | * called automatically when you start Sails in development mode using 9 | * `sails lift` or `node app` in a development environment. 10 | * 11 | * For more information see: 12 | * https://sailsjs.com/anatomy/tasks/register/default.js 13 | * 14 | */ 15 | module.exports = function (grunt) { 16 | 17 | 18 | grunt.registerTask('default', [ 19 | // 'polyfill:dev', //« uncomment to ALSO transpile during development (for broader browser compat.) 20 | 'compileAssets', 21 | // 'babel', //« uncomment to ALSO transpile during development (for broader browser compat.) 22 | 'linkAssets', 23 | 'watch' 24 | ]); 25 | 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /api/controllers/dashboard/view-main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | friendlyName: 'View main page', 4 | 5 | description: 'Display the dashboard "main" page.', 6 | 7 | exits: { 8 | success: { 9 | viewTemplatePath: 'pages/dashboard/main', 10 | description: 'Display the main page for authenticated users.' 11 | }, 12 | error: { 13 | description: 'An error occurred while fetching the targets or rendering the page.', 14 | } 15 | }, 16 | 17 | fn: async function () { 18 | try { 19 | var targets = await Target.find(); 20 | 21 | // Check if targets were retrieved successfully 22 | if (!targets) { 23 | sails.log.warn('No targets found, returning an empty array.'); 24 | } 25 | 26 | return { targets }; 27 | 28 | } catch (error) { 29 | sails.log.error('Error retrieving targets or rendering the main page:', error); 30 | throw 'error'; 31 | } 32 | } 33 | 34 | }; 35 | -------------------------------------------------------------------------------- /api/policies/is-logged-in.js: -------------------------------------------------------------------------------- 1 | /** 2 | * is-logged-in 3 | * 4 | * A simple policy that allows any request from an authenticated user. 5 | * 6 | * For more about how to use policies, see: 7 | * https://sailsjs.com/config/policies 8 | * https://sailsjs.com/docs/concepts/policies 9 | * https://sailsjs.com/docs/concepts/policies/access-control-and-permissions 10 | */ 11 | module.exports = async function (req, res, proceed) { 12 | 13 | // If `req.me` is set, then we know that this request originated 14 | // from a logged-in user. So we can safely proceed to the next policy-- 15 | // or, if this is the last policy, the relevant action. 16 | // > For more about where `req.me` comes from, check out this app's 17 | // > custom hook (`api/hooks/custom/index.js`). 18 | if (req.me) { 19 | return proceed(); 20 | } 21 | 22 | //--• 23 | // Otherwise, this request did not come from a logged-in user. 24 | return res.unauthorized(); 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /api/models/Target.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Target.js 3 | * 4 | * A link that fetched every X seconds. 5 | */ 6 | 7 | module.exports = { 8 | 9 | attributes: { 10 | 11 | description: { 12 | type: 'string', 13 | example: 'Uber S3 bucket', 14 | required: true, 15 | }, 16 | 17 | link: { 18 | type: 'string', 19 | required: true, 20 | unique: true, 21 | example: 'https://s3.amazonaws.com/yahoo_us/' 22 | }, 23 | 24 | status: { 25 | type: 'string', 26 | isIn: ['changed', 'unchanged'], 27 | defaultsTo: 'unchanged', 28 | }, 29 | 30 | keywords: { 31 | type: 'string', 32 | }, 33 | 34 | acceptedChange: { 35 | type: 'number', 36 | }, 37 | 38 | cookie: { 39 | type: 'string', 40 | }, 41 | 42 | fetchEvery: { 43 | type: 'string', 44 | isIn: ['hour', 'day', 'week', 'month'], 45 | defaultsTo: 'hour', 46 | }, 47 | 48 | 49 | 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /api/responses/expired.js: -------------------------------------------------------------------------------- 1 | /** 2 | * expired.js 3 | * 4 | * A custom response that content-negotiates the current request to either: 5 | * • serve an HTML error page about the specified token being invalid or expired 6 | * • or send back 498 (Token Expired/Invalid) with no response body. 7 | * 8 | * Example usage: 9 | * ``` 10 | * return res.expired(); 11 | * ``` 12 | * 13 | * Or with actions2: 14 | * ``` 15 | * exits: { 16 | * badToken: { 17 | * description: 'Provided token was expired, invalid, or already used up.', 18 | * responseType: 'expired' 19 | * } 20 | * } 21 | * ``` 22 | */ 23 | module.exports = function expired() { 24 | 25 | var req = this.req; 26 | var res = this.res; 27 | 28 | sails.log.verbose('Ran custom response: res.expired()'); 29 | 30 | if (req.wantsJSON) { 31 | return res.status(498).send('Token Expired/Invalid'); 32 | } 33 | else { 34 | return res.status(498).view('498'); 35 | } 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ################################################ 2 | # ╔═╗╔╦╗╦╔╦╗╔═╗╦═╗┌─┐┌─┐┌┐┌┌─┐┬┌─┐ 3 | # ║╣ ║║║ ║ ║ ║╠╦╝│ │ ││││├┤ ││ ┬ 4 | # o╚═╝═╩╝╩ ╩ ╚═╝╩╚═└─┘└─┘┘└┘└ ┴└─┘ 5 | # 6 | # > Formatting conventions for your Sails app. 7 | # 8 | # This file (`.editorconfig`) exists to help 9 | # maintain consistent formatting throughout the 10 | # files in your Sails app. 11 | # 12 | # For the sake of convention, the Sails team's 13 | # preferred settings are included here out of the 14 | # box. You can also change this file to fit your 15 | # team's preferences (for example, if all of the 16 | # developers on your team have a strong preference 17 | # for tabs over spaces), 18 | # 19 | # To review what each of these options mean, see: 20 | # http://editorconfig.org/ 21 | # 22 | ################################################ 23 | root = true 24 | 25 | [*] 26 | indent_style = space 27 | indent_size = 2 28 | end_of_line = lf 29 | charset = utf-8 30 | trim_trailing_whitespace = true 31 | insert_final_newline = true 32 | -------------------------------------------------------------------------------- /tasks/register/buildProd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/register/buildProd.js` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This Grunt tasklist will be executed instead of `build` if you 7 | * run `sails www` in a production environment, e.g.: 8 | * `NODE_ENV=production sails www` 9 | * 10 | * For more information see: 11 | * https://sailsjs.com/anatomy/tasks/register/build-prod.js 12 | * 13 | */ 14 | module.exports = function(grunt) { 15 | grunt.registerTask('buildProd', [ 16 | 'polyfill:prod', //« Remove this to skip transpilation in production (not recommended) 17 | 'compileAssets', 18 | 'babel', //« Remove this to skip transpilation in production (not recommended) 19 | 'concat', 20 | 'uglify', 21 | 'cssmin', 22 | 'hash',//« Cache-busting 23 | 'copy:beforeLinkBuildProd',//« For prettier URLs after cache-busting 24 | 'linkAssetsBuildProd', 25 | 'clean:build', 26 | 'copy:build', 27 | 'clean:afterBuildProd' 28 | ]); 29 | }; 30 | 31 | -------------------------------------------------------------------------------- /api/responses/unauthorized.js: -------------------------------------------------------------------------------- 1 | /** 2 | * unauthorized.js 3 | * 4 | * A custom response that content-negotiates the current request to either: 5 | * • log out the current user and redirect them to the login page 6 | * • or send back 401 (Unauthorized) with no response body. 7 | * 8 | * Example usage: 9 | * ``` 10 | * return res.unauthorized(); 11 | * ``` 12 | * 13 | * Or with actions2: 14 | * ``` 15 | * exits: { 16 | * badCombo: { 17 | * description: 'That email address and password combination is not recognized.', 18 | * responseType: 'unauthorized' 19 | * } 20 | * } 21 | * ``` 22 | */ 23 | module.exports = function unauthorized() { 24 | 25 | var req = this.req; 26 | var res = this.res; 27 | 28 | sails.log.verbose('Ran custom response: res.unauthorized()'); 29 | 30 | if (req.wantsJSON) { 31 | return res.sendStatus(401); 32 | } 33 | // Or log them out (if necessary) and then redirect to the login page. 34 | else { 35 | 36 | if (req.session.userId) { 37 | delete req.session.userId; 38 | } 39 | 40 | return res.redirect('/login'); 41 | } 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /assets/js/cloud.setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cloud.setup.js 3 | * 4 | * Configuration for this Sails app's generated browser SDK ("Cloud"). 5 | * 6 | * Above all, the purpose of this file is to provide endpoint definitions, 7 | * each of which corresponds with one particular route+action on the server. 8 | * 9 | * > This file was automatically generated. 10 | * > (To regenerate, run `sails run rebuild-cloud-sdk`) 11 | */ 12 | 13 | Cloud.setup({ 14 | 15 | /* eslint-disable */ 16 | methods: {"confirmEmail":{"verb":"GET","url":"/email/confirm","args":["token"]}, 17 | "logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]}, 18 | "updatePassword":{"verb":"PUT","url":"/api/v1/account/update-password","args":["password"]}, 19 | "updateSettings":{"verb":"PUT","url":"/api/v1/settings","args":["reportToTelegram","telegramToken","telegramChatID"]}, 20 | "updateProfile":{"verb":"PUT","url":"/api/v1/account/update-profile","args":["fullName","emailAddress"]}, 21 | "login":{"verb":"PUT","url":"/api/v1/entrance/login","args":["emailAddress","password","rememberMe"]}, 22 | "addLink": { "verb": "POST", "url": "/api/v1/links", "args": ["description", "link", "fetchEvery"] }} 23 | 24 | /* eslint-enable */ 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /config/crontab.js: -------------------------------------------------------------------------------- 1 | module.exports.crontab = { 2 | /* 3 | Crontab control 4 | Note: if you want to modify the cron times you can find them here. 5 | */ 6 | 7 | crons: function () { 8 | var jsonArray = []; 9 | 10 | //jsonArray.push({ interval: '* * * * *', method: 'fetchResponseHour' }); 11 | 12 | // Every hour fetching 13 | jsonArray.push({ interval: '0 * * * *', method: 'fetchResponseHour' }); 14 | // Every day fetching 15 | jsonArray.push({ interval: '0 1 * * *', method: 'fetchResponseDay' }); 16 | // Every week fetching 17 | jsonArray.push({ interval: '0 0 * * 0', method: 'fetchResponseWeek' }); 18 | // Every month fetching 19 | jsonArray.push({ interval: '0 0 1 * *', method: 'fetchResponseMonth' }); 20 | 21 | return jsonArray; 22 | }, 23 | 24 | // Cron methods for each period 25 | 26 | fetchResponseHour: function () { 27 | require('../crontab/fetchResponse.js').run('hour'); 28 | }, 29 | fetchResponseDay: function () { 30 | require('../crontab/fetchResponse.js').run('day'); 31 | }, 32 | fetchResponseWeek: function () { 33 | require('../crontab/fetchResponse.js').run('week'); 34 | }, 35 | fetchResponseMonth: function () { 36 | require('../crontab/fetchResponse.js').run('month'); 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /config/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Built-in Log Configuration 3 | * (sails.config.log) 4 | * 5 | * Configure the log level for your app, as well as the transport 6 | * (Underneath the covers, Sails uses Winston for logging, which 7 | * allows for some pretty neat custom transports/adapters for log messages) 8 | * 9 | * For more information on the Sails logger, check out: 10 | * https://sailsjs.com/docs/concepts/logging 11 | */ 12 | 13 | module.exports.log = { 14 | 15 | /*************************************************************************** 16 | * * 17 | * Valid `level` configs: i.e. the minimum log level to capture with * 18 | * sails.log.*() * 19 | * * 20 | * The order of precedence for log levels from lowest to highest is: * 21 | * silly, verbose, info, debug, warn, error * 22 | * * 23 | * You may also set the level to "silent" to suppress all logs. * 24 | * * 25 | ***************************************************************************/ 26 | 27 | // level: 'info' 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /tasks/register/polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/register/polyfill.js` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * For more information see: 7 | * https://sailsjs.com/anatomy/tasks/register/polyfill.js 8 | * 9 | */ 10 | module.exports = function(grunt) { 11 | grunt.registerTask('polyfill:prod', 'Add the polyfill file to the top of the list of files to concatenate', ()=>{ 12 | grunt.config.set('concat.js.src', [require('sails-hook-grunt/accessible/babel-polyfill')].concat(grunt.config.get('concat.js.src'))); 13 | }); 14 | grunt.registerTask('polyfill:dev', 'Add the polyfill file to the top of the list of files to copy and link', ()=>{ 15 | grunt.config.set('copy.dev.files', grunt.config.get('copy.dev.files').concat({ 16 | expand: true, 17 | cwd: require('path').dirname(require('sails-hook-grunt/accessible/babel-polyfill')), 18 | src: require('path').basename(require('sails-hook-grunt/accessible/babel-polyfill')), 19 | dest: '.tmp/public/polyfill' 20 | })); 21 | var devLinkFiles = grunt.config.get('sails-linker.devJs.files'); 22 | grunt.config.set('sails-linker.devJs.files', Object.keys(devLinkFiles).reduce((linkerConfigSoFar, glob)=>{ 23 | linkerConfigSoFar[glob] = ['.tmp/public/polyfill/polyfill.min.js'].concat(devLinkFiles[glob]); 24 | return linkerConfigSoFar; 25 | }, {})); 26 | }); 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /assets/styles/importer.less: -------------------------------------------------------------------------------- 1 | /** 2 | * importer.less 3 | * 4 | * By default, new Sails projects are configured to compile this file 5 | * from LESS to CSS. Unlike CSS files, LESS files are not compiled and 6 | * included automatically unless they are imported below. 7 | * 8 | * For more information see: 9 | * https://sailsjs.com/anatomy/assets/styles/importer-less 10 | */ 11 | 12 | // Styleguide (LESS mixins/variables only, no global selectors) 13 | @import 'styleguide/index.less'; 14 | 15 | // Overall layout (contains global selectors) 16 | @import 'bootstrap-overrides.less'; 17 | @import 'layout.less'; 18 | 19 | // Per-component styles 20 | // @import 'components/datepicker.component.less'; 21 | @import 'components/ajax-button.component.less'; 22 | 23 | // Per-page styles 24 | @import 'pages/homepage.less'; 25 | @import 'pages/dashboard/welcome.less'; 26 | @import 'pages/entrance/signup.less'; 27 | @import 'pages/entrance/confirmed-email.less'; 28 | @import 'pages/entrance/login.less'; 29 | @import 'pages/entrance/forgot-password.less'; 30 | @import 'pages/entrance/new-password.less'; 31 | @import 'pages/account/account-overview.less'; 32 | @import 'pages/account/edit-password.less'; 33 | @import 'pages/account/edit-profile.less'; 34 | @import 'pages/legal/terms.less'; 35 | @import 'pages/legal/privacy.less'; 36 | @import 'pages/faq.less'; 37 | @import 'pages/contact.less'; 38 | @import 'pages/404.less'; 39 | @import 'pages/500.less'; 40 | @import 'pages/498.less'; 41 | -------------------------------------------------------------------------------- /api/controllers/settings/update-settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | friendlyName: 'Update settings', 4 | 5 | description: 'Update application settings.', 6 | 7 | inputs: { 8 | telegramToken: { 9 | type: 'string', 10 | required: true 11 | }, 12 | telegramChatID: { 13 | type: 'string', 14 | required: true 15 | }, 16 | reportToTelegram: { 17 | type: 'boolean', 18 | required: true 19 | } 20 | }, 21 | 22 | fn: async function (inputs) { 23 | try { 24 | let telegramToken = inputs.telegramToken; 25 | let telegramChatID = inputs.telegramChatID; 26 | let reportToTelegram = inputs.reportToTelegram; 27 | 28 | // Attempt to update the settings 29 | var updatedRecords = await Setting.update({ app: 'this' }).set({ 30 | telegramToken: telegramToken, 31 | telegramChatID: telegramChatID, 32 | reportToTelegram: reportToTelegram 33 | }).fetch(); 34 | 35 | // Check if any records were updated 36 | if (updatedRecords.length > 0) { 37 | return this.res.send('1'); // Successfully updated 38 | } else { 39 | return this.res.status(404).send('No records updated'); // No records found to update 40 | } 41 | 42 | } catch (error) { 43 | // Log the error and send an error response 44 | sails.log.error('Error updating settings:', error); 45 | return this.res.status(500).send('An error occurred while updating settings'); 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /tasks/config/sync.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/config/sync` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Synchronize files from the `assets` folder to `.tmp/public`, 7 | * smashing anything that's already there. 8 | * 9 | * For more information, see: 10 | * https://sailsjs.com/anatomy/tasks/config/sync.js 11 | * 12 | */ 13 | module.exports = function(grunt) { 14 | 15 | grunt.config.set('sync', { 16 | dev: { 17 | files: [{ 18 | cwd: './assets', 19 | src: ['**/*.!(coffee|less)'], 20 | dest: '.tmp/public' 21 | }] 22 | } 23 | }); 24 | 25 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 26 | // This Grunt plugin is part of the default asset pipeline in Sails, 27 | // so it's already been automatically loaded for you at this point. 28 | // 29 | // Of course, you can always remove this Grunt plugin altogether by 30 | // deleting this file. But check this out: you can also use your 31 | // _own_ custom version of this Grunt plugin. 32 | // 33 | // Here's how: 34 | // 35 | // 1. Install it as a local dependency of your Sails app: 36 | // ``` 37 | // $ npm install grunt-sync --save-dev --save-exact 38 | // ``` 39 | // 40 | // 41 | // 2. Then uncomment the following code: 42 | // 43 | // ``` 44 | // // Load Grunt plugin from the node_modules/ folder. 45 | // grunt.loadNpmTasks('grunt-sync'); 46 | // ``` 47 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 48 | 49 | }; 50 | -------------------------------------------------------------------------------- /tasks/config/less.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/config/less` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Compile your LESS files into a CSS stylesheet. 7 | * 8 | * For more information, see: 9 | * https://sailsjs.com/anatomy/tasks/config/less.js 10 | * 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('less', { 15 | dev: { 16 | files: [{ 17 | expand: true, 18 | cwd: 'assets/styles/', 19 | src: ['importer.less'], 20 | dest: '.tmp/public/styles/', 21 | ext: '.css' 22 | }] 23 | } 24 | }); 25 | 26 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 27 | // This Grunt plugin is part of the default asset pipeline in Sails, 28 | // so it's already been automatically loaded for you at this point. 29 | // 30 | // Of course, you can always remove this Grunt plugin altogether by 31 | // deleting this file. But check this out: you can also use your 32 | // _own_ custom version of this Grunt plugin. 33 | // 34 | // Here's how: 35 | // 36 | // 1. Install it as a local dependency of your Sails app: 37 | // ``` 38 | // $ npm install grunt-contrib-less --save-dev --save-exact 39 | // ``` 40 | // 41 | // 42 | // 2. Then uncomment the following code: 43 | // 44 | // ``` 45 | // // Load Grunt plugin from the node_modules/ folder. 46 | // grunt.loadNpmTasks('grunt-contrib-less'); 47 | // ``` 48 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 49 | 50 | }; 51 | -------------------------------------------------------------------------------- /api/controllers/account/logout.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 4 | friendlyName: 'Logout', 5 | 6 | 7 | description: 'Log out of this app.', 8 | 9 | 10 | extendedDescription: 11 | `This action deletes the \`req.session.userId\` key from the session of the requesting user agent. 12 | Actual garbage collection of session data depends on this app's session store, and 13 | potentially also on the [TTL configuration](https://sailsjs.com/docs/reference/configuration/sails-config-session) 14 | you provided for it. 15 | 16 | Note that this action does not check to see whether or not the requesting user was 17 | actually logged in. (If they weren't, then this action is just a no-op.)`, 18 | 19 | 20 | exits: { 21 | 22 | success: { 23 | description: 'The requesting user agent has been successfully logged out.' 24 | }, 25 | 26 | redirect: { 27 | description: 'The requesting user agent looks to be a web browser.', 28 | extendedDescription: 'After logging out from a web browser, the user is redirected away.', 29 | responseType: 'redirect' 30 | } 31 | 32 | }, 33 | 34 | 35 | fn: async function () { 36 | 37 | // Clear the `userId` property from this session. 38 | delete this.req.session.userId; 39 | 40 | // Then finish up, sending an appropriate response. 41 | // > Under the covers, this persists the now-logged-out session back 42 | // > to the underlying session store. 43 | if (!this.req.wantsJSON) { 44 | throw {redirect: '/login'}; 45 | } 46 | 47 | } 48 | 49 | 50 | }; 51 | -------------------------------------------------------------------------------- /tasks/config/clean.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/config/clean` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Remove generated files and folders. 7 | * 8 | * For more information, see: 9 | * https://sailsjs.com/anatomy/tasks/config/clean.js 10 | * 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('clean', { 15 | dev: ['.tmp/public/**'], 16 | build: ['www'], 17 | afterBuildProd: [ 18 | 'www/concat', 19 | 'www/min', 20 | 'www/hash', 21 | 'www/js', 22 | 'www/styles', 23 | 'www/templates', 24 | 'www/dependencies' 25 | ] 26 | }); 27 | 28 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 29 | // This Grunt plugin is part of the default asset pipeline in Sails, 30 | // so it's already been automatically loaded for you at this point. 31 | // 32 | // Of course, you can always remove this Grunt plugin altogether by 33 | // deleting this file. But check this out: you can also use your 34 | // _own_ custom version of this Grunt plugin. 35 | // 36 | // Here's how: 37 | // 38 | // 1. Install it as a local dependency of your Sails app: 39 | // ``` 40 | // $ npm install grunt-contrib-clean --save-dev --save-exact 41 | // ``` 42 | // 43 | // 44 | // 2. Then uncomment the following code: 45 | // 46 | // ``` 47 | // // Load Grunt plugin from the node_modules/ folder. 48 | // grunt.loadNpmTasks('grunt-contrib-clean'); 49 | // ``` 50 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 51 | 52 | }; 53 | -------------------------------------------------------------------------------- /api/models/User.js: -------------------------------------------------------------------------------- 1 | /** 2 | * User.js 3 | * 4 | * A user who can log in to this application. 5 | */ 6 | 7 | module.exports = { 8 | 9 | attributes: { 10 | 11 | // ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗ 12 | // ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗ 13 | // ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝ 14 | 15 | emailAddress: { 16 | type: 'string', 17 | required: true, 18 | unique: true, 19 | isEmail: true, 20 | maxLength: 200, 21 | example: 'mary.sue@example.com' 22 | }, 23 | 24 | password: { 25 | type: 'string', 26 | required: true, 27 | description: 'Securely hashed representation of the user\'s login password.', 28 | protect: true, 29 | example: '2$28a8eabna301089103-13948134nad' 30 | }, 31 | 32 | fullName: { 33 | type: 'string', 34 | required: true, 35 | description: 'Full representation of the user\'s name.', 36 | maxLength: 120, 37 | example: 'Mary Sue van der McHenst' 38 | }, 39 | lastSeenAt: { 40 | type: 'number', 41 | description: 'A JS timestamp (epoch ms) representing the moment at which this user most recently interacted with the backend while logged in (or 0 if they have not interacted with the backend at all yet).', 42 | example: 1502844074211 43 | }, 44 | 45 | // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ 46 | // ║╣ ║║║╠╩╗║╣ ║║╚═╗ 47 | // ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝ 48 | // n/a 49 | 50 | // ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ 51 | // ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗ 52 | // ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝ 53 | // n/a 54 | 55 | }, 56 | 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /tasks/config/cssmin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/config/cssmin` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Together with the `concat` task, this is the final step that minifies 7 | * all CSS files from `assets/styles/` (and potentially your LESS importer 8 | * file from `assets/styles/importer.less`) 9 | * 10 | * For more information, see: 11 | * https://sailsjs.com/anatomy/tasks/config/cssmin.js 12 | * 13 | */ 14 | module.exports = function(grunt) { 15 | 16 | grunt.config.set('cssmin', { 17 | dist: { 18 | src: ['.tmp/public/concat/production.css'], 19 | dest: '.tmp/public/min/production.min.css' 20 | } 21 | }); 22 | 23 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 24 | // This Grunt plugin is part of the default asset pipeline in Sails, 25 | // so it's already been automatically loaded for you at this point. 26 | // 27 | // Of course, you can always remove this Grunt plugin altogether by 28 | // deleting this file. But check this out: you can also use your 29 | // _own_ custom version of this Grunt plugin. 30 | // 31 | // Here's how: 32 | // 33 | // 1. Install it as a local dependency of your Sails app: 34 | // ``` 35 | // $ npm install grunt-contrib-cssmin --save-dev --save-exact 36 | // ``` 37 | // 38 | // 39 | // 2. Then uncomment the following code: 40 | // 41 | // ``` 42 | // // Load Grunt plugin from the node_modules/ folder. 43 | // grunt.loadNpmTasks('grunt-contrib-cssmin'); 44 | // ``` 45 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 46 | 47 | }; 48 | -------------------------------------------------------------------------------- /tasks/config/babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/config/babel` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Transpile >=ES6 code for broader browser compatibility. 7 | * 8 | * For more information, see: 9 | * https://sailsjs.com/anatomy/tasks/config/babel.js 10 | * 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('babel', { 15 | dist: { 16 | options: { 17 | presets: [require('sails-hook-grunt/accessible/babel-preset-env')] 18 | }, 19 | files: [ 20 | { 21 | expand: true, 22 | cwd: '.tmp/public', 23 | src: ['js/**/*.js'], 24 | dest: '.tmp/public' 25 | } 26 | ] 27 | } 28 | }); 29 | 30 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 31 | // This Grunt plugin is part of the default asset pipeline in Sails, 32 | // so it's already been automatically loaded for you at this point. 33 | // 34 | // Of course, you can always remove this Grunt plugin altogether by 35 | // deleting this file. But check this out: you can also use your 36 | // _own_ custom version of this Grunt plugin. 37 | // 38 | // Here's how: 39 | // 40 | // 1. Install it as a local dependency of your Sails app: 41 | // ``` 42 | // $ npm install grunt-babel --save-dev --save-exact 43 | // ``` 44 | // 45 | // 46 | // 2. Then uncomment the following code: 47 | // 48 | // ``` 49 | // // Load Grunt plugin from the node_modules/ folder. 50 | // grunt.loadNpmTasks('grunt-babel'); 51 | // ``` 52 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 53 | 54 | }; 55 | -------------------------------------------------------------------------------- /tasks/config/watch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/config/watch` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Run predefined tasks whenever certain files are added, changed or deleted. 7 | * 8 | * For more information, see: 9 | * https://sailsjs.com/anatomy/tasks/config/watch.js 10 | * 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('watch', { 15 | assets: { 16 | 17 | // Assets to watch: 18 | files: [ 19 | 'assets/**/*', 20 | 'tasks/pipeline.js', 21 | '!**/node_modules/**' 22 | ], 23 | 24 | // When assets are changed: 25 | tasks: [ 26 | 'syncAssets', 27 | 'linkAssets' 28 | ] 29 | } 30 | }); 31 | 32 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 33 | // This Grunt plugin is part of the default asset pipeline in Sails, 34 | // so it's already been automatically loaded for you at this point. 35 | // 36 | // Of course, you can always remove this Grunt plugin altogether by 37 | // deleting this file. But check this out: you can also use your 38 | // _own_ custom version of this Grunt plugin. 39 | // 40 | // Here's how: 41 | // 42 | // 1. Install it as a local dependency of your Sails app: 43 | // ``` 44 | // $ npm install grunt-contrib-watch --save-dev --save-exact 45 | // ``` 46 | // 47 | // 48 | // 2. Then uncomment the following code: 49 | // 50 | // ``` 51 | // // Load Grunt plugin from the node_modules/ folder. 52 | // grunt.loadNpmTasks('grunt-contrib-watch'); 53 | // ``` 54 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 55 | 56 | }; 57 | -------------------------------------------------------------------------------- /tasks/config/concat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/config/concat` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * An intermediate step to generate monolithic files that can 7 | * then be passed in to `uglify` and/or `cssmin` for minification. 8 | * 9 | * For more information, see: 10 | * https://sailsjs.com/anatomy/tasks/config/concat.js 11 | * 12 | */ 13 | module.exports = function(grunt) { 14 | 15 | grunt.config.set('concat', { 16 | js: { 17 | src: require('../pipeline').jsFilesToInject, 18 | dest: '.tmp/public/concat/production.js' 19 | }, 20 | css: { 21 | src: require('../pipeline').cssFilesToInject, 22 | dest: '.tmp/public/concat/production.css' 23 | } 24 | }); 25 | 26 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 27 | // This Grunt plugin is part of the default asset pipeline in Sails, 28 | // so it's already been automatically loaded for you at this point. 29 | // 30 | // Of course, you can always remove this Grunt plugin altogether by 31 | // deleting this file. But check this out: you can also use your 32 | // _own_ custom version of this Grunt plugin. 33 | // 34 | // Here's how: 35 | // 36 | // 1. Install it as a local dependency of your Sails app: 37 | // ``` 38 | // $ npm install grunt-contrib-concat --save-dev --save-exact 39 | // ``` 40 | // 41 | // 42 | // 2. Then uncomment the following code: 43 | // 44 | // ``` 45 | // // Load Grunt plugin from the node_modules/ folder. 46 | // grunt.loadNpmTasks('grunt-contrib-concat'); 47 | // ``` 48 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 49 | 50 | }; 51 | -------------------------------------------------------------------------------- /assets/js/pages/entrance/login.page.js: -------------------------------------------------------------------------------- 1 | parasails.registerPage('login', { 2 | // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ 3 | // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ 4 | // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ 5 | data: { 6 | // Main syncing/loading state for this page. 7 | syncing: false, 8 | 9 | // Form data 10 | formData: { 11 | rememberMe: true, 12 | }, 13 | 14 | // For tracking client-side validation errors in our form. 15 | // > Has property set to `true` for each invalid property in `formData`. 16 | formErrors: { /* … */ }, 17 | 18 | // A set of validation rules for our form. 19 | // > The form will not be submitted if these are invalid. 20 | formRules: { 21 | emailAddress: { required: true, isEmail: true }, 22 | password: { required: true }, 23 | }, 24 | 25 | // Server error state for the form 26 | cloudError: '', 27 | }, 28 | 29 | // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ 30 | // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ 31 | // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ 32 | beforeMount: function() { 33 | // Attach any initial data from the server. 34 | _.extend(this, SAILS_LOCALS); 35 | }, 36 | mounted: async function() { 37 | //… 38 | }, 39 | 40 | // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ 41 | // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ 42 | // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ 43 | methods: { 44 | 45 | submittedForm: async function() { 46 | // Redirect to the logged-in dashboard on success. 47 | // > (Note that we re-enable the syncing state here. This is on purpose-- 48 | // > to make sure the spinner stays there until the page navigation finishes.) 49 | this.syncing = true; 50 | window.location = '/'; 51 | }, 52 | 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /config/blueprints.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Blueprint API Configuration 3 | * (sails.config.blueprints) 4 | * 5 | * For background on the blueprint API in Sails, check out: 6 | * https://sailsjs.com/docs/reference/blueprint-api 7 | * 8 | * For details and more available options, see: 9 | * https://sailsjs.com/config/blueprints 10 | */ 11 | 12 | module.exports.blueprints = { 13 | 14 | /*************************************************************************** 15 | * * 16 | * Automatically expose implicit routes for every action in your app? * 17 | * * 18 | ***************************************************************************/ 19 | 20 | // actions: false, 21 | 22 | 23 | /*************************************************************************** 24 | * * 25 | * Automatically expose RESTful routes for your models? * 26 | * * 27 | ***************************************************************************/ 28 | 29 | rest: false, 30 | 31 | 32 | /*************************************************************************** 33 | * * 34 | * Automatically expose CRUD "shortcut" routes to GET requests? * 35 | * (These are enabled by default in development only.) * 36 | * * 37 | ***************************************************************************/ 38 | 39 | shortcuts: false, 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /views/pages/account/account-overview.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

My account

4 |
5 |
6 |
7 |

Personal information

8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 |
16 |
Name:
17 |
{{me.fullName}}
18 |
19 |
20 |
Email:
21 |
22 | {{me.emailChangeCandidate ? me.emailChangeCandidate : me.emailAddress}} 23 | Unverified 24 |
25 |
26 |
27 |
28 |
29 |

Password

30 |
31 |
32 | 33 | 34 | 35 |
36 |
37 |
38 |
Password:
39 |
••••••••••
40 |
41 |
42 | 43 | 44 |
45 | 46 |
47 | <%- /* Expose locals as `window.SAILS_LOCALS` :: */ exposeLocalsToBrowser() %> 48 | -------------------------------------------------------------------------------- /tasks/config/copy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/config/copy` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Copy files and/or folders. 7 | * 8 | * For more information, see: 9 | * https://sailsjs.com/anatomy/tasks/config/copy.js 10 | * 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('copy', { 15 | dev: { 16 | files: [{ 17 | expand: true, 18 | cwd: './assets', 19 | src: ['**/*.!(coffee|less)'], 20 | dest: '.tmp/public' 21 | }] 22 | }, 23 | build: { 24 | files: [{ 25 | expand: true, 26 | cwd: '.tmp/public', 27 | src: ['**/*'], 28 | dest: 'www' 29 | }] 30 | }, 31 | beforeLinkBuildProd: { 32 | files: [{ 33 | expand: true, 34 | cwd: '.tmp/public/hash', 35 | src: ['**/*'], 36 | dest: '.tmp/public/dist' 37 | }] 38 | }, 39 | }); 40 | 41 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 42 | // This Grunt plugin is part of the default asset pipeline in Sails, 43 | // so it's already been automatically loaded for you at this point. 44 | // 45 | // Of course, you can always remove this Grunt plugin altogether by 46 | // deleting this file. But check this out: you can also use your 47 | // _own_ custom version of this Grunt plugin. 48 | // 49 | // Here's how: 50 | // 51 | // 1. Install it as a local dependency of your Sails app: 52 | // ``` 53 | // $ npm install grunt-contrib-copy --save-dev --save-exact 54 | // ``` 55 | // 56 | // 57 | // 2. Then uncomment the following code: 58 | // 59 | // ``` 60 | // // Load Grunt plugin from the node_modules/ folder. 61 | // grunt.loadNpmTasks('grunt-contrib-copy'); 62 | // ``` 63 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 64 | 65 | }; 66 | -------------------------------------------------------------------------------- /assets/js/components/ajax-button.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ----------------------------------------------------------------------------- 4 | * A button with a built-in loading spinner. 5 | * 6 | * @type {Component} 7 | * 8 | * @event click [emitted when clicked] 9 | * ----------------------------------------------------------------------------- 10 | */ 11 | 12 | parasails.registerComponent('ajaxButton', { 13 | // ╔═╗╦═╗╔═╗╔═╗╔═╗ 14 | // ╠═╝╠╦╝║ ║╠═╝╚═╗ 15 | // ╩ ╩╚═╚═╝╩ ╚═╝ 16 | props: [ 17 | 'syncing' 18 | ], 19 | 20 | // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ 21 | // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ 22 | // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ 23 | data: function (){ 24 | return { 25 | //… 26 | }; 27 | }, 28 | 29 | // ╦ ╦╔╦╗╔╦╗╦ 30 | // ╠═╣ ║ ║║║║ 31 | // ╩ ╩ ╩ ╩ ╩╩═╝ 32 | template: ` 33 | 44 | `, 45 | 46 | // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ 47 | // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ 48 | // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ 49 | beforeMount: function() { 50 | //… 51 | }, 52 | mounted: async function(){ 53 | //… 54 | }, 55 | beforeDestroy: function() { 56 | //… 57 | }, 58 | 59 | // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ 60 | // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ 61 | // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ 62 | methods: { 63 | 64 | click: async function(){ 65 | this.$emit('click'); 66 | }, 67 | 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /tasks/config/uglify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/config/uglify` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Minify client-side JavaScript files using UglifyES. 7 | * 8 | * For more information, see: 9 | * https://sailsjs.com/anatomy/tasks/config/uglify.js 10 | * 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('uglify', { 15 | dist: { 16 | src: ['.tmp/public/concat/production.js'], 17 | dest: '.tmp/public/min/production.min.js' 18 | }, 19 | options: { 20 | mangle: { 21 | reserved: [ 22 | 'AsyncFunction', 23 | 'SailsSocket', 24 | 'Promise', 25 | 'File', 26 | 'FileList', 27 | 'FormData', 28 | 'Location', 29 | 'RttcRefPlaceholder', 30 | ], 31 | keep_fnames: true//eslint-disable-line 32 | }, 33 | compress: { 34 | keep_fnames: true//eslint-disable-line 35 | } 36 | } 37 | }); 38 | 39 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 40 | // This Grunt plugin is part of the default asset pipeline in Sails, 41 | // so it's already been automatically loaded for you at this point. 42 | // 43 | // Of course, you can always remove this Grunt plugin altogether by 44 | // deleting this file. But check this out: you can also use your 45 | // _own_ custom version of this Grunt plugin. 46 | // 47 | // Here's how: 48 | // 49 | // 1. Install it as a local dependency of your Sails app: 50 | // ``` 51 | // $ npm install grunt-contrib-uglify --save-dev --save-exact 52 | // ``` 53 | // 54 | // 55 | // 2. Then uncomment the following code: 56 | // 57 | // ``` 58 | // // Load Grunt plugin from the node_modules/ folder. 59 | // grunt.loadNpmTasks('grunt-contrib-uglify'); 60 | // ``` 61 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 62 | 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * Use `app.js` to run your app without `sails lift`. 5 | * To start the server, run: `node app.js`. 6 | * 7 | * This is handy in situations where the sails CLI is not relevant or useful, 8 | * such as when you deploy to a server, or a PaaS like Heroku. 9 | * 10 | * For example: 11 | * => `node app.js` 12 | * => `npm start` 13 | * => `forever start app.js` 14 | * => `node debug app.js` 15 | * 16 | * The same command-line arguments and env vars are supported, e.g.: 17 | * `NODE_ENV=production node app.js --port=80 --verbose` 18 | * 19 | * For more information see: 20 | * https://sailsjs.com/anatomy/app.js 21 | */ 22 | 23 | 24 | // Ensure we're in the project directory, so cwd-relative paths work as expected 25 | // no matter where we actually lift from. 26 | // > Note: This is not required in order to lift, but it is a convenient default. 27 | process.chdir(__dirname); 28 | 29 | 30 | 31 | // Attempt to import `sails` dependency, as well as `rc` (for loading `.sailsrc` files). 32 | var sails; 33 | var rc; 34 | try { 35 | sails = require('sails'); 36 | rc = require('sails/accessible/rc'); 37 | } catch (err) { 38 | console.error('Encountered an error when attempting to require(\'sails\'):'); 39 | console.error(err.stack); 40 | console.error('--'); 41 | console.error('To run an app using `node app.js`, you need to have Sails installed'); 42 | console.error('locally (`./node_modules/sails`). To do that, just make sure you\'re'); 43 | console.error('in the same directory as your app and run `npm install`.'); 44 | console.error(); 45 | console.error('If Sails is installed globally (i.e. `npm install -g sails`) you can'); 46 | console.error('also run this app with `sails lift`. Running with `sails lift` will'); 47 | console.error('not run this file (`app.js`), but it will do exactly the same thing.'); 48 | console.error('(It even uses your app directory\'s local Sails install, if possible.)'); 49 | return; 50 | }//-• 51 | 52 | 53 | // Start server 54 | sails.lift(rc('sails')); 55 | -------------------------------------------------------------------------------- /config/views.js: -------------------------------------------------------------------------------- 1 | /** 2 | * View Engine Configuration 3 | * (sails.config.views) 4 | * 5 | * Server-sent views are a secure and effective way to get your app up 6 | * and running. Views are normally served from actions. Below, you can 7 | * configure your templating language/framework of choice and configure 8 | * Sails' layout support. 9 | * 10 | * For details on available options for configuring server-side views, check out: 11 | * https://sailsjs.com/config/views 12 | * 13 | * For more background information on views and partials in Sails, check out: 14 | * https://sailsjs.com/docs/concepts/views 15 | */ 16 | 17 | module.exports.views = { 18 | 19 | /*************************************************************************** 20 | * * 21 | * Extension to use for your views. When calling `res.view()` in an action, * 22 | * you can leave this extension off. For example, calling * 23 | * `res.view('homepage')` will (using default settings) look for a * 24 | * `views/homepage.ejs` file. * 25 | * * 26 | ***************************************************************************/ 27 | 28 | // extension: 'ejs', 29 | 30 | /*************************************************************************** 31 | * * 32 | * The path (relative to the views directory, and without extension) to * 33 | * the default layout file to use, or `false` to disable layouts entirely. * 34 | * * 35 | * Note that layouts only work with the built-in EJS view engine! * 36 | * * 37 | ***************************************************************************/ 38 | 39 | layout: 'layouts/layout' 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /assets/styles/layout.less: -------------------------------------------------------------------------------- 1 | @footer-height: 40px; 2 | @container-md-max-width: 1100px; 3 | 4 | [v-cloak] { display: none; } 5 | 6 | html, body { 7 | height: 100%; 8 | margin: 0; 9 | } 10 | 11 | #page-wrap { 12 | height: 100%; 13 | /* lesshint-disable */height: auto !important;/* lesshint-enable */ 14 | // ^^The above is to disable "importantRule" and "duplicateProperty" rules. 15 | min-height: 100%; 16 | position: relative; 17 | padding-bottom: @footer-height; 18 | 19 | header { 20 | .logo { 21 | height: 40px; 22 | } 23 | a { 24 | cursor: pointer; 25 | } 26 | .dropdown-menu.account-menu { 27 | left: auto; 28 | right: 0px; 29 | } 30 | } 31 | } 32 | 33 | #page-footer { 34 | border-top: 1px solid rgba(0, 0, 0, 0.1); 35 | height: @footer-height; 36 | width: 100%; 37 | position: absolute; 38 | left: 0px; 39 | bottom: 0px; 40 | .xs-only { 41 | display: none; 42 | } 43 | } 44 | 45 | body.detected-mobile { 46 | // Above and beyond the media queries below, this selector (which relies on 47 | // `parasails` automatically attaching this class, if appropriate) contains 48 | // styles intended to be activated specifically when loaded from a recognized 49 | // mobile device, regardless of viewport dimensions. This includes tablet 50 | // devices (like the iPad) as well as handset devices (like the iPhone). 51 | // … 52 | } 53 | 54 | @media (max-width: 800px) { 55 | #page-wrap { 56 | padding-bottom: 75px; 57 | #page-footer { 58 | height: 75px; 59 | .copy, .nav { 60 | width: 100%; 61 | display: block; 62 | text-align: center; 63 | .nav-item { 64 | display: inline-block; 65 | a { 66 | display: inline-block; 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | @media (max-width: 450px) { 75 | #page-wrap { 76 | padding-bottom: 85px; 77 | #page-footer { 78 | height: 85px; 79 | .xs-only { 80 | display: block; 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /config/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Session Configuration 3 | * (sails.config.session) 4 | * 5 | * Use the settings below to configure session integration in your app. 6 | * (for additional recommended settings, see `config/env/production.js`) 7 | * 8 | * For all available options, see: 9 | * https://sailsjs.com/config/session 10 | */ 11 | require('dotenv').config(); 12 | module.exports.session = { 13 | 14 | /*************************************************************************** 15 | * * 16 | * Session secret is automatically generated when your new app is created * 17 | * Replace at your own risk in production-- you will invalidate the cookies * 18 | * of your users, forcing them to log in again. * 19 | * * 20 | ***************************************************************************/ 21 | secret: 'fa81085e0b36703388a90970ec18a6e9', 22 | 23 | 24 | /*************************************************************************** 25 | * * 26 | * Customize when built-in session support will be skipped. * 27 | * * 28 | * (Useful for performance tuning; particularly to avoid wasting cycles on * 29 | * session management when responding to simple requests for static assets, * 30 | * like images or stylesheets.) * 31 | * * 32 | * https://sailsjs.com/config/session * 33 | * * 34 | ***************************************************************************/ 35 | // isSessionDisabled: function (req){ 36 | // return !!req.path.match(req._sails.LOOKS_LIKE_ASSET_RX); 37 | // }, 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /api/helpers/send-request.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | friendlyName: 'Send request', 4 | 5 | description: 'Send a GET request to the specified URL with optional cookies.', 6 | 7 | inputs: { 8 | link: { 9 | description: 'The target URL', 10 | example: 'http://www.google.com', 11 | type: 'string', 12 | required: true 13 | }, 14 | cookie: { 15 | description: 'The cookies that will be included in the request', 16 | example: 'user_id=1234', 17 | type: 'string', 18 | }, 19 | }, 20 | 21 | exits: { 22 | success: { 23 | description: 'Request was successful and the response is returned.' 24 | }, 25 | invalidUrl: { 26 | description: 'The provided URL is invalid or malformed.', 27 | responseType: 'badRequest' 28 | }, 29 | requestFailed: { 30 | description: 'The request to the specified URL failed.', 31 | responseType: 'serverError' 32 | } 33 | }, 34 | 35 | fn: async function (inputs, exits) { 36 | var request = require('request'); 37 | 38 | // Validate URL format 39 | try { 40 | new URL(inputs.link); 41 | } catch (err) { 42 | sails.log.error(`Invalid URL: ${inputs.link}`, err); 43 | return exits.invalidUrl('The provided URL is invalid.'); 44 | } 45 | 46 | request.get({ 47 | url: inputs.link, 48 | headers: { 49 | 'cookie': inputs.cookie || '' 50 | } 51 | }, (error, response, body) => { 52 | if (error) { 53 | sails.log.error(`Request to ${inputs.link} failed:`, error); 54 | return exits.requestFailed('Failed to send the request.'); 55 | } else if (response.statusCode < 200 || response.statusCode >= 300) { 56 | sails.log.warn(`Request to ${inputs.link} returned status ${response.statusCode}`); 57 | return exits.requestFailed(`Request failed with status code ${response.statusCode}.`); 58 | } else { 59 | // Request was successful 60 | sails.log(`Request to ${inputs.link} was successful.`); 61 | return exits.success(body); 62 | } 63 | }); 64 | } 65 | 66 | }; 67 | -------------------------------------------------------------------------------- /views/pages/entrance/login.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Sign in to your account

4 | 26 |
27 |
28 | <%- /* Expose locals as `window.SAILS_LOCALS` :: */ exposeLocalsToBrowser() %> 29 | -------------------------------------------------------------------------------- /api/controllers/link/delete-link.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | friendlyName: 'Delete Link', 4 | 5 | description: 'Delete link from the database and its associated file.', 6 | 7 | fn: async function () { 8 | try { 9 | let fs = require('fs'); 10 | let id = this.req.params.id; 11 | sails.log(`Attempting to delete record with ID: ${id}`); 12 | 13 | if (!id) { 14 | sails.log.error('No ID provided.'); 15 | return this.res.status(400).send('No ID provided.'); 16 | } 17 | 18 | // Attempt to delete the record from the database 19 | var destroyedRecord; 20 | try { 21 | destroyedRecord = await Target.destroyOne({ id: id }); 22 | } catch (error) { 23 | sails.log.error(`Error deleting record with ID ${id}:`, error); 24 | return this.res.status(500).send('Failed to delete the record.'); 25 | } 26 | 27 | // If the record was successfully deleted, attempt to delete the associated file 28 | if (destroyedRecord) { 29 | sails.log(`Successfully deleted record with ID: ${id}`); 30 | 31 | // Construct the file path 32 | let filePath = `./responses/${id}.txt`; 33 | 34 | // Attempt to delete the file 35 | try { 36 | if (fs.existsSync(filePath)) { 37 | fs.unlinkSync(filePath); 38 | sails.log(`Successfully deleted file associated with ID: ${id}`); 39 | } else { 40 | sails.log.warn(`File not found for ID: ${id}`); 41 | } 42 | } catch (fileError) { 43 | sails.log.error(`Error deleting file for record with ID ${id}:`, fileError); 44 | return this.res.status(500).send('Record deleted, but failed to delete the associated file.'); 45 | } 46 | 47 | return this.res.send('1'); 48 | } else { 49 | sails.log.warn(`No record found with ID: ${id}`); 50 | return this.res.status(404).send('Record not found.'); 51 | } 52 | 53 | } catch (error) { 54 | sails.log.error('An unexpected error occurred:', error); 55 | return this.res.status(500).send('An unexpected error occurred.'); 56 | } 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /.lesshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // ╦ ╔═╗╔═╗╔═╗╦ ╦╦╔╗╔╔╦╗┬─┐┌─┐ 3 | // ║ ║╣ ╚═╗╚═╗╠═╣║║║║ ║ ├┬┘│ 4 | // o╩═╝╚═╝╚═╝╚═╝╩ ╩╩╝╚╝ ╩ ┴└─└─┘ 5 | // Configuration designed for the lesshint linter. Describes a loose set of LESS 6 | // conventions that help avoid typos, unexpected failed builds, and hard-to-debug 7 | // selector and CSS rule issues. 8 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 9 | // For more information about any of the rules below, check out the reference page 10 | // of all rules at https://github.com/lesshint/lesshint/blob/v6.3.6/lib/linters/README.md 11 | // If you're unsure or could use some advice, come by https://sailsjs.com/support. 12 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 13 | "singleLinePerSelector": false, 14 | "singleLinePerProperty": false, 15 | "zeroUnit": false, 16 | "idSelector": false, 17 | "propertyOrdering": false, 18 | "spaceAroundBang": false, 19 | "fileExtensions": [".less", ".css"], 20 | "excludedFiles": ["vendor.less"], 21 | "importPath": false, 22 | "borderZero": false, 23 | "hexLength": false, 24 | "hexNotation": false, 25 | "newlineAfterBlock": false, 26 | "spaceBeforeBrace": { 27 | "style": "one_space" 28 | }, 29 | "spaceAfterPropertyName": false, 30 | "spaceAfterPropertyColon": { 31 | "enabled": true, 32 | "style": "one_space" 33 | }, 34 | "maxCharPerLine": false, 35 | "emptyRule": false, 36 | "importantRule": true, 37 | "qualifyingElement": false 38 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 39 | // ^^ This last one is only disabled because the lesshint parser seems to have 40 | // a hard time distinguishing between things like `div.bar` and `&.bar`. 41 | // In this case, the ampersand has a distinct meaning, and it does not refer 42 | // to an element. (It's referring to the case where that class is matched at 43 | // the parent level, rather than talking about a descendant.) 44 | // https://github.com/lesshint/lesshint/blob/v6.3.6/lib/linters/README.md#qualifyingelement 45 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 46 | } 47 | -------------------------------------------------------------------------------- /views/pages/account/edit-password.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Change password

4 |
5 | 6 |
7 |
8 |
9 | 10 | 11 |
Please enter a password or choose "Cancel".
12 |
13 |
14 |
15 |
16 | 17 | 18 |
Your new password and confirmation do not match.
19 |
20 |
21 |
22 |
23 |
24 |

An error occured while processing your request. Please check your information and try again, or contact support if the error persists.

25 |
26 |
27 |
28 | Cancel 29 | Save changes 30 |
31 |
32 |
33 |
34 |
35 |
36 | <%- /* Expose locals as `window.SAILS_LOCALS` :: */ exposeLocalsToBrowser() %> 37 | -------------------------------------------------------------------------------- /assets/js/pages/updateSettings.page.js: -------------------------------------------------------------------------------- 1 | parasails.registerPage('edit-settings', { 2 | // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ 3 | // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ 4 | // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ 5 | data: { 6 | // Main syncing/loading state for this page. 7 | syncing: false, 8 | 9 | // Form data 10 | formData: { /* … */ }, 11 | 12 | // For tracking client-side validation errors in our form. 13 | // > Has property set to `true` for each invalid property in `formData`. 14 | formErrors: { /* … */ }, 15 | 16 | // Server error state for the form 17 | cloudError: '', 18 | 19 | 20 | }, 21 | 22 | // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ 23 | // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ 24 | // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ 25 | beforeMount: function() { 26 | // Attach raw data exposed by the server. 27 | _.extend(this, SAILS_LOCALS); 28 | 29 | // Set the form data. 30 | this.formData.telegramToken = this.telegramToken; 31 | this.formData.telegramChatID = this.telegramChatID; 32 | this.formData.reportToTelegram = this.reportToTelegram; 33 | 34 | }, 35 | mounted: async function() { 36 | // console.log(this.test) 37 | }, 38 | 39 | // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ 40 | // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ 41 | // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ 42 | methods: { 43 | 44 | submittedForm: async function() { 45 | // Redirect to the account page on success. 46 | // > (Note that we re-enable the syncing state here. This is on purpose-- 47 | // > to make sure the spinner stays there until the page navigation finishes.) 48 | this.syncing = true; 49 | window.location = '/main'; 50 | }, 51 | 52 | handleParsingForm: function() { 53 | // Clear out any pre-existing error messages. 54 | this.formErrors = {}; 55 | 56 | var argins = this.formData; 57 | 58 | 59 | 60 | // If there were any issues, they've already now been communicated to the user, 61 | // so simply return undefined. (This signifies that the submission should be 62 | // cancelled.) 63 | if (Object.keys(this.formErrors).length > 0) { 64 | return; 65 | } 66 | 67 | return argins; 68 | }, 69 | 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /tasks/config/hash.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/config/hash` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Implement cache-busting for minified CSS and JavaScript files. 7 | * 8 | * For more information, see: 9 | * https://sailsjs.com/anatomy/tasks/config/hash.js 10 | * 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('hash', { 15 | options: { 16 | mapping: '', 17 | srcBasePath: '', 18 | destBasePath: '', 19 | flatten: false, 20 | hashLength: 8, 21 | hashFunction: function(source, encoding){ 22 | if (!source || !encoding) { 23 | throw new Error('Consistency violation: Cannot compute unique hash for production .css/.js cache-busting suffix, because `source` and/or `encoding` are falsey-- but they should be truthy strings! Here they are, respectively:\nsource: '+require('util').inspect(source, {depth:null})+'\nencoding: '+require('util').inspect(encoding, {depth:null})); 24 | } 25 | return require('crypto').createHash('sha1').update(source, encoding).digest('hex'); 26 | } 27 | }, 28 | js: { 29 | src: '.tmp/public/min/*.js', 30 | dest: '.tmp/public/hash/' 31 | }, 32 | css: { 33 | src: '.tmp/public/min/*.css', 34 | dest: '.tmp/public/hash/' 35 | } 36 | }); 37 | 38 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 39 | // This Grunt plugin is part of the default asset pipeline in Sails, 40 | // so it's already been automatically loaded for you at this point. 41 | // 42 | // Of course, you can always remove this Grunt plugin altogether by 43 | // deleting this file. But check this out: you can also use your 44 | // _own_ custom version of this Grunt plugin. 45 | // 46 | // Here's how: 47 | // 48 | // 1. Install it as a local dependency of your Sails app: 49 | // ``` 50 | // $ npm install grunt-hash --save-dev --save-exact 51 | // ``` 52 | // 53 | // 54 | // 2. Then uncomment the following code: 55 | // 56 | // ``` 57 | // // Load Grunt plugin from the node_modules/ folder. 58 | // grunt.loadNpmTasks('grunt-hash'); 59 | // ``` 60 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 61 | 62 | }; 63 | -------------------------------------------------------------------------------- /config/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internationalization / Localization Settings 3 | * (sails.config.i18n) 4 | * 5 | * If your app will touch people from all over the world, i18n (or internationalization) 6 | * may be an important part of your international strategy. 7 | * 8 | * For a complete list of options for Sails' built-in i18n support, see: 9 | * https://sailsjs.com/config/i-18-n 10 | * 11 | * For more info on i18n in Sails in general, check out: 12 | * https://sailsjs.com/docs/concepts/internationalization 13 | */ 14 | 15 | module.exports.i18n = { 16 | 17 | /*************************************************************************** 18 | * * 19 | * Which locales are supported? * 20 | * * 21 | ***************************************************************************/ 22 | 23 | locales: ['en'], 24 | 25 | /**************************************************************************** 26 | * * 27 | * What is the default locale for the site? Note that this setting will be * 28 | * overridden for any request that sends an "Accept-Language" header (i.e. * 29 | * most browsers), but it's still useful if you need to localize the * 30 | * response for requests made by non-browser clients (e.g. cURL). * 31 | * * 32 | ****************************************************************************/ 33 | 34 | // defaultLocale: 'en', 35 | 36 | /**************************************************************************** 37 | * * 38 | * Path (relative to app root) of directory to store locale (translation) * 39 | * files in. * 40 | * * 41 | ****************************************************************************/ 42 | 43 | // localesDirectory: 'config/locales' 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /api/controllers/link/add-link.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | friendlyName: 'Add Link', 3 | 4 | description: 'Add new link to the system.', 5 | 6 | fn: async function () { 7 | const validURL = (str) => { 8 | var pattern = new RegExp( 9 | '^(https?:\\/\\/)?' + 10 | '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + 11 | '((\\d{1,3}\\.){3}\\d{1,3}))' + 12 | '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + 13 | '(\\?[;&a-z\\d%_.~+=-]*)?' + 14 | '(\\#[-a-z\\d_]*)?$', 15 | 'i' 16 | ); 17 | return !!pattern.test(str); 18 | }; 19 | 20 | try { 21 | let bulk = this.req.body.bulkAdd; 22 | let targets = []; 23 | if (bulk) { 24 | targets = this.req.body.links.split('\n'); 25 | } else { 26 | let link = this.req.body.link; 27 | targets.push(link); 28 | } 29 | 30 | for (let link of targets) { 31 | let desc = this.req.body.description; 32 | let fetchEvery = this.req.body.fetchEvery; 33 | let keywords = this.req.body.keywords || ''; 34 | let acceptedChange = this.req.body.acceptedChange || ''; 35 | let cookie = this.req.body.cookie || ''; 36 | 37 | if (!desc || !link || !fetchEvery) { 38 | return this.res.status(400).send('Required fields missing.'); 39 | } 40 | 41 | if (!validURL(link.trim())) { 42 | return this.res.status(400).send('Invalid URL.'); 43 | } 44 | 45 | // Schedule the job using Agenda to run in 1 second 46 | try { 47 | await sails.config.agenda.schedule('in 1 second', 'process link', { 48 | link, desc, fetchEvery, keywords, acceptedChange, cookie 49 | }); 50 | sails.log(`Scheduled job to process link: ${link} in 1 second`); 51 | } catch (error) { 52 | sails.log.error('Error scheduling job:', error); 53 | return this.res.status(500).send('Error scheduling job.'); 54 | } 55 | } 56 | 57 | return this.res.status(200).send('Links scheduled for processing in 1 second.'); 58 | } catch (error) { 59 | sails.log.error('An unexpected error occurred:', error); 60 | return this.res.status(500).send('An unexpected error occurred.'); 61 | } 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /views/pages/account/edit-profile.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Update personal info

4 |
5 | 6 |
7 |
8 |
9 | 10 | 11 |
Please enter your full name.
12 |
13 |
14 |
15 |
16 | 17 | 18 |
Please enter a valid email address.
19 |
20 |
21 |
22 |
23 |
24 |

There is already an account using that email address.

25 |

An error occured while processing your request. Please check your information and try again, or contact support if the error persists.

26 |
27 |
28 |
29 | Cancel 30 | Save changes 31 |
32 |
33 |
34 |
35 |
36 |
37 | <%- /* Expose locals as `window.SAILS_LOCALS` :: */ exposeLocalsToBrowser() %> 38 | -------------------------------------------------------------------------------- /assets/js/pages/account/edit-password.page.js: -------------------------------------------------------------------------------- 1 | parasails.registerPage('edit-password', { 2 | // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ 3 | // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ 4 | // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ 5 | data: { 6 | // Main syncing/loading state for this page. 7 | syncing: false, 8 | 9 | // Form data 10 | formData: { /* … */ }, 11 | 12 | // For tracking client-side validation errors in our form. 13 | // > Has property set to `true` for each invalid property in `formData`. 14 | formErrors: { /* … */ }, 15 | 16 | // Server error state for the form 17 | cloudError: '', 18 | }, 19 | 20 | // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ 21 | // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ 22 | // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ 23 | beforeMount: function() { 24 | // Attach raw data exposed by the server. 25 | _.extend(this, SAILS_LOCALS); 26 | }, 27 | mounted: async function() { 28 | //… 29 | }, 30 | 31 | // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ 32 | // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ 33 | // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ 34 | methods: { 35 | 36 | submittedForm: async function() { 37 | // Redirect to a different web page on success. 38 | // > (Note that we re-enable the syncing state here. This is on purpose-- 39 | // > to make sure the spinner stays there until the page navigation finishes.) 40 | this.syncing = true; 41 | window.location = '/account'; 42 | }, 43 | 44 | handleParsingForm: function() { 45 | // Clear out any pre-existing error messages. 46 | this.formErrors = {}; 47 | 48 | var argins = { password: this.formData.password }; 49 | 50 | // Validate password: 51 | if(!argins.password) { 52 | this.formErrors.password = true; 53 | } 54 | 55 | // Validate password confirmation: 56 | if(argins.password && argins.password !== this.formData.confirmPassword) { 57 | this.formErrors.confirmPassword = true; 58 | } 59 | 60 | // If there were any issues, they've already now been communicated to the user, 61 | // so simply return undefined. (This signifies that the submission should be 62 | // cancelled.) 63 | if (Object.keys(this.formErrors).length > 0) { 64 | return; 65 | } 66 | 67 | return argins; 68 | }, 69 | 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /api/controllers/link/update-link.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | friendlyName: 'Update target', 4 | 5 | description: 'Update the status of a target.', 6 | 7 | exits: { 8 | success: {}, 9 | notFound: { 10 | description: 'No target with the specified ID was found in the database.', 11 | responseType: 'notFound' 12 | }, 13 | fileNotFound: { 14 | description: 'The specified diff file could not be found.', 15 | responseType: 'notFound' 16 | }, 17 | serverError: { 18 | description: 'An unexpected error occurred.', 19 | responseType: 'serverError' 20 | } 21 | }, 22 | 23 | fn: async function () { 24 | try { 25 | let fs = require('fs'); 26 | let id = this.req.params.id; 27 | 28 | // Attempt to update the target record 29 | let updateRecord = await Target.updateOne({ id: id }) 30 | .set({ 31 | status: 'unchanged' 32 | }); 33 | 34 | // If no record was found, return a 404 response 35 | if (!updateRecord) { 36 | return this.res.notFound('Target not found'); 37 | } 38 | 39 | // Define the path to the diff file 40 | let diffFile = 'responses/diffs/' + id + '.txt'; 41 | 42 | // Attempt to read the diff file 43 | let data; 44 | try { 45 | data = fs.readFileSync(diffFile); 46 | } catch (err) { 47 | // If the file is not found, return a 404 response 48 | sails.log.warn(err); 49 | return this.res.notFound('Diff file not found'); 50 | } 51 | 52 | // Convert the file content to string 53 | let resp = data.toString(); 54 | 55 | // Attempt to delete the diff file after reading it 56 | try { 57 | fs.unlinkSync(diffFile); 58 | } catch (err) { 59 | sails.log.warn(`Failed to delete diff file: ${diffFile}`, err); 60 | // Continue execution even if the file deletion fails 61 | } 62 | 63 | // Set the response content type and send the file content 64 | this.res.setHeader('Content-Type', 'text/plain'); 65 | return this.res.send(resp); 66 | 67 | } catch (error) { 68 | // Log the error and return a server error response 69 | sails.log.error('Error updating target:', error); 70 | return this.res.serverError('An unexpected error occurred'); 71 | } 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /config/security.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Security Settings 3 | * (sails.config.security) 4 | * 5 | * These settings affect aspects of your app's security, such 6 | * as how it deals with cross-origin requests (CORS) and which 7 | * routes require a CSRF token to be included with the request. 8 | * 9 | * For an overview of how Sails handles security, see: 10 | * https://sailsjs.com/documentation/concepts/security 11 | * 12 | * For additional options and more information, see: 13 | * https://sailsjs.com/config/security 14 | */ 15 | 16 | module.exports.security = { 17 | 18 | /*************************************************************************** 19 | * * 20 | * CORS is like a more modern version of JSONP-- it allows your application * 21 | * to circumvent browsers' same-origin policy, so that the responses from * 22 | * your Sails app hosted on one domain (e.g. example.com) can be received * 23 | * in the client-side JavaScript code from a page you trust hosted on _some * 24 | * other_ domain (e.g. trustedsite.net). * 25 | * * 26 | * For additional options and more information, see: * 27 | * https://sailsjs.com/docs/concepts/security/cors * 28 | * * 29 | ***************************************************************************/ 30 | 31 | // cors: { 32 | // allRoutes: false, 33 | // allowOrigins: '*', 34 | // allowCredentials: false, 35 | // }, 36 | 37 | 38 | /**************************************************************************** 39 | * * 40 | * CSRF protection should be enabled for this application. * 41 | * * 42 | * For more information, see: * 43 | * https://sailsjs.com/docs/concepts/security/csrf * 44 | * * 45 | ****************************************************************************/ 46 | 47 | csrf: true 48 | 49 | }; 50 | -------------------------------------------------------------------------------- /assets/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // ╔═╗╔═╗╦ ╦╔╗╔╔╦╗┬─┐┌─┐ ┌─┐┬ ┬┌─┐┬─┐┬─┐┬┌┬┐┌─┐ 3 | // ║╣ ╚═╗║ ║║║║ ║ ├┬┘│ │ │└┐┌┘├┤ ├┬┘├┬┘│ ││├┤ 4 | // o╚═╝╚═╝╩═╝╩╝╚╝ ╩ ┴└─└─┘ └─┘ └┘ └─┘┴└─┴└─┴─┴┘└─┘ 5 | // ┌─ ┌─┐┌─┐┬─┐ ┌┐ ┬─┐┌─┐┬ ┬┌─┐┌─┐┬─┐ ┬┌─┐ ┌─┐┌─┐┌─┐┌─┐┌┬┐┌─┐ ─┐ 6 | // │ ├┤ │ │├┬┘ ├┴┐├┬┘│ ││││└─┐├┤ ├┬┘ │└─┐ ├─┤└─┐└─┐├┤ │ └─┐ │ 7 | // └─ └ └─┘┴└─ └─┘┴└─└─┘└┴┘└─┘└─┘┴└─ └┘└─┘ ┴ ┴└─┘└─┘└─┘ ┴ └─┘ ─┘ 8 | // > An .eslintrc configuration override for use in the `assets/` directory. 9 | // 10 | // This extends the top-level .eslintrc file, primarily to change the set of 11 | // supported globals, as well as any other relevant settings. (Since JavaScript 12 | // code in the `assets/` folder is intended for the browser habitat, a different 13 | // set of globals is supported. For example, instead of Node.js/Sails globals 14 | // like `sails` and `process`, you have access to browser globals like `window`.) 15 | // 16 | // (See .eslintrc in the root directory of this Sails app for more context.) 17 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 18 | 19 | "extends": [ 20 | "../.eslintrc" 21 | ], 22 | 23 | "env": { 24 | "browser": true, 25 | "node": false 26 | }, 27 | 28 | "parserOptions": { 29 | "ecmaVersion": 8 30 | //^ If you are not using a transpiler like Babel, change this to `5`. 31 | }, 32 | 33 | "globals": { 34 | 35 | // Allow any window globals you're relying on here; e.g. 36 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 37 | "SAILS_LOCALS": true, 38 | "io": true, 39 | "Cloud": true, 40 | "parasails": true, 41 | "$": true, 42 | "_": true, 43 | "bowser": true, 44 | "StripeCheckout": true, 45 | "Stripe": true, 46 | "Vue": true, 47 | "VueRouter": true, 48 | "moment": true, 49 | // "google": true, 50 | // ...etc. 51 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 52 | 53 | // Make sure backend globals aren't indadvertently tolerated in our client-side JS: 54 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 55 | "sails": false, 56 | "User": false 57 | // ...and any other backend globals (e.g. `"Organization": false`) 58 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /assets/js/pages/account/edit-profile.page.js: -------------------------------------------------------------------------------- 1 | parasails.registerPage('edit-profile', { 2 | // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ 3 | // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ 4 | // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ 5 | data: { 6 | // Main syncing/loading state for this page. 7 | syncing: false, 8 | 9 | // Form data 10 | formData: { /* … */ }, 11 | 12 | // For tracking client-side validation errors in our form. 13 | // > Has property set to `true` for each invalid property in `formData`. 14 | formErrors: { /* … */ }, 15 | 16 | // Server error state for the form 17 | cloudError: '', 18 | }, 19 | 20 | // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ 21 | // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ 22 | // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ 23 | beforeMount: function() { 24 | // Attach raw data exposed by the server. 25 | _.extend(this, SAILS_LOCALS); 26 | 27 | // Set the form data. 28 | this.formData.fullName = this.me.fullName; 29 | this.formData.emailAddress = this.me.emailChangeCandidate ? this.me.emailChangeCandidate : this.me.emailAddress; 30 | }, 31 | mounted: async function() { 32 | //… 33 | }, 34 | 35 | // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ 36 | // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ 37 | // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ 38 | methods: { 39 | 40 | submittedForm: async function() { 41 | // Redirect to the account page on success. 42 | // > (Note that we re-enable the syncing state here. This is on purpose-- 43 | // > to make sure the spinner stays there until the page navigation finishes.) 44 | this.syncing = true; 45 | window.location = '/main'; 46 | }, 47 | 48 | handleParsingForm: function() { 49 | // Clear out any pre-existing error messages. 50 | this.formErrors = {}; 51 | 52 | var argins = this.formData; 53 | 54 | // Validate name: 55 | if(!argins.fullName) { 56 | this.formErrors.fullName = true; 57 | } 58 | 59 | // Validate email: 60 | if(!argins.emailAddress) { 61 | this.formErrors.emailAddress = true; 62 | } 63 | 64 | // If there were any issues, they've already now been communicated to the user, 65 | // so simply return undefined. (This signifies that the submission should be 66 | // cancelled.) 67 | if (Object.keys(this.formErrors).length > 0) { 68 | return; 69 | } 70 | 71 | return argins; 72 | }, 73 | 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /api/helpers/send-telegram.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | friendlyName: 'Send telegram', 4 | 5 | description: 'Send notification to a telegram bot', 6 | 7 | inputs: { 8 | target: { 9 | description: 'Target url', 10 | example: 'https://www.google.com/test.js', 11 | type: 'string', 12 | required: true, // Ensure that target is required 13 | }, 14 | charChange: { 15 | description: 'Number of different chars', 16 | example: 43, 17 | type: 'number', 18 | required: true, // Ensure that charChange is required 19 | }, 20 | }, 21 | 22 | exits: { 23 | success: { 24 | description: 'All done.', 25 | }, 26 | 27 | invalidInput: { 28 | description: 'Invalid input provided.', 29 | responseType: 'badRequest', 30 | }, 31 | 32 | settingNotFound: { 33 | description: 'Telegram settings not found in the database.', 34 | responseType: 'notFound', 35 | }, 36 | 37 | telegramError: { 38 | description: 'Failed to send message via Telegram.', 39 | responseType: 'serverError', 40 | }, 41 | }, 42 | 43 | fn: async function (inputs, exits) { 44 | const TelegramBot = require('node-telegram-bot-api'); 45 | let setting; 46 | 47 | try { 48 | setting = await Setting.find(); 49 | if (!setting || setting.length === 0) { 50 | return exits.settingNotFound({ message: 'Telegram settings not found.' }); 51 | } 52 | setting = setting[0]; 53 | } catch (error) { 54 | sails.log.error('Error retrieving settings from the database:', error); 55 | return exits.settingNotFound({ message: 'Error retrieving Telegram settings.' }); 56 | } 57 | 58 | const token = setting.telegramToken; 59 | const chatID = setting.telegramChatID; 60 | 61 | if (!token || !chatID) { 62 | sails.log.error('Telegram token or chat ID is missing in settings.'); 63 | return exits.settingNotFound({ message: 'Telegram token or chat ID is missing.' }); 64 | } 65 | 66 | const bot = new TelegramBot(token); 67 | 68 | let message = `${inputs.target} has an update with ${inputs.charChange} different chars`; 69 | 70 | try { 71 | await bot.sendMessage(chatID, message); 72 | } catch (error) { 73 | sails.log.error('Error sending message via Telegram:', error); 74 | return exits.telegramError({ message: 'Failed to send message via Telegram.' }); 75 | } 76 | 77 | return exits.success(); 78 | } 79 | 80 | }; 81 | -------------------------------------------------------------------------------- /views/pages/edit-settings.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Settings

5 |
6 | 7 |
8 |
9 |
10 | 11 | 12 |
Please enter a valid Telegram bot token.
13 |
14 |
15 |
16 |
17 | 18 | 19 |
Please enter a valid chat id.
20 |
21 |
22 | 23 |
24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 |
32 |
33 |

An error occured while processing your request. Please check your information and try again.

34 |
35 | 36 | 37 |
38 |
39 | Cancel 40 | Save changes 41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 |
49 | <%- /* Expose locals as `window.SAILS_LOCALS` :: */ exposeLocalsToBrowser() %> 50 | -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route Mappings 3 | * (sails.config.routes) 4 | * 5 | * Your routes tell Sails what to do each time it receives a request. 6 | * 7 | * For more information on configuring custom routes, check out: 8 | * https://sailsjs.com/anatomy/config/routes-js 9 | */ 10 | 11 | module.exports.routes = { 12 | 13 | // ╦ ╦╔═╗╔╗ ╔═╗╔═╗╔═╗╔═╗╔═╗ 14 | // ║║║║╣ ╠╩╗╠═╝╠═╣║ ╦║╣ ╚═╗ 15 | // ╚╩╝╚═╝╚═╝╩ ╩ ╩╚═╝╚═╝╚═╝ 16 | 'GET /': '/main', 17 | 'GET /main/:unused?': { action: 'dashboard/view-main' }, 18 | 19 | 20 | 21 | 'GET /login': { action: 'entrance/view-login' }, 22 | 23 | 'GET /account': { action: 'account/view-account-overview' }, 24 | 'GET /account/password': { action: 'account/view-edit-password' }, 25 | 'GET /account/profile': { action: 'account/view-edit-profile' }, 26 | 'GET /settings': { action: 'settings/view-update-settings' }, 27 | 28 | 29 | // ╔╦╗╦╔═╗╔═╗ ╦═╗╔═╗╔╦╗╦╦═╗╔═╗╔═╗╔╦╗╔═╗ ┬ ╔╦╗╔═╗╦ ╦╔╗╔╦ ╔═╗╔═╗╔╦╗╔═╗ 30 | // ║║║║╚═╗║ ╠╦╝║╣ ║║║╠╦╝║╣ ║ ║ ╚═╗ ┌┼─ ║║║ ║║║║║║║║ ║ ║╠═╣ ║║╚═╗ 31 | // ╩ ╩╩╚═╝╚═╝ ╩╚═╚═╝═╩╝╩╩╚═╚═╝╚═╝ ╩ ╚═╝ └┘ ═╩╝╚═╝╚╩╝╝╚╝╩═╝╚═╝╩ ╩═╩╝╚═╝ 32 | '/logout': '/api/v1/account/logout', 33 | 34 | 35 | // ╦ ╦╔═╗╔╗ ╦ ╦╔═╗╔═╗╦╔═╔═╗ 36 | // ║║║║╣ ╠╩╗╠═╣║ ║║ ║╠╩╗╚═╗ 37 | // ╚╩╝╚═╝╚═╝╩ ╩╚═╝╚═╝╩ ╩╚═╝ 38 | // … 39 | 40 | 41 | // ╔═╗╔═╗╦ ╔═╗╔╗╔╔╦╗╔═╗╔═╗╦╔╗╔╔╦╗╔═╗ 42 | // ╠═╣╠═╝║ ║╣ ║║║ ║║╠═╝║ ║║║║║ ║ ╚═╗ 43 | // ╩ ╩╩ ╩ ╚═╝╝╚╝═╩╝╩ ╚═╝╩╝╚╝ ╩ ╚═╝ 44 | // Note that, in this app, these API endpoints may be accessed using the `Cloud.*()` methods 45 | // from the Parasails library, or by using those method names as the `action` in . 46 | '/api/v1/account/logout': { action: 'account/logout' }, 47 | 'PUT /api/v1/account/update-password': { action: 'account/update-password' }, 48 | 'PUT /api/v1/account/update-profile': { action: 'account/update-profile' }, 49 | 50 | 'PUT /api/v1/settings': { action: 'settings/update-settings' }, 51 | 52 | 'GET /api/v1/links': { action: 'link/get-links' }, 53 | 'POST /api/v1/links': { action: 'link/add-link' }, 54 | 'GET /api/v1/links/:id': { action: 'link/get-links' }, 55 | 'PUT /api/v1/links/:id': { action: 'link/update-link', csrf: false }, 56 | 'DELETE /api/v1/links/:id': { action: 'link/delete-link', csrf: false }, 57 | 58 | 'PUT /api/v1/entrance/login': { action: 'entrance/login' }, 59 | 60 | }; 61 | -------------------------------------------------------------------------------- /config/http.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP Server Settings 3 | * (sails.config.http) 4 | * 5 | * Configuration for the underlying HTTP server in Sails. 6 | * (for additional recommended settings, see `config/env/production.js`) 7 | * 8 | * For more information on configuration, check out: 9 | * https://sailsjs.com/config/http 10 | */ 11 | 12 | module.exports.http = { 13 | 14 | /**************************************************************************** 15 | * * 16 | * Sails/Express middleware to run for every HTTP request. * 17 | * (Only applies to HTTP requests -- not virtual WebSocket requests.) * 18 | * * 19 | * https://sailsjs.com/documentation/concepts/middleware * 20 | * * 21 | ****************************************************************************/ 22 | 23 | middleware: { 24 | 25 | /*************************************************************************** 26 | * * 27 | * The order in which middleware should be run for HTTP requests. * 28 | * (This Sails app's routes are handled by the "router" middleware below.) * 29 | * * 30 | ***************************************************************************/ 31 | 32 | // order: [ 33 | // 'cookieParser', 34 | // 'session', 35 | // 'bodyParser', 36 | // 'compress', 37 | // 'poweredBy', 38 | // 'router', 39 | // 'www', 40 | // 'favicon', 41 | // ], 42 | 43 | 44 | /*************************************************************************** 45 | * * 46 | * The body parser that will handle incoming multipart HTTP requests. * 47 | * * 48 | * https://sailsjs.com/config/http#?customizing-the-body-parser * 49 | * * 50 | ***************************************************************************/ 51 | 52 | // bodyParser: (function _configureBodyParser(){ 53 | // var skipper = require('skipper'); 54 | // var middlewareFn = skipper({ strict: true }); 55 | // return middlewareFn; 56 | // })(), 57 | 58 | }, 59 | 60 | }; 61 | -------------------------------------------------------------------------------- /config/globals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global Variable Configuration 3 | * (sails.config.globals) 4 | * 5 | * Configure which global variables which will be exposed 6 | * automatically by Sails. 7 | * 8 | * For more information on any of these options, check out: 9 | * https://sailsjs.com/config/globals 10 | */ 11 | 12 | module.exports.globals = { 13 | 14 | /**************************************************************************** 15 | * * 16 | * Whether to expose the locally-installed Lodash as a global variable * 17 | * (`_`), making it accessible throughout your app. * 18 | * * 19 | ****************************************************************************/ 20 | 21 | _: require('@sailshq/lodash'), 22 | 23 | /**************************************************************************** 24 | * * 25 | * This app was generated without a dependency on the "async" NPM package. * 26 | * * 27 | * > Don't worry! This is totally unrelated to JavaScript's "async/await". * 28 | * > Your code can (and probably should) use `await` as much as possible. * 29 | * * 30 | ****************************************************************************/ 31 | 32 | async: false, 33 | 34 | /**************************************************************************** 35 | * * 36 | * Whether to expose each of your app's models as global variables. * 37 | * (See the link at the top of this file for more information.) * 38 | * * 39 | ****************************************************************************/ 40 | 41 | models: true, 42 | 43 | /**************************************************************************** 44 | * * 45 | * Whether to expose the Sails app instance as a global variable (`sails`), * 46 | * making it accessible throughout your app. * 47 | * * 48 | ****************************************************************************/ 49 | 50 | sails: true, 51 | 52 | }; 53 | -------------------------------------------------------------------------------- /config/datastores.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Datastores 3 | * (sails.config.datastores) 4 | * 5 | * A set of datastore configurations which tell Sails where to fetch or save 6 | * data when you execute built-in model methods like `.find()` and `.create()`. 7 | * 8 | * > This file is mainly useful for configuring your development database, 9 | * > as well as any additional one-off databases used by individual models. 10 | * > Ready to go live? Head towards `config/env/production.js`. 11 | * 12 | * For more information on configuring datastores, check out: 13 | * https://sailsjs.com/config/datastores 14 | */ 15 | require('dotenv').config(); 16 | module.exports.datastores = { 17 | 18 | 19 | /*************************************************************************** 20 | * * 21 | * Your app's default datastore. * 22 | * * 23 | * Sails apps read and write to local disk by default, using a built-in * 24 | * database adapter called `sails-disk`. This feature is purely for * 25 | * convenience during development; since `sails-disk` is not designed for * 26 | * use in a production environment. * 27 | * * 28 | * To use a different db _in development_, follow the directions below. * 29 | * Otherwise, just leave the default datastore as-is, with no `adapter`. * 30 | * * 31 | * (For production configuration, see `config/env/production.js`.) * 32 | * * 33 | ***************************************************************************/ 34 | 35 | default: { 36 | 37 | /*************************************************************************** 38 | * * 39 | * Want to use a different database during development? * 40 | * * 41 | * 1. Choose an adapter: * 42 | * https://sailsjs.com/plugins/databases * 43 | * * 44 | * 2. Install it as a dependency of your Sails app. * 45 | * (For example: npm install sails-mysql --save) * 46 | * * 47 | * 3. Then pass it in, along with a connection URL. * 48 | * (See https://sailsjs.com/config/datastores for help.) * 49 | * * 50 | ***************************************************************************/ 51 | // adapter: 'sails-mysql', 52 | // url: 'mysql://user:password@host:port/database', 53 | 54 | adapter: 'sails-mongo', 55 | url: process.env.MONGO_URL 56 | 57 | }, 58 | 59 | 60 | }; 61 | -------------------------------------------------------------------------------- /assets/styles/pages/homepage.less: -------------------------------------------------------------------------------- 1 | #homepage { 2 | 3 | .hero { 4 | padding-top: 100px; 5 | padding-bottom: 25px; 6 | color: @brand; 7 | position: relative; 8 | .hero-image { 9 | width: 220px; 10 | height: 170px; 11 | margin-left: auto; 12 | margin-right: auto; 13 | position: relative; 14 | img { 15 | position: absolute; 16 | } 17 | .sky { 18 | width: 170px; 19 | left: 25px; 20 | top: 25px; 21 | } 22 | .cloud { 23 | .fly-fade(); 24 | width: 80px; 25 | &.cloud-1 { 26 | top: 55px; 27 | left: -40px; 28 | opacity: 0; 29 | .animation-delay(3.5s); 30 | } 31 | &.cloud-2 { 32 | top: 45px; 33 | left: -40px; 34 | opacity: 0; 35 | } 36 | } 37 | .ship { 38 | .skid(); 39 | width: 160px; 40 | bottom: 50px; 41 | left: 18px; 42 | } 43 | .water { 44 | width: 170px; 45 | bottom: 40px; 46 | left: 25px; 47 | } 48 | } 49 | h1 { 50 | padding-bottom: 50px; 51 | } 52 | .more-info-text { 53 | .bob(); 54 | cursor: pointer; 55 | margin-top: 75px; 56 | position: absolute; 57 | width: 100%; 58 | bottom: 25px; 59 | left: 0px; 60 | .text { 61 | font-size: 16px; 62 | text-transform: uppercase; 63 | letter-spacing: 2px; 64 | font-weight: 700; 65 | } 66 | .icon { 67 | font-size: 20px; 68 | } 69 | } 70 | } 71 | 72 | .about-wrapper { 73 | background-color: #eef5f9b8; 74 | .about { 75 | padding-top: 75px; 76 | padding-bottom: 50px; 77 | p { 78 | max-width: 800px; 79 | margin-left: auto; 80 | margin-right: auto; 81 | } 82 | } 83 | .features { 84 | padding-top: 25px; 85 | padding-bottom: 100px; 86 | .feature { 87 | .icon { 88 | background-color: @brand; 89 | color: @accent-white; 90 | width: 75px; 91 | height: 75px; 92 | margin-left: auto; 93 | margin-right: auto; 94 | margin-bottom: 25px; 95 | border-radius: 50%; 96 | font-size: 35px; 97 | line-height: 75px; 98 | text-align: center; 99 | } 100 | } 101 | } 102 | } 103 | 104 | 105 | .setup { 106 | padding-top: 75px; 107 | .step { 108 | margin-top: 75px; 109 | margin-bottom: 75px; 110 | padding-left: 240px; 111 | position: relative; 112 | .step-image { 113 | position: absolute; 114 | left: 0px; 115 | top: 0px; 116 | width: 140px; 117 | img { 118 | width: 100%; 119 | } 120 | } 121 | } 122 | } 123 | 124 | .pep-talk { 125 | padding-top: 50px; 126 | padding-bottom: 100px; 127 | p { 128 | max-width: 800px; 129 | margin-left: auto; 130 | margin-right: auto; 131 | } 132 | a { 133 | border-bottom: none; 134 | } 135 | } 136 | 137 | &.uninitialized { 138 | height: 100%; 139 | .hero, .about, .features, .setup, .pep-talk { 140 | opacity: 0; 141 | } 142 | } 143 | 144 | @media (max-width: 991px) { 145 | .setup { 146 | .step { 147 | padding-left: 0px; 148 | .step-image { 149 | display: none; 150 | } 151 | } 152 | } 153 | } 154 | @media (max-width: 450px) { 155 | .hero { 156 | padding-top: 50px; 157 | h1.display-4 { 158 | font-size: 34px; 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /assets/js/utilities/open-stripe-checkout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * openStripeCheckout() 3 | * 4 | * Open the Stripe Checkout modal dialog and resolve when it is closed. 5 | * 6 | * ----------------------------------------------------------------- 7 | * @param {String} stripePublishableKey 8 | * @param {String} billingEmailAddress 9 | * @param {String} headingText (optional) 10 | * @param {String} descriptionText (optional) 11 | * @param {String} buttonText (optional) 12 | * ----------------------------------------------------------------- 13 | * @returns {Dictionary?} (or undefined if the form was cancelled) 14 | * e.g. 15 | * { 16 | * stripeToken: '…', 17 | * billingCardLast4: '…', 18 | * billingCardBrand: '…', 19 | * billingCardExpMonth: '…', 20 | * billingCardExpYear: '…' 21 | * } 22 | * ----------------------------------------------------------------- 23 | * Example usage: 24 | * ``` 25 | * var billingInfo = await openStripeCheckout( 26 | * 'pk_test_Qz5RfDmVV5IunTFAHtDqDWn4', 27 | * 'foo@example.com' 28 | * ); 29 | * ``` 30 | */ 31 | 32 | parasails.registerUtility('openStripeCheckout', async function openStripeCheckout(stripePublishableKey, billingEmailAddress, headingText, descriptionText, buttonText) { 33 | 34 | // Cache (& use cached) "checkout handler" globally on the page so that we 35 | // don't end up configuring it more than once (i.e. so Stripe.js doesn't 36 | // complain). 37 | var CACHE_KEY = '_cachedStripeCheckoutHandler'; 38 | if (!window[CACHE_KEY]) { 39 | window[CACHE_KEY] = StripeCheckout.configure({ 40 | key: stripePublishableKey, 41 | }); 42 | } 43 | var checkoutHandler = window[CACHE_KEY]; 44 | 45 | // Track whether the "token" callback was triggered. 46 | // (If it has NOT at the time the "closed" callback is triggered, then we 47 | // know the checkout form was cancelled.) 48 | var hasTriggeredTokenCallback; 49 | 50 | // Build a Promise & send it back as our "thenable" (AsyncFunction's return value). 51 | // (this is necessary b/c we're wrapping an api that isn't `await`-compatible) 52 | return new Promise((resolve, reject)=>{ 53 | try { 54 | // Open Stripe checkout. 55 | // (https://stripe.com/docs/checkout#integration-custom) 56 | checkoutHandler.open({ 57 | name: headingText || 'NEW_APP_NAME', 58 | description: descriptionText || 'Link your credit card.', 59 | panelLabel: buttonText || 'Save card', 60 | email: billingEmailAddress,//« So that Stripe doesn't prompt for an email address 61 | locale: 'auto', 62 | zipCode: false, 63 | allowRememberMe: false, 64 | closed: ()=>{ 65 | // If the Checkout dialog was cancelled, resolve undefined. 66 | if (!hasTriggeredTokenCallback) { 67 | resolve(); 68 | } 69 | }, 70 | token: (stripeData)=>{ 71 | 72 | // After payment info has been successfully added, and a token 73 | // was obtained... 74 | hasTriggeredTokenCallback = true; 75 | 76 | // Normalize token and billing card info from Stripe and resolve 77 | // with that. 78 | let stripeToken = stripeData.id; 79 | let billingCardLast4 = stripeData.card.last4; 80 | let billingCardBrand = stripeData.card.brand; 81 | let billingCardExpMonth = String(stripeData.card.exp_month); 82 | let billingCardExpYear = String(stripeData.card.exp_year); 83 | 84 | resolve({ 85 | stripeToken, 86 | billingCardLast4, 87 | billingCardBrand, 88 | billingCardExpMonth, 89 | billingCardExpYear 90 | }); 91 | }//Œ 92 | });//_∏_ 93 | } catch (err) { 94 | reject(err); 95 | } 96 | });//_∏_ 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /assets/styles/bootstrap-overrides.less: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is for overriding some default bootstrap styles. 3 | * 4 | * > NOTE THAT THIS FILE AFFECTS GLOBAL STYLES. 5 | */ 6 | 7 | // lesshint-disable 8 | * { 9 | box-sizing: border-box; 10 | } 11 | // lesshint-enable 12 | 13 | img { 14 | display: block; 15 | } 16 | 17 | // Get rid of weird background on s with button styles 18 | .btn, [type='button'] { 19 | -webkit-appearance: none; 20 | } 21 | 22 | // Custom link styles within bodies of text 23 | h1, h2, h3, h4, h5, h6, p, li, blockquote, label { 24 | >a:not(.btn), small >a:not(.btn) { 25 | color: @brand; 26 | border-bottom: 1px solid @text-normal; 27 | &:hover { 28 | text-decoration: none; 29 | color: @text-normal; 30 | } 31 | } 32 | } 33 | 34 | blockquote { 35 | border-left: 3px solid @border-lt-gray; 36 | padding-left: 20px; 37 | } 38 | 39 | // Custom modal styles 40 | .modal-backdrop { 41 | background-color: @accent-white; 42 | &.show { 43 | opacity: 0.95; 44 | } 45 | } 46 | .modal { 47 | -webkit-overflow-scrolling: touch;//« makes this actually scrollable on certain phones 48 | .petticoat { 49 | position: fixed; 50 | width: 100%; 51 | height: 75px;// should cover topbar 52 | z-index: 50; 53 | left: 0px; 54 | top: 0px; 55 | background-color: @accent-white; 56 | } 57 | .modal-dialog { 58 | z-index: 100; 59 | position: relative; 60 | max-width: 700px; 61 | } 62 | .modal-content { 63 | max-width: 700px; 64 | border-radius: 0px; 65 | border-color: @accent-white; 66 | padding-top: 50px; 67 | padding-bottom: 50px; 68 | padding-left: 25px; 69 | padding-right: 25px; 70 | .modal-header { 71 | border-bottom: none; 72 | display: block; 73 | position: relative; 74 | text-align: center; 75 | padding-bottom: 0px; 76 | padding-left: 0px; 77 | padding-right: 0px; 78 | padding-top: 0px; 79 | .modal-close-button { 80 | .btn-reset(); 81 | width: 32px; 82 | height: 32px; 83 | padding: 6px; 84 | position: absolute; 85 | right: -5px; 86 | top: -5px; 87 | background-image: url('/images/icon-close.png'); 88 | background-size: 20px; 89 | background-repeat: no-repeat; 90 | background-position: center; 91 | opacity: 0.8; 92 | &:hover { 93 | opacity: 1; 94 | } 95 | } 96 | .modal-title { 97 | font-weight: @bold; 98 | } 99 | .modal-intro { 100 | margin-left: auto; 101 | margin-right: auto; 102 | color: @text-muted; 103 | margin-bottom: 20px; 104 | } 105 | hr { 106 | margin-top: 25px; 107 | margin-left: auto; 108 | margin-right: auto; 109 | margin-bottom: 10px; 110 | width: 100px; 111 | height: 2px; 112 | border-top: 2px solid @brand; 113 | } 114 | } 115 | .modal-body { 116 | padding-top: 10px; 117 | padding-bottom: 10px; 118 | padding-left: 0px; 119 | padding-right: 0px; 120 | .section-heading { 121 | margin-top: 25px; 122 | padding-bottom: 10px; 123 | margin-bottom: 25px; 124 | border-bottom: 1px solid @border-lt-gray; 125 | } 126 | } 127 | .modal-footer { 128 | padding-top: 25px; 129 | padding-bottom: 0px; 130 | padding-left: 0px; 131 | padding-right: 0px; 132 | border-top: 1px solid @border-lt-gray; 133 | margin-top: 10px; 134 | &.no-border { 135 | border-top: 0px; 136 | padding-top: 10px; 137 | margin-top: 0px; 138 | } 139 | } 140 | } 141 | @media screen and (max-width: 600px) { 142 | .modal-content { 143 | .modal-header { 144 | .modal-close-button { 145 | right: -20px; 146 | top: -45px; 147 | } 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################ 2 | # ┌─┐┬┌┬┐╦╔═╗╔╗╔╔═╗╦═╗╔═╗ 3 | # │ ┬│ │ ║║ ╦║║║║ ║╠╦╝║╣ 4 | # o└─┘┴ ┴ ╩╚═╝╝╚╝╚═╝╩╚═╚═╝ 5 | # 6 | # > Files to exclude from your app's repo. 7 | # 8 | # This file (`.gitignore`) is only relevant if 9 | # you are using git. 10 | # 11 | # It exists to signify to git that certain files 12 | # and/or directories should be ignored for the 13 | # purposes of version control. 14 | # 15 | # This keeps tmp files and sensitive credentials 16 | # from being uploaded to your repository. And 17 | # it allows you to configure your app for your 18 | # machine without accidentally committing settings 19 | # which will smash the local settings of other 20 | # developers on your team. 21 | # 22 | # Some reasonable defaults are included below, 23 | # but, of course, you should modify/extend/prune 24 | # to fit your needs! 25 | # 26 | ################################################ 27 | 28 | 29 | ################################################ 30 | # Local Configuration 31 | # 32 | # Explicitly ignore files which contain: 33 | # 34 | # 1. Sensitive information you'd rather not push to 35 | # your git repository. 36 | # e.g., your personal API keys or passwords. 37 | # 38 | # 2. Developer-specific configuration 39 | # Basically, anything that would be annoying 40 | # to have to change every time you do a 41 | # `git pull` on your laptop. 42 | # e.g. your local development database, or 43 | # the S3 bucket you're using for file uploads 44 | # during development. 45 | # 46 | ################################################ 47 | 48 | config/local.js 49 | 50 | 51 | ################################################ 52 | # Dependencies 53 | # 54 | # 55 | # When releasing a production app, you _could_ 56 | # hypothetically include your node_modules folder 57 | # in your git repo, but during development, it 58 | # is always best to exclude it, since different 59 | # developers may be working on different kernels, 60 | # where dependencies would need to be recompiled 61 | # anyway. 62 | # 63 | # Most of the time, the node_modules folder can 64 | # be excluded from your code repository, even 65 | # in production, thanks to features like the 66 | # package-lock.json file / NPM shrinkwrap. 67 | # 68 | # But no matter what, since this is a Sails app, 69 | # you should always push up the package-lock.json 70 | # or shrinkwrap file to your repository, to avoid 71 | # accidentally pulling in upgraded dependencies 72 | # and breaking your code. 73 | # 74 | # That said, if you are having trouble with 75 | # dependencies, (particularly when using 76 | # `npm link`) this can be pretty discouraging. 77 | # But rather than just adding the lockfile to 78 | # your .gitignore, try this first: 79 | # ``` 80 | # rm -rf node_modules 81 | # rm package-lock.json 82 | # npm install 83 | # ``` 84 | # 85 | # [?] For more tips/advice, come by and say hi 86 | # over at https://sailsjs.com/support 87 | # 88 | ################################################ 89 | 90 | node_modules 91 | 92 | 93 | ################################################ 94 | # 95 | # > Do you use bower? 96 | # > re: the bower_components dir, see this: 97 | # > http://addyosmani.com/blog/checking-in-front-end-dependencies/ 98 | # > (credit Addy Osmani, @addyosmani) 99 | # 100 | ################################################ 101 | 102 | 103 | ################################################ 104 | # Temporary files generated by Sails/Waterline. 105 | ################################################ 106 | 107 | .tmp 108 | 109 | 110 | ################################################ 111 | # Miscellaneous 112 | # 113 | # Common files generated by text editors, 114 | # operating systems, file systems, dbs, etc. 115 | ################################################ 116 | 117 | *~ 118 | *# 119 | .DS_STORE 120 | .netbeans 121 | nbproject 122 | .idea 123 | *.iml 124 | .vscode 125 | .node_history 126 | dump.rdb 127 | 128 | npm-debug.log 129 | lib-cov 130 | *.seed 131 | *.log 132 | *.out 133 | *.pid 134 | 135 | -------------------------------------------------------------------------------- /config/sockets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WebSocket Server Settings 3 | * (sails.config.sockets) 4 | * 5 | * Use the settings below to configure realtime functionality in your app. 6 | * (for additional recommended settings, see `config/env/production.js`) 7 | * 8 | * For all available options, see: 9 | * https://sailsjs.com/config/sockets 10 | */ 11 | 12 | module.exports.sockets = { 13 | 14 | /*************************************************************************** 15 | * * 16 | * `transports` * 17 | * * 18 | * The protocols or "transports" that socket clients are permitted to * 19 | * use when connecting and communicating with this Sails application. * 20 | * * 21 | * > Never change this here without also configuring `io.sails.transports` * 22 | * > in your client-side code. If the client and the server are not using * 23 | * > the same array of transports, sockets will not work properly. * 24 | * > * 25 | * > For more info, see: * 26 | * > https://sailsjs.com/docs/reference/web-sockets/socket-client * 27 | * * 28 | ***************************************************************************/ 29 | 30 | // transports: [ 'websocket' ], 31 | 32 | 33 | /*************************************************************************** 34 | * * 35 | * `beforeConnect` * 36 | * * 37 | * This custom beforeConnect function will be run each time BEFORE a new * 38 | * socket is allowed to connect, when the initial socket.io handshake is * 39 | * performed with the server. * 40 | * * 41 | * https://sailsjs.com/config/sockets#?beforeconnect * 42 | * * 43 | ***************************************************************************/ 44 | 45 | // beforeConnect: function(handshake, proceed) { 46 | // 47 | // // `true` allows the socket to connect. 48 | // // (`false` would reject the connection) 49 | // return proceed(undefined, true); 50 | // 51 | // }, 52 | 53 | 54 | /*************************************************************************** 55 | * * 56 | * `afterDisconnect` * 57 | * * 58 | * This custom afterDisconnect function will be run each time a socket * 59 | * disconnects * 60 | * * 61 | ***************************************************************************/ 62 | 63 | // afterDisconnect: function(session, socket, done) { 64 | // 65 | // // By default: do nothing. 66 | // // (but always trigger the callback) 67 | // return done(); 68 | // 69 | // }, 70 | 71 | 72 | /*************************************************************************** 73 | * * 74 | * Whether to expose a 'GET /__getcookie' route that sets an HTTP-only * 75 | * session cookie. * 76 | * * 77 | ***************************************************************************/ 78 | 79 | // grant3rdPartyCookie: true, 80 | 81 | 82 | }; 83 | -------------------------------------------------------------------------------- /api/controllers/entrance/login.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 4 | friendlyName: 'Login', 5 | 6 | 7 | description: 'Log in using the provided email and password combination.', 8 | 9 | 10 | extendedDescription: 11 | `This action attempts to look up the user record in the database with the 12 | specified email address. Then, if such a user exists, it uses 13 | bcrypt to compare the hashed password from the database with the provided 14 | password attempt.`, 15 | 16 | 17 | inputs: { 18 | 19 | emailAddress: { 20 | description: 'The email to try in this attempt, e.g. "irl@example.com".', 21 | type: 'string', 22 | required: true 23 | }, 24 | 25 | password: { 26 | description: 'The unencrypted password to try in this attempt, e.g. "passwordlol".', 27 | type: 'string', 28 | required: true 29 | }, 30 | 31 | rememberMe: { 32 | description: 'Whether to extend the lifetime of the user\'s session.', 33 | extendedDescription: 34 | `Note that this is NOT SUPPORTED when using virtual requests (e.g. sending 35 | requests over WebSockets instead of HTTP).`, 36 | type: 'boolean' 37 | } 38 | 39 | }, 40 | 41 | exits: { 42 | 43 | success: { 44 | description: 'The requesting user agent has been successfully logged in.', 45 | extendedDescription: 46 | `Under the covers, this stores the id of the logged-in user in the session 47 | as the \`userId\` key. The next time this user agent sends a request, assuming 48 | it includes a cookie (like a web browser), Sails will automatically make this 49 | user id available as req.session.userId in the corresponding action. (Also note 50 | that, thanks to the included "custom" hook, when a relevant request is received 51 | from a logged-in user, that user's entire record from the database will be fetched 52 | and exposed as \`req.me\`.)` 53 | }, 54 | 55 | badCombo: { 56 | description: `The provided email and password combination does not 57 | match any user in the database.`, 58 | responseType: 'unauthorized' 59 | // ^This uses the custom `unauthorized` response located in `api/responses/unauthorized.js`. 60 | // To customize the generic "unauthorized" response across this entire app, change that file 61 | // (see api/responses/unauthorized). 62 | // 63 | // To customize the response for _only this_ action, replace `responseType` with 64 | // something else. For example, you might set `statusCode: 498` and change the 65 | // implementation below accordingly (see http://sailsjs.com/docs/concepts/controllers). 66 | } 67 | 68 | }, 69 | 70 | 71 | fn: async function (inputs) { 72 | 73 | // Look up by the email address. 74 | // (note that we lowercase it to ensure the lookup is always case-insensitive, 75 | // regardless of which database we're using) 76 | 77 | var userRecord = await User.findOne({ 78 | emailAddress: inputs.emailAddress.toLowerCase(), 79 | }); 80 | 81 | // If there was no matching user, respond thru the "badCombo" exit. 82 | if(!userRecord) { 83 | throw 'badCombo'; 84 | } 85 | 86 | // If the password doesn't match, then also exit thru "badCombo". 87 | await sails.helpers.passwords.checkPassword(inputs.password, userRecord.password) 88 | .intercept('incorrect', 'badCombo'); 89 | 90 | // If "Remember Me" was enabled, then keep the session alive for 91 | // a longer amount of time. (This causes an updated "Set Cookie" 92 | // response header to be sent as the result of this request -- thus 93 | // we must be dealing with a traditional HTTP request in order for 94 | // this to work.) 95 | if (inputs.rememberMe) { 96 | if (this.req.isSocket) { 97 | sails.log.warn( 98 | 'Received `rememberMe: true` from a virtual request, but it was ignored\n'+ 99 | 'because a browser\'s session cookie cannot be reset over sockets.\n'+ 100 | 'Please use a traditional HTTP request instead.' 101 | ); 102 | } else { 103 | this.req.session.cookie.maxAge = sails.config.custom.rememberMeCookieMaxAge; 104 | } 105 | }//fi 106 | 107 | // Modify the active session instance. 108 | // (This will be persisted when the response is sent.) 109 | this.req.session.userId = userRecord.id; 110 | 111 | } 112 | 113 | }; 114 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // ╔═╗╔═╗╦ ╦╔╗╔╔╦╗┬─┐┌─┐ 3 | // ║╣ ╚═╗║ ║║║║ ║ ├┬┘│ 4 | // o╚═╝╚═╝╩═╝╩╝╚╝ ╩ ┴└─└─┘ 5 | // A set of basic code conventions designed to encourage quality and consistency 6 | // across your Sails app's code base. These rules are checked against 7 | // automatically any time you run `npm test`. 8 | // 9 | // > An additional eslintrc override file is included in the `assets/` folder 10 | // > right out of the box. This is specifically to allow for variations in acceptable 11 | // > global variables between front-end JavaScript code designed to run in the browser 12 | // > vs. backend code designed to run in a Node.js/Sails process. 13 | // 14 | // > Note: If you're using mocha, you'll want to add an extra override file to your 15 | // > `test/` folder so that eslint will tolerate mocha-specific globals like `before` 16 | // > and `describe`. 17 | // Designed for ESLint v4. 18 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 19 | // For more information about any of the rules below, check out the relevant 20 | // reference page on eslint.org. For example, to get details on "no-sequences", 21 | // you would visit `http://eslint.org/docs/rules/no-sequences`. If you're unsure 22 | // or could use some advice, come by https://sailsjs.com/support. 23 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 24 | 25 | "env": { 26 | "node": true 27 | }, 28 | 29 | "parserOptions": { 30 | "ecmaVersion": 2018 31 | }, 32 | 33 | "globals": { 34 | // If "no-undef" is enabled below, be sure to list all global variables that 35 | // are used in this app's backend code (including the globalIds of models): 36 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 37 | "Promise": true, 38 | "sails": true, 39 | "_": true, 40 | 41 | // Models: 42 | "User": true, 43 | "Target":true, 44 | "Setting":true 45 | 46 | // …and any others. 47 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 48 | }, 49 | 50 | "rules": { 51 | "block-scoped-var": ["error"], 52 | "callback-return": ["error", ["done", "proceed", "next", "onwards", "callback", "cb"]], 53 | "camelcase": ["warn", {"properties":"always"}], 54 | "comma-style": ["warn", "last"], 55 | "curly": ["warn"], 56 | "eqeqeq": ["error", "always"], 57 | "eol-last": ["warn"], 58 | "handle-callback-err": ["error"], 59 | "indent": ["warn", 2, { 60 | "SwitchCase": 1, 61 | "MemberExpression": "off", 62 | "FunctionDeclaration": {"body":1, "parameters":"off"}, 63 | "FunctionExpression": {"body":1, "parameters":"off"}, 64 | "CallExpression": {"arguments":"off"}, 65 | "ArrayExpression": 1, 66 | "ObjectExpression": 1, 67 | "ignoredNodes": ["ConditionalExpression"] 68 | }], 69 | "linebreak-style": ["error", "unix"], 70 | "no-dupe-keys": ["error"], 71 | "no-duplicate-case": ["error"], 72 | "no-extra-semi": ["warn"], 73 | "no-labels": ["error"], 74 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 75 | "no-redeclare": ["warn"], 76 | "no-return-assign": ["error", "always"], 77 | "no-sequences": ["error"], 78 | "no-trailing-spaces": ["warn"], 79 | "no-undef": ["error"], 80 | "no-unexpected-multiline": ["warn"], 81 | "no-unreachable": ["warn"], 82 | "no-unused-vars": ["warn", {"caughtErrors":"all", "caughtErrorsIgnorePattern": "^unused($|[A-Z].*$)", "argsIgnorePattern": "^unused($|[A-Z].*$)", "varsIgnorePattern": "^unused($|[A-Z].*$)" }], 83 | "no-use-before-define": ["error", {"functions":false}], 84 | "one-var": ["warn", "never"], 85 | "prefer-arrow-callback": ["warn", {"allowNamedFunctions":true}], 86 | "quotes": ["warn", "single", {"avoidEscape":false, "allowTemplateLiterals":true}], 87 | "semi": ["warn", "always"], 88 | "semi-spacing": ["warn", {"before":false, "after":true}], 89 | "semi-style": ["warn", "last"] 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /api/helpers/diff-highlight.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-redeclare */ 2 | /* eslint-disable eqeqeq */ 3 | /* eslint-disable block-scoped-var */ 4 | module.exports = { 5 | 6 | 7 | friendlyName: 'Diff highlight', 8 | 9 | 10 | description: '', 11 | 12 | 13 | inputs: { 14 | res1: { 15 | description: 'First page', 16 | example: 'HTML page', 17 | type: 'string', 18 | }, 19 | res2: { 20 | description: 'Second page', 21 | example: 'HTML page', 22 | type: 'string', 23 | }, 24 | }, 25 | 26 | 27 | exits: { 28 | 29 | success: { 30 | description: 'All done.', 31 | }, 32 | 33 | }, 34 | 35 | 36 | fn: async function (inputs, exits) { 37 | /* 38 | * Javascript Diff Algorithm 39 | * By John Resig (http://ejohn.org/) 40 | * Modified by Chu Alan "sprite" 41 | * 42 | * Released under the MIT license. 43 | * 44 | * More Info: 45 | * http://ejohn.org/projects/javascript-diff-algorithm/ 46 | */ 47 | 48 | let diffHighlight = diffString(inputs.res1, inputs.res2); 49 | 50 | return exits.success(diffHighlight); 51 | 52 | function diffString( o, n ) { 53 | o = o.replace(/\s+$/, ''); 54 | n = n.replace(/\s+$/, ''); 55 | 56 | var out = diff(o == '' ? [] : o.split(/\s+/), n == '' ? [] : n.split(/\s+/) ); 57 | var str = ''; 58 | 59 | var oSpace = o.match(/\s+/g); 60 | if (oSpace == null) { 61 | oSpace = ['\n']; 62 | } else { 63 | oSpace.push('\n'); 64 | } 65 | var nSpace = n.match(/\s+/g); 66 | if (nSpace == null) { 67 | nSpace = ['\n']; 68 | } else { 69 | nSpace.push('\n'); 70 | } 71 | 72 | if (out.n.length == 0) { 73 | for (var i = 0; i < out.o.length; i++) { 74 | str += '[[START_DEL_URL_Tracker]]' + out.o[i] + oSpace[i] + '[[END_DEL_URL_Tracker]]'; 75 | } 76 | } else { 77 | if (out.n[0].text == null) { 78 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 79 | str += '[[START_DEL_URL_Tracker]]' + out.o[n] + oSpace[n] + '[[END_DEL_URL_Tracker]]'; 80 | } 81 | } 82 | 83 | for ( var i = 0; i < out.n.length; i++ ) { 84 | if (out.n[i].text == null) { 85 | str += '[[START_INS_URL_Tracker]]' + out.n[i] + nSpace[i] + '[[END_INS_URL_Tracker]]'; 86 | } else { 87 | var pre = ''; 88 | 89 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { 90 | pre += '[[START_DEL_URL_Tracker]]' + out.o[n] + oSpace[n] + '[[END_DEL_URL_Tracker]]'; 91 | } 92 | str += ' ' + out.n[i].text + nSpace[i] + pre; 93 | } 94 | } 95 | } 96 | 97 | return str; 98 | } 99 | 100 | function diff( o, n ) { 101 | var ns = new Object(); 102 | var os = new Object(); 103 | 104 | for ( var i = 0; i < n.length; i++ ) { 105 | if ( ns[ n[i] ] == null ) 106 | {ns[ n[i] ] = { rows: new Array(), o: null };} 107 | ns[ n[i] ].rows.push( i ); 108 | } 109 | 110 | for ( var i = 0; i < o.length; i++ ) { 111 | if ( os[ o[i] ] == null ) 112 | {os[ o[i] ] = { rows: new Array(), n: null };} 113 | os[ o[i] ].rows.push( i ); 114 | } 115 | 116 | for ( var i in ns ) { 117 | if ( ns[i].rows.length == 1 && typeof(os[i]) !== 'undefined' && os[i].rows.length == 1 ) { 118 | n[ ns[i].rows[0] ] = { text: n[ ns[i].rows[0] ], row: os[i].rows[0] }; 119 | o[ os[i].rows[0] ] = { text: o[ os[i].rows[0] ], row: ns[i].rows[0] }; 120 | } 121 | } 122 | 123 | for ( var i = 0; i < n.length - 1; i++ ) { 124 | if ( n[i].text != null && n[i+1].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && 125 | n[i+1] == o[ n[i].row + 1 ] ) { 126 | n[i+1] = { text: n[i+1], row: n[i].row + 1 }; 127 | o[n[i].row+1] = { text: o[n[i].row+1], row: i + 1 }; 128 | } 129 | } 130 | 131 | for ( var i = n.length - 1; i > 0; i-- ) { 132 | if ( n[i].text != null && n[i-1].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && 133 | n[i-1] == o[ n[i].row - 1 ] ) { 134 | n[i-1] = { text: n[i-1], row: n[i].row - 1 }; 135 | o[n[i].row-1] = { text: o[n[i].row-1], row: i - 1 }; 136 | } 137 | } 138 | 139 | return { o: o, n: n }; 140 | } 141 | 142 | } 143 | 144 | 145 | }; 146 | 147 | -------------------------------------------------------------------------------- /config/env/staging.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Staging environment settings 3 | * (sails.config.*) 4 | * 5 | * This is mostly a carbon copy of the production environment settings 6 | * in config/env/production.js, but with the overrides listed below. 7 | * For more detailed information and links about what these settings do 8 | * see the production config file. 9 | * 10 | * > This file takes effect when `sails.config.environment` is "staging". 11 | * > But note that NODE_ENV should still be "production" when lifting 12 | * > your app in the staging environment. In other words: 13 | * > ``` 14 | * > NODE_ENV=production sails_environment=staging node app 15 | * > ``` 16 | * 17 | * If you're unsure or want advice, stop by: 18 | * https://sailsjs.com/support 19 | */ 20 | 21 | var PRODUCTION_CONFIG = require('./production'); 22 | //-------------------------------------------------------------------------- 23 | // /\ Start with your production config, even if it's just a guess for now, 24 | // || then configure your staging environment afterwards. 25 | // (That way, all you need to do in this file is set overrides.) 26 | //-------------------------------------------------------------------------- 27 | 28 | module.exports = Object.assign({}, PRODUCTION_CONFIG, { 29 | 30 | datastores: Object.assign({}, PRODUCTION_CONFIG.datastores, { 31 | default: Object.assign({}, PRODUCTION_CONFIG.datastores.default, { 32 | // url: 'mysql://shared:some_password_everyone_knows@db.example.com:3306/my_staging_db', 33 | //-------------------------------------------------------------------------- 34 | // /\ Hard-code your staging db `url`. 35 | // || (or use system env var: `sails_datastores__default__url`) 36 | //-------------------------------------------------------------------------- 37 | }) 38 | }), 39 | 40 | sockets: Object.assign({}, PRODUCTION_CONFIG.sockets, { 41 | 42 | onlyAllowOrigins: [ 43 | 'http://localhost:1337', 44 | // 'https://example-staging.herokuapp.com', 45 | // 'http://example-staging.herokuapp.com', 46 | // 'https://staging.example.com', 47 | // 'http://staging.example.com', 48 | ], 49 | //-------------------------------------------------------------------------- 50 | // /\ Hard-code a staging-only override for allowed origins. 51 | // || (or set this array via JSON-encoded system env var) 52 | // ``` 53 | // sails_sockets__onlyAllowOrigins='["http://localhost:1337", "…"]' 54 | // ``` 55 | //-------------------------------------------------------------------------- 56 | 57 | // url: 'redis://shared:some_password_everyone_knows@bigsquid.redistogo.com:9562/', 58 | //-------------------------------------------------------------------------- 59 | // /\ Hard-code your staging Redis server's `url`. 60 | // || (or use system env var: `sails_sockets__url`) 61 | //-------------------------------------------------------------------------- 62 | }), 63 | 64 | session: Object.assign({}, PRODUCTION_CONFIG.session, { 65 | // url: 'redis://shared:some_password_everyone_knows@bigsquid.redistogo.com:9562/staging-sessions', 66 | //-------------------------------------------------------------------------- 67 | // /\ Hard-code your staging Redis server's `url` again here. 68 | // || (or use system env var: `sails_session__url`) 69 | //-------------------------------------------------------------------------- 70 | }), 71 | 72 | custom: Object.assign({}, PRODUCTION_CONFIG.custom, { 73 | 74 | baseUrl: 'https://staging.example.com', 75 | //-------------------------------------------------------------------------- 76 | // /\ Hard-code the base URL where your staging environment is hosted. 77 | // || (or use system env var: `sails_custom__baseUrl`) 78 | //-------------------------------------------------------------------------- 79 | 80 | internalEmailAddress: 'support+staging@example.com', 81 | //-------------------------------------------------------------------------- 82 | // /\ Hard-code the email address that should receive support/contact form 83 | // || messages in staging (or use `sails_custom__internalEmailAddress`) 84 | //-------------------------------------------------------------------------- 85 | 86 | // sendgridSecret: 'SG.fake.3e0Bn0qSQVnwb1E4qNPz9JZP5vLZYqjh7sn8S93oSHU', 87 | // stripeSecret: 'sk_sandbox__fake_Nfgh82401348jaDa3lkZ0d9Hm', 88 | // stripePublishableKey: 'pk_sandbox__fake_fKd3mZJs1mlYrzWt7JQtkcRb', 89 | //-------------------------------------------------------------------------- 90 | // /\ Hard-code credentials to use in staging for other 3rd party APIs, etc. 91 | // || (or use system environment variables prefixed with "sails_custom__") 92 | //-------------------------------------------------------------------------- 93 | 94 | }) 95 | 96 | }); 97 | -------------------------------------------------------------------------------- /api/controllers/account/update-profile.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 4 | friendlyName: 'Update profile', 5 | 6 | 7 | description: 'Update the profile for the logged-in user.', 8 | 9 | 10 | inputs: { 11 | 12 | fullName: { 13 | type: 'string' 14 | }, 15 | 16 | emailAddress: { 17 | type: 'string' 18 | }, 19 | 20 | }, 21 | 22 | 23 | exits: { 24 | 25 | emailAlreadyInUse: { 26 | statusCode: 409, 27 | description: 'The provided email address is already in use.', 28 | }, 29 | 30 | }, 31 | 32 | 33 | fn: async function (inputs) { 34 | 35 | var newEmailAddress = inputs.emailAddress; 36 | if (newEmailAddress !== undefined) { 37 | newEmailAddress = newEmailAddress.toLowerCase(); 38 | } 39 | 40 | // Determine if this request wants to change the current user's email address, 41 | // revert her pending email address change, modify her pending email address 42 | // change, or if the email address won't be affected at all. 43 | var desiredEmailEffect;// ('change-immediately', 'begin-change', 'cancel-pending-change', 'modify-pending-change', or '') 44 | if ( 45 | newEmailAddress === undefined || 46 | (this.req.me.emailStatus !== 'change-requested' && newEmailAddress === this.req.me.emailAddress) || 47 | (this.req.me.emailStatus === 'change-requested' && newEmailAddress === this.req.me.emailChangeCandidate) 48 | ) { 49 | desiredEmailEffect = ''; 50 | } else if (this.req.me.emailStatus === 'change-requested' && newEmailAddress === this.req.me.emailAddress) { 51 | desiredEmailEffect = 'cancel-pending-change'; 52 | } else if (this.req.me.emailStatus === 'change-requested' && newEmailAddress !== this.req.me.emailAddress) { 53 | desiredEmailEffect = 'modify-pending-change'; 54 | } else if (!sails.config.custom.verifyEmailAddresses || this.req.me.emailStatus === 'unconfirmed') { 55 | desiredEmailEffect = 'change-immediately'; 56 | } else { 57 | desiredEmailEffect = 'begin-change'; 58 | } 59 | 60 | 61 | // If the email address is changing, make sure it is not already being used. 62 | if (_.contains(['begin-change', 'change-immediately', 'modify-pending-change'], desiredEmailEffect)) { 63 | let conflictingUser = await User.findOne({ 64 | or: [ 65 | { emailAddress: newEmailAddress }, 66 | { emailChangeCandidate: newEmailAddress } 67 | ] 68 | }); 69 | if (conflictingUser) { 70 | throw 'emailAlreadyInUse'; 71 | } 72 | } 73 | 74 | 75 | // Start building the values to set in the db. 76 | // (We always set the fullName if provided.) 77 | var valuesToSet = { 78 | fullName: inputs.fullName, 79 | }; 80 | 81 | switch (desiredEmailEffect) { 82 | 83 | // Change now 84 | case 'change-immediately': 85 | _.extend(valuesToSet, { 86 | emailAddress: newEmailAddress, 87 | emailChangeCandidate: '', 88 | emailProofToken: '', 89 | emailProofTokenExpiresAt: 0, 90 | emailStatus: this.req.me.emailStatus === 'unconfirmed' ? 'unconfirmed' : 'confirmed' 91 | }); 92 | break; 93 | 94 | // Begin new email change, or modify a pending email change 95 | case 'begin-change': 96 | case 'modify-pending-change': 97 | _.extend(valuesToSet, { 98 | emailChangeCandidate: newEmailAddress, 99 | emailProofToken: await sails.helpers.strings.random('url-friendly'), 100 | emailProofTokenExpiresAt: Date.now() + sails.config.custom.emailProofTokenTTL, 101 | emailStatus: 'change-requested' 102 | }); 103 | break; 104 | 105 | // Cancel pending email change 106 | case 'cancel-pending-change': 107 | _.extend(valuesToSet, { 108 | emailChangeCandidate: '', 109 | emailProofToken: '', 110 | emailProofTokenExpiresAt: 0, 111 | emailStatus: 'confirmed' 112 | }); 113 | break; 114 | 115 | // Otherwise, do nothing re: email 116 | } 117 | 118 | // Save to the db 119 | await User.updateOne({id: this.req.me.id }) 120 | .set(valuesToSet); 121 | 122 | 123 | // If an email address change was requested, and re-confirmation is required, 124 | // send the "confirm account" email. 125 | if (desiredEmailEffect === 'begin-change' || desiredEmailEffect === 'modify-pending-change') { 126 | await sails.helpers.sendTemplateEmail.with({ 127 | to: newEmailAddress, 128 | subject: 'Your account has been updated', 129 | template: 'email-verify-new-email', 130 | templateData: { 131 | fullName: inputs.fullName||this.req.me.fullName, 132 | token: valuesToSet.emailProofToken 133 | } 134 | }); 135 | } 136 | 137 | } 138 | 139 | 140 | }; 141 | -------------------------------------------------------------------------------- /scripts/rebuild-cloud-sdk.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | friendlyName: 'Rebuild Cloud SDK', 4 | 5 | 6 | description: 'Regenerate the configuration for the "Cloud SDK" -- the JavaScript module used for AJAX and WebSockets.', 7 | 8 | 9 | fn: async function(){ 10 | 11 | var path = require('path'); 12 | 13 | var endpointsByMethodName = {}; 14 | var extraEndpointsOnlyForTestsByMethodName = {}; 15 | 16 | for (let address in sails.config.routes) { 17 | let target = sails.config.routes[address]; 18 | 19 | // If the route target is an array, then only consider 20 | // the very last sub-target in the array. 21 | if (_.isArray(target)) { 22 | target = _.last(target); 23 | }//fi 24 | 25 | // Skip redirects 26 | // (Note that, by doing this, we also skip traditional shorthand 27 | // -- that's ok though.) 28 | if (_.isString(target)) { 29 | continue; 30 | } 31 | 32 | // Skip routes whose target doesn't contain `action` for any 33 | // other miscellaneous reason. 34 | if (!target.action) { 35 | continue; 36 | } 37 | 38 | // Just about everything else gets a Cloud SDK method. 39 | 40 | // We determine its name using the bare action name. 41 | var bareActionName = _.last(target.action.split(/\//)); 42 | var methodName = _.camelCase(bareActionName); 43 | var expandedAddress = sails.getRouteFor(target); 44 | 45 | // Skip routes that just serve views. 46 | // (but still generate them for use in tests, for convenience) 47 | if (target.view || (bareActionName.match(/^view-/))) { 48 | extraEndpointsOnlyForTestsByMethodName[methodName] = { 49 | verb: (expandedAddress.method||'get').toUpperCase(), 50 | url: expandedAddress.url 51 | }; 52 | continue; 53 | }//• 54 | 55 | endpointsByMethodName[methodName] = { 56 | verb: (expandedAddress.method||'get').toUpperCase(), 57 | url: expandedAddress.url, 58 | }; 59 | 60 | // If this is an actions2 action, then determine appropriate serial usage. 61 | // (deduced the same way as helpers) 62 | // > If there is no such action for some reason, then don't compile a 63 | // > method for this one. 64 | var requestable = sails.getActions()[target.action]; 65 | if (!requestable) { 66 | sails.log.warn('Skipping unrecognized action: `'+target.action+'`'); 67 | continue; 68 | } 69 | var def = requestable.toJSON && requestable.toJSON(); 70 | if (def && def.fn) { 71 | if (def.args !== undefined) { 72 | endpointsByMethodName[methodName].args = def.args; 73 | } else { 74 | endpointsByMethodName[methodName].args = _.reduce(def.inputs, (args, inputDef, inputCodeName)=>{ 75 | args.push(inputCodeName); 76 | return args; 77 | }, []); 78 | } 79 | } 80 | 81 | // And we determine whether it needs to communicate over WebSockets 82 | // by checking for an additional property in the route target. 83 | if (target.isSocket) { 84 | endpointsByMethodName[methodName].protocol = 'io.socket'; 85 | } 86 | }//∞ 87 | 88 | // Smash and rewrite the `cloud.setup.js` file in the assets folder to 89 | // reflect the latest set of available cloud actions exposed by this Sails 90 | // app (as determined by its routes above) 91 | await sails.helpers.fs.write.with({ 92 | destination: path.resolve(sails.config.appPath, 'assets/js/cloud.setup.js'), 93 | force: true, 94 | string: ``+ 95 | `/** 96 | * cloud.setup.js 97 | * 98 | * Configuration for this Sails app's generated browser SDK ("Cloud"). 99 | * 100 | * Above all, the purpose of this file is to provide endpoint definitions, 101 | * each of which corresponds with one particular route+action on the server. 102 | * 103 | * > This file was automatically generated. 104 | * > (To regenerate, run \`sails run rebuild-cloud-sdk\`) 105 | */ 106 | 107 | Cloud.setup({ 108 | 109 | /* eslint-disable */ 110 | methods: ${JSON.stringify(endpointsByMethodName)} 111 | /* eslint-enable */ 112 | 113 | });`+ 114 | `\n` 115 | }); 116 | 117 | // Also, if a `test/` folder exists, set up a barebones bounce of this data 118 | // as a JSON file inside of it, for testing purposes: 119 | var hasTestFolder = await sails.helpers.fs.exists(path.resolve(sails.config.appPath, 'test/')); 120 | if (hasTestFolder) { 121 | await sails.helpers.fs.write.with({ 122 | destination: path.resolve(sails.config.appPath, 'test/private/CLOUD_SDK_METHODS.json'), 123 | string: JSON.stringify(_.extend(endpointsByMethodName, extraEndpointsOnlyForTestsByMethodName)), 124 | force: true 125 | }); 126 | } 127 | 128 | sails.log.info('--'); 129 | sails.log.info('Successfully rebuilt Cloud SDK for use in the browser.'); 130 | sails.log.info('(and CLOUD_SDK_METHODS.json for use in automated tests)'); 131 | 132 | } 133 | 134 | }; 135 | -------------------------------------------------------------------------------- /assets/js/components/js-timestamp.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ----------------------------------------------------------------------------- 4 | * A human-readable, self-updating "timeago" timestamp, with some special rules: 5 | * 6 | * • Within 24 hours, displays in "timeago" format. 7 | * • Within a month, displays month, day, and time of day. 8 | * • Within a year, displays just the month and day. 9 | * • Older/newer than that, displays the month and day with the full year. 10 | * 11 | * @type {Component} 12 | * ----------------------------------------------------------------------------- 13 | */ 14 | 15 | parasails.registerComponent('jsTimestamp', { 16 | 17 | // ╔═╗╦═╗╔═╗╔═╗╔═╗ 18 | // ╠═╝╠╦╝║ ║╠═╝╚═╗ 19 | // ╩ ╩╚═╚═╝╩ ╚═╝ 20 | props: [ 21 | 'at',// « The JS timestamp to format 22 | 'short',// « Whether to shorten the formatted date by not including the time of day (may only be used with timeago, and even then only applicable in certain situations) 23 | 'format',// « one of: 'calendar', 'timeago' (defaults to 'timeago'. Otherwise, the "calendar" format displays data as US-style calendar dates with a four-character year, separated by dashes. In other words: "MM-DD-YYYY") 24 | ], 25 | 26 | // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ 27 | // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ 28 | // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ 29 | data: function (){ 30 | return { 31 | formatType: undefined, 32 | formattedTimestamp: '', 33 | interval: undefined 34 | }; 35 | }, 36 | 37 | // ╦ ╦╔╦╗╔╦╗╦ 38 | // ╠═╣ ║ ║║║║ 39 | // ╩ ╩ ╩ ╩ ╩╩═╝ 40 | template: ` 41 | {{formattedTimestamp}} 42 | `, 43 | 44 | // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ 45 | // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ 46 | // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ 47 | beforeMount: function() { 48 | if (this.at === undefined) { 49 | throw new Error('Incomplete usage of : Please specify `at` as a JS timestamp (i.e. epoch ms, a number). For example: ``'); 50 | } 51 | if(this.format === undefined) { 52 | this.formatType = 'timeago'; 53 | } else { 54 | if(!_.contains(['calendar', 'timeago'], this.format)) { throw new Error('Unsupported `format` ('+this.format+') passed in to the JS timestamp component! Must be either \'calendar\' or \'timeago\'.'); } 55 | this.formatType = this.format; 56 | } 57 | 58 | // If timeago timestamp, update the timestamp every minute. 59 | if(this.formatType === 'timeago') { 60 | this._formatTimeago(); 61 | this.interval = setInterval(async()=>{ 62 | try { 63 | this._formatTimeago(); 64 | await this.forceRender(); 65 | } catch (err) { 66 | err.raw = err; 67 | throw new Error('Encountered unexpected error while attempting to automatically re-render in the background, as the seconds tick by. '+err.message); 68 | } 69 | },60*1000);//œ 70 | } 71 | 72 | // If calendar timestamp, just set it the once. 73 | // (Also don't allow usage with `short`.) 74 | if(this.formatType === 'calendar') { 75 | this.formattedTimestamp = moment(this.at).format('MM-DD-YYYY'); 76 | if (this.short) { 77 | throw new Error('Invalid usage of : Cannot use `short="true"` at the same time as `format="calendar"`.'); 78 | } 79 | } 80 | }, 81 | 82 | beforeDestroy: function() { 83 | if(this.formatType === 'timeago') { 84 | clearInterval(this.interval); 85 | } 86 | }, 87 | 88 | watch: { 89 | at: function() { 90 | // Render to account for after-mount programmatic changes to `at`. 91 | if(this.formatType === 'timeago') { 92 | this._formatTimeago(); 93 | } else if(this.formatType === 'calendar') { 94 | this.formattedTimestamp = moment(this.at).format('MM-DD-YYYY'); 95 | } else { 96 | throw new Error(); 97 | } 98 | } 99 | }, 100 | 101 | 102 | // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ 103 | // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ 104 | // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ 105 | methods: { 106 | 107 | _formatTimeago: function() { 108 | var now = new Date().getTime(); 109 | var timeDifference = Math.abs(now - this.at); 110 | 111 | // If the timestamp is less than a day old, format as time ago. 112 | if(timeDifference < 1000*60*60*24) { 113 | this.formattedTimestamp = moment(this.at).fromNow(); 114 | } else { 115 | // If the timestamp is less than a month-ish old, we'll include the 116 | // time of day in the formatted timestamp. 117 | let includeTime = !this.short && timeDifference < 1000*60*60*24*31; 118 | 119 | // If the timestamp is from a different year, we'll include the year 120 | // in the formatted timestamp. 121 | let includeYear = moment(now).format('YYYY') !== moment(this.at).format('YYYY'); 122 | 123 | this.formattedTimestamp = moment(this.at).format('MMMM DD'+(includeYear ? ', YYYY' : '')+(includeTime ? ' [at] h:mma' : '')); 124 | } 125 | 126 | } 127 | 128 | } 129 | 130 | }); 131 | -------------------------------------------------------------------------------- /crontab/fetchResponse.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // Cron fetch function 4 | run: async function (period) { 5 | const fs = require('fs'); 6 | 7 | let targets; 8 | let setting; 9 | 10 | try { 11 | targets = await Target.find({ fetchEvery: period }); 12 | setting = await Setting.find(); 13 | if (setting.length === 0) { 14 | sails.log.error('Settings not found.'); 15 | return; 16 | } 17 | setting = setting[0]; 18 | } catch (error) { 19 | sails.log.error('Error fetching targets or settings from the database:', error); 20 | return; 21 | } 22 | 23 | if (targets.length === 0) { 24 | sails.log('No targets found for the given period.'); 25 | return; 26 | } 27 | 28 | targets.forEach(async (target) => { 29 | try { 30 | // Send new request 31 | let response = await sails.helpers.sendRequest(target.link, target.cookie); 32 | 33 | // Check if the response contains a keyword from the DB 34 | if (target.keywords !== '') { 35 | let keywords = target.keywords.split(','); 36 | 37 | for (const keyword of keywords) { 38 | if (response.indexOf(keyword) > -1) { 39 | try { 40 | await Target.updateOne({ id: target.id }).set({ 41 | status: 'changed' 42 | }); 43 | } catch (error) { 44 | sails.log.error(`Error updating target status for keyword match (Target ID: ${target.id}):`, error); 45 | } 46 | } 47 | } 48 | } 49 | 50 | // Get the last response file 51 | let responseFile = 'responses/' + target.id + '.txt'; 52 | 53 | let lastResponseBody; 54 | try { 55 | lastResponseBody = fs.readFileSync(responseFile, 'utf8'); 56 | } catch (error) { 57 | sails.log.error(`Error reading last response file for target ID ${target.id}:`, error); 58 | return; 59 | } 60 | 61 | // Find number of differences between response and lastResponseBody file 62 | let acceptedChange; 63 | try { 64 | acceptedChange = await sails.helpers.diffCheck(response, lastResponseBody); 65 | } catch (error) { 66 | sails.log.error(`Error performing diff check for target ID ${target.id}:`, error); 67 | return; 68 | } 69 | 70 | // Check if the new acceptedChange is higher than the user input 71 | if (acceptedChange > target.acceptedChange) { 72 | 73 | // Get diff highlight text 74 | let diff; 75 | try { 76 | diff = await sails.helpers.diffHighlight(lastResponseBody, response); 77 | } catch (error) { 78 | sails.log.error(`Error generating diff highlight for target ID ${target.id}:`, error); 79 | return; 80 | } 81 | 82 | // Create file for the diffHighlight text 83 | let diffFile = 'responses/diffs/' + target.id + '.txt'; 84 | 85 | // Save diffHighlight into file 86 | try { 87 | fs.writeFileSync(diffFile, diff); 88 | console.log('Diff file saved successfully.'); 89 | } catch (error) { 90 | sails.log.error(`Error saving diff file for target ID ${target.id}:`, error); 91 | return; 92 | } 93 | 94 | // Check if user wants to be reported via email 95 | if (setting.reportToEmail) { 96 | try { 97 | await sails.helpers.sendEmail(target.link); 98 | } catch (error) { 99 | sails.log.error(`Error sending email notification for target ID ${target.id}:`, error); 100 | } 101 | } 102 | 103 | // Check if user wants to be reported via telegram 104 | if (setting.reportToTelegram) { 105 | try { 106 | await sails.helpers.sendTelegram(target.link, acceptedChange); 107 | } catch (error) { 108 | sails.log.error(`Error sending Telegram notification for target ID ${target.id}:`, error); 109 | } 110 | } 111 | 112 | // Update database 113 | try { 114 | var updatedTarget = await Target.updateOne({ id: target.id }).set({ 115 | status: 'changed' 116 | }); 117 | 118 | if (updatedTarget) { 119 | sails.log(`Target ID ${target.id} status updated to 'changed'.`); 120 | } 121 | } catch (error) { 122 | sails.log.error(`Error updating target in database for target ID ${target.id}:`, error); 123 | return; 124 | } 125 | 126 | // Update response file with the new content 127 | try { 128 | fs.writeFileSync(responseFile, response); 129 | console.log('Response file is up-to-date.'); 130 | } catch (error) { 131 | sails.log.error(`Error updating response file for target ID ${target.id}:`, error); 132 | } 133 | } 134 | 135 | } catch (error) { 136 | sails.log.error(`Error processing target ID ${target.id}:`, error); 137 | } 138 | }); 139 | 140 | } 141 | }; 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "url-tracker", 3 | "private": true, 4 | "version": "0.5.0", 5 | "description": "URL Tracker is a robust and scalable web application designed to monitor and track URLs, log changes, and provide detailed insights into the status and behavior of various web resources. Built on top of Sails.js, this application is perfect for tracking URLs for uptime, content changes, and performance monitoring.", 6 | "keywords": [], 7 | "dependencies": { 8 | "@sailshq/connect-redis": "^3.2.1", 9 | "@sailshq/lodash": "^3.10.6", 10 | "@sailshq/socket.io-redis": "^5.2.1", 11 | "agenda": "^5.0.0", 12 | "bull": "^4.16.0", 13 | "crypto": "^1.0.1", 14 | "dotenv": "^16.4.5", 15 | "leven": "^3.1.0", 16 | "node-schedule": "^1.3.3", 17 | "node-telegram-bot-api": "^0.51.0", 18 | "nodemailer": "^6.9.14", 19 | "request": "^2.88.2", 20 | "sails": "^1.5.12", 21 | "sails-hook-apianalytics": "^2.0.6", 22 | "sails-hook-organics": "^2.2.2", 23 | "sails-hook-orm": "^2.1.1", 24 | "sails-hook-sockets": "^2.0.4", 25 | "sails-mongo": "^2.1.1" 26 | }, 27 | "devDependencies": { 28 | "eslint": "5.16.0", 29 | "grunt": "1.0.4", 30 | "htmlhint": "0.11.0", 31 | "lesshint": "6.3.6", 32 | "sails-hook-grunt": "^4.0.1" 33 | }, 34 | "scripts": { 35 | "start": "NODE_ENV=production node app.js", 36 | "test": "npm run lint && npm run custom-tests && echo 'Done.'", 37 | "lint": "./node_modules/eslint/bin/eslint.js . --fix --max-warnings=0 --report-unused-disable-directives && echo '✔ Your .js files look so good.' && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/**/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/**/**/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/**/**/**/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/**/**/**/**/**/*.ejs && echo '✔ So do your .ejs files.' && ./node_modules/lesshint/bin/lesshint assets/styles/ --max-warnings=0 && echo '✔ Your .less files look good, too.'", 38 | "custom-tests": "echo \"(No other custom tests yet.)\" && echo", 39 | "deploy": "echo 'This script assumes a dead-simple, opinionated setup on Heroku.' && echo 'But, of course, you can deploy your app anywhere you like.' && echo '(Node.js/Sails.js apps are supported on all modern hosting platforms.)' && echo && echo 'Warning: Specifically, this script assumes you are on the master branch, and that your app can be deployed simply by force-pushing on top of the *deploy* branch. It will also temporarily use a local *predeploy* branch for preparing assets, that it will delete after it finishes. Please make sure there is nothing you care about on either of these two branches!!!' && echo '' && echo '' && echo 'Preparing to deploy...' && echo '--' && git status && echo '' && echo '--' && echo 'I hope you are on the master branch and have everything committed/pulled/pushed and are completely up to date and stuff.' && echo '********************************************' && echo '** IF NOT THEN PLEASE PRESS NOW! **' && echo '********************************************' && echo 'Press CTRL+C to cancel.' && echo '(you have five seconds)' && sleep 1 && echo '...4' && sleep 1 && echo '...3' && sleep 1 && echo '...2' && sleep 1 && echo '...1' && sleep 1 && echo '' && echo 'Alright, here we go. No turning back now!' && echo 'Trying to switch to master branch...' && git checkout master && echo && echo 'OK. Now wiping node_modules/ and running npm install...' && rm -rf node_modules && rm -rf package-lock.json && npm install && (git add package-lock.json && git commit -am 'AUTOMATED COMMIT: Did fresh npm install before deploying, and it caused something relevant (probably the package-lock.json file) to change! This commit tracks that change.' || true) && echo 'Deploying as version:' && npm version patch && echo '' && git push origin master && git push --tags && (git branch -D predeploy > /dev/null 2>&1 || true) && git checkout -b predeploy && (echo 'Now building+minifying assets for production...' && echo '(Hang tight, this could take a while.)' && echo && node node_modules/grunt/bin/grunt buildProd || (echo && echo '------------------------------------------' && echo 'IMPORTANT! IMPORTANT! IMPORTANT!' && echo 'ERROR: Could not compile assets for production!' && echo && echo 'Attempting to recover automatically by stashing, ' && echo 'switching back to the master branch, and then ' && echo 'deleting the predeploy branch... ' && echo && echo 'After this, please fix the issues logged above' && echo 'and push that up. Then, try deploying again.' && echo '------------------------------------------' && echo && echo 'Staging, deleting the predeploy branch, and switching back to master...' && git stash && git checkout master && git branch -D predeploy && false)) && mv www .www && git add .www && node -e 'sailsrc = JSON.parse(require(\"fs\").readFileSync(\"./.sailsrc\", \"utf8\")); if (sailsrc.paths&&sailsrc.paths.public !== undefined || sailsrc.hooks&&sailsrc.hooks.grunt !== undefined) { throw new Error(\"Cannot complete deployment script: .sailsrc file has conflicting contents! Please throw away this midway-complete deployment, switch back to your original branch (master), remove the conflicting stuff from .sailsrc, then commit and push that up.\"); } sailsrc.paths = sailsrc.paths || {}; sailsrc.paths.public = \"./.www\"; sailsrc.hooks = sailsrc.hooks || {}; sailsrc.hooks.grunt = false; require(\"fs\").writeFileSync(\"./.sailsrc\", JSON.stringify(sailsrc))' && git commit -am 'AUTOMATED COMMIT: Automatically bundling compiled assets as part of deploy, updating the EJS layout and .sailsrc file accordingly.' && git push origin predeploy && git checkout master && git push origin +predeploy:deploy && git push --tags && git branch -D predeploy && git push origin :predeploy && echo '' && echo '--' && echo 'OK, done. It should be live momentarily on your staging environment.' && echo '(if you get impatient, check the Heroku dashboard for status)' && echo && echo 'Staging environment:' && echo ' 🌐–• https://staging.example.com' && echo ' (hold ⌘ and click to open links in the terminal)' && echo && echo 'Please review that to make sure it looks good.' && echo 'When you are ready to go to production, visit your pipeline on Heroku and press the PROMOTE TO PRODUCTION button.'" 40 | }, 41 | "main": "app.js", 42 | "repository": { 43 | "type": "git", 44 | "url": "git://github.com/ahussam/url-tracker.git" 45 | }, 46 | "author": "Abdullah Al-Sultani", 47 | "license": "MIT", 48 | "engines": { 49 | "node": "^10.15" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/custom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom configuration 3 | * (sails.config.custom) 4 | * 5 | * One-off settings specific to your application. 6 | * 7 | * For more information on custom configuration, visit: 8 | * https://sailsjs.com/config/custom 9 | */ 10 | 11 | module.exports.custom = { 12 | 13 | /************************************************************************** 14 | * * 15 | * The base URL to use during development. * 16 | * * 17 | * • No trailing slash at the end * 18 | * • `http://` or `https://` at the beginning. * 19 | * * 20 | * > This is for use in custom logic that builds URLs. * 21 | * > It is particularly handy for building dynamic links in emails, * 22 | * > but it can also be used for user-uploaded images, webhooks, etc. * 23 | * * 24 | **************************************************************************/ 25 | baseUrl: 'http://localhost:1337', 26 | 27 | /************************************************************************** 28 | * * 29 | * Display names/dates for your app * 30 | * * 31 | * > These are here to make it easier to change out the placeholder * 32 | * > platform name, company name, etc. that are displayed all over the * 33 | * > app when it's first generated. * 34 | * * 35 | **************************************************************************/ 36 | platformCopyrightYear: '2024', 37 | platformName: 'URL Tracker', 38 | platformCompanyName: 'Abdullah Al-Sultani', 39 | platformCompanyAboutHref: 'https://github.com/ahussam/url-tracker', 40 | privacyPolicyUpdatedAt: 'DATE_PRIVACY_POLICY_LAST_UPDATED', 41 | termsOfServiceUpdatedAt: 'DATE_TERMS_OF_SERVICE_LAST_UPDATED', 42 | 43 | /************************************************************************** 44 | * * 45 | * The TTL (time-to-live) for various sorts of tokens before they expire. * 46 | * * 47 | **************************************************************************/ 48 | passwordResetTokenTTL: 24*60*60*1000,// 24 hours 49 | emailProofTokenTTL: 24*60*60*1000,// 24 hours 50 | 51 | /************************************************************************** 52 | * * 53 | * The extended length that browsers should retain the session cookie * 54 | * if "Remember Me" was checked while logging in. * 55 | * * 56 | **************************************************************************/ 57 | rememberMeCookieMaxAge: 30*24*60*60*1000, // 30 days 58 | 59 | /************************************************************************** 60 | * * 61 | * Automated email configuration * 62 | * * 63 | * Sandbox Sendgrid credentials for use during development, as well as any * 64 | * other default settings related to "how" and "where" automated emails * 65 | * are sent. * 66 | * * 67 | * (https://app.sendgrid.com/settings/api_keys) * 68 | * * 69 | **************************************************************************/ 70 | // sendgridSecret: 'SG.fake.3e0Bn0qSQVnwb1E4qNPz9JZP5vLZYqjh7sn8S93oSHU', 71 | //-------------------------------------------------------------------------- 72 | // /\ Configure this to enable support for automated emails. 73 | // || (Important for password recovery, verification, contact form, etc.) 74 | //-------------------------------------------------------------------------- 75 | 76 | // The sender that all outgoing emails will appear to come from. 77 | fromEmailAddress: 'noreply@example.com', 78 | fromName: 'The NEW_APP_NAME Team', 79 | 80 | // Email address for receiving support messages & other correspondences. 81 | // > If you're using the default privacy policy, this will be referenced 82 | // > as the contact email of your "data protection officer" for the purpose 83 | // > of compliance with regulations such as GDPR. 84 | internalEmailAddress: 'support+development@example.com', 85 | 86 | // Whether to require proof of email address ownership any time a new user 87 | // signs up, or when an existing user attempts to change their email address. 88 | verifyEmailAddresses: false, 89 | 90 | /************************************************************************** 91 | * * 92 | * Billing & payments configuration * 93 | * * 94 | * (https://dashboard.stripe.com/account/apikeys) * 95 | * * 96 | **************************************************************************/ 97 | // stripePublishableKey: 'pk_test_Zzd814nldl91104qor5911gjald', 98 | // stripeSecret: 'sk_test_Zzd814nldl91104qor5911gjald', 99 | //-------------------------------------------------------------------------- 100 | // /\ Configure these to enable support for billing features. 101 | // || (Or if you don't need billing, feel free to remove them.) 102 | //-------------------------------------------------------------------------- 103 | 104 | /*************************************************************************** 105 | * * 106 | * Any other custom config this Sails app should use during development. * 107 | * * 108 | ***************************************************************************/ 109 | // … 110 | 111 | }; 112 | -------------------------------------------------------------------------------- /tasks/config/sails-linker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `tasks/config/sails-linker` 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Automatically inject ', 30 | appRoot: '.tmp/public' 31 | }, 32 | files: { 33 | '.tmp/public/**/*.html': require('../pipeline').jsFilesToInject, 34 | 'views/**/*.html': require('../pipeline').jsFilesToInject, 35 | 'views/**/*.ejs': require('../pipeline').jsFilesToInject 36 | } 37 | }, 38 | 39 | devJsBuild: { 40 | options: { 41 | startTag: '', 42 | endTag: '', 43 | fileTmpl: '', 44 | appRoot: '.tmp/public', 45 | // relative: true 46 | // ^^ Uncomment this if compiling assets for use in PhoneGap, CDN, etc. 47 | // (but be note that this can break custom font URLs) 48 | }, 49 | files: { 50 | '.tmp/public/**/*.html': require('../pipeline').jsFilesToInject, 51 | 'views/**/*.html': require('../pipeline').jsFilesToInject, 52 | 'views/**/*.ejs': require('../pipeline').jsFilesToInject 53 | } 54 | }, 55 | 56 | prodJs: { 57 | options: { 58 | startTag: '', 59 | endTag: '', 60 | fileTmpl: '', 61 | appRoot: '.tmp/public' 62 | }, 63 | files: { 64 | '.tmp/public/**/*.html': ['.tmp/public/min/production.min.js'], 65 | 'views/**/*.html': ['.tmp/public/min/production.min.js'], 66 | 'views/**/*.ejs': ['.tmp/public/min/production.min.js'] 67 | } 68 | }, 69 | 70 | prodJsBuild: { 71 | options: { 72 | startTag: '', 73 | endTag: '', 74 | fileTmpl: '', 75 | appRoot: '.tmp/public', 76 | // relative: true 77 | // ^^ Uncomment this if compiling assets for use in PhoneGap, CDN, etc. 78 | // (but be note that this can break custom font URLs) 79 | }, 80 | files: { 81 | '.tmp/public/**/*.html': ['.tmp/public/dist/*.js'], 82 | 'views/**/*.html': ['.tmp/public/dist/*.js'], 83 | 'views/**/*.ejs': ['.tmp/public/dist/*.js'] 84 | } 85 | }, 86 | 87 | 88 | // ╔═╗╔╦╗╦ ╦╦ ╔═╗╔═╗╦ ╦╔═╗╔═╗╔╦╗╔═╗ 89 | // ╚═╗ ║ ╚╦╝║ ║╣ ╚═╗╠═╣║╣ ║╣ ║ ╚═╗ 90 | // ╚═╝ ╩ ╩ ╩═╝╚═╝╚═╝╩ ╩╚═╝╚═╝ ╩ ╚═╝ 91 | // ┌─ ┬┌┐┌┌─┐┬ ┬ ┬┌┬┐┬┌┐┌┌─┐ ╔═╗╔═╗╔═╗ ┬ ┌─┐┌─┐┌┬┐┌─┐┬┬ ┌─┐┌┬┐ ╦ ╔═╗╔═╗╔═╗ ─┐ 92 | // │─── │││││ │ │ │ │││││││ ┬ ║ ╚═╗╚═╗ ┌┼─ │ │ ││││├─┘││ ├┤ ││ ║ ║╣ ╚═╗╚═╗ ───│ 93 | // └─ ┴┘└┘└─┘┴─┘└─┘─┴┘┴┘└┘└─┘ ╚═╝╚═╝╚═╝ └┘ └─┘└─┘┴ ┴┴ ┴┴─┘└─┘─┴┘ ╩═╝╚═╝╚═╝╚═╝ ─┘ 94 | devStyles: { 95 | options: { 96 | startTag: '', 97 | endTag: '', 98 | fileTmpl: '', 99 | appRoot: '.tmp/public' 100 | }, 101 | 102 | files: { 103 | '.tmp/public/**/*.html': require('../pipeline').cssFilesToInject, 104 | 'views/**/*.html': require('../pipeline').cssFilesToInject, 105 | 'views/**/*.ejs': require('../pipeline').cssFilesToInject 106 | } 107 | }, 108 | 109 | devStylesBuild: { 110 | options: { 111 | startTag: '', 112 | endTag: '', 113 | fileTmpl: '', 114 | appRoot: '.tmp/public', 115 | // relative: true 116 | // ^^ Uncomment this if compiling assets for use in PhoneGap, CDN, etc. 117 | // (but be note that this can break custom font URLs) 118 | }, 119 | 120 | files: { 121 | '.tmp/public/**/*.html': require('../pipeline').cssFilesToInject, 122 | 'views/**/*.html': require('../pipeline').cssFilesToInject, 123 | 'views/**/*.ejs': require('../pipeline').cssFilesToInject 124 | } 125 | }, 126 | 127 | prodStyles: { 128 | options: { 129 | startTag: '', 130 | endTag: '', 131 | fileTmpl: '', 132 | appRoot: '.tmp/public' 133 | }, 134 | files: { 135 | '.tmp/public/index.html': ['.tmp/public/min/production.min.css'], 136 | 'views/**/*.html': ['.tmp/public/min/production.min.css'], 137 | 'views/**/*.ejs': ['.tmp/public/min/production.min.css'] 138 | } 139 | }, 140 | 141 | prodStylesBuild: { 142 | options: { 143 | startTag: '', 144 | endTag: '', 145 | fileTmpl: '', 146 | appRoot: '.tmp/public', 147 | // relative: true 148 | // ^^ Uncomment this if compiling assets for use in PhoneGap, CDN, etc. 149 | // (but be note that this can break custom font URLs) 150 | }, 151 | files: { 152 | '.tmp/public/index.html': ['.tmp/public/dist/*.css'], 153 | 'views/**/*.html': ['.tmp/public/dist/*.css'], 154 | 'views/**/*.ejs': ['.tmp/public/dist/*.css'] 155 | } 156 | }, 157 | 158 | });// 159 | 160 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 161 | // This Grunt plugin is part of the default asset pipeline in Sails, 162 | // so it's already been automatically loaded for you at this point. 163 | // 164 | // Of course, you can always remove this Grunt plugin altogether by 165 | // deleting this file. But check this out: you can also use your 166 | // _own_ custom version of this Grunt plugin. 167 | // 168 | // Here's how: 169 | // 170 | // 1. Install it as a local dependency of your Sails app: 171 | // ``` 172 | // $ npm install grunt-sails-linker --save-dev --save-exact 173 | // ``` 174 | // 175 | // 176 | // 2. Then uncomment the following code: 177 | // 178 | // ``` 179 | // // Load Grunt plugin from the node_modules/ folder. 180 | // grunt.loadNpmTasks('grunt-sails-linker'); 181 | // ``` 182 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 183 | 184 | }; 185 | -------------------------------------------------------------------------------- /config/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Seed Function 3 | * (sails.config.bootstrap) 4 | * 5 | * A function that runs just before your Sails app gets lifted. 6 | * > Need more flexibility? You can also create a hook. 7 | * 8 | * For more information on seeding your app with fake data, check out: 9 | * https://sailsjs.com/config/bootstrap 10 | */ 11 | 12 | module.exports.bootstrap = async function() { 13 | 14 | // Import dependencies 15 | var path = require('path'); 16 | 17 | // This bootstrap version indicates what version of fake data we're dealing with here. 18 | var HARD_CODED_DATA_VERSION = 0; 19 | 20 | // This path indicates where to store/look for the JSON file that tracks the "last run bootstrap info" 21 | // locally on this development computer (if we happen to be on a development computer). 22 | var bootstrapLastRunInfoPath = path.resolve(sails.config.appPath, '.tmp/bootstrap-version.json'); 23 | 24 | // Whether or not to continue doing the stuff in this file (i.e. wiping and regenerating data) 25 | // depends on some factors: 26 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 27 | 28 | // If the hard-coded data version has been incremented, or we're being forced 29 | // (i.e. `--drop` or `--environment=test` was set), then run the meat of this 30 | // bootstrap script to wipe all existing data and rebuild hard-coded data. 31 | if (sails.config.models.migrate !== 'drop' && sails.config.environment !== 'test') { 32 | // If this is _actually_ a production environment (real or simulated), or we have 33 | // `migrate: safe` enabled, then prevent accidentally removing all data! 34 | if (process.env.NODE_ENV==='production' || sails.config.models.migrate === 'safe') { 35 | sails.log('Since we are running with migrate: \'safe\' and/or NODE_ENV=production (in the "'+sails.config.environment+'" Sails environment, to be precise), skipping the rest of the bootstrap to avoid data loss...'); 36 | return; 37 | }//• 38 | 39 | // Compare bootstrap version from code base to the version that was last run 40 | var lastRunBootstrapInfo = await sails.helpers.fs.readJson(bootstrapLastRunInfoPath) 41 | .tolerate('doesNotExist');// (it's ok if the file doesn't exist yet-- just keep going.) 42 | 43 | if (lastRunBootstrapInfo && lastRunBootstrapInfo.lastRunVersion === HARD_CODED_DATA_VERSION) { 44 | sails.log('Skipping v'+HARD_CODED_DATA_VERSION+' bootstrap script... (because it\'s already been run)'); 45 | sails.log('(last run on this computer: @ '+(new Date(lastRunBootstrapInfo.lastRunAt))+')'); 46 | return; 47 | }//• 48 | 49 | sails.log('Running v'+HARD_CODED_DATA_VERSION+' bootstrap script... ('+(lastRunBootstrapInfo ? 'before this, the last time the bootstrap ran on this computer was for v'+lastRunBootstrapInfo.lastRunVersion+' @ '+(new Date(lastRunBootstrapInfo.lastRunAt)) : 'looks like this is the first time the bootstrap has run on this computer')+')'); 50 | } 51 | else { 52 | sails.log('Running bootstrap script because it was forced... (either `--drop` or `--environment=test` was used)'); 53 | } 54 | 55 | // Since the hard-coded data version has been incremented, and we're running in 56 | // a "throwaway data" environment, delete all records from all models. 57 | for (let identity in sails.models) { 58 | await sails.models[identity].destroy({}); 59 | }//∞ 60 | 61 | // By convention, this is a good place to set up fake data during development. 62 | // await User.createEach([ 63 | // { emailAddress: 'admin@example.com', fullName: 'First user', isSuperAdmin: true, password: await sails.helpers.passwords.hashPassword('abc123') }, 64 | // ]); 65 | 66 | // await Setting.create({ 67 | // app: 'this', 68 | // reportToTelegram: false, 69 | // telegramToken: '', 70 | // telegramChatID: '', 71 | // }); 72 | 73 | 74 | // Save new bootstrap version 75 | await sails.helpers.fs.writeJson.with({ 76 | destination: bootstrapLastRunInfoPath, 77 | json: { 78 | lastRunVersion: HARD_CODED_DATA_VERSION, 79 | lastRunAt: Date.now() 80 | }, 81 | force: true 82 | }) 83 | .tolerate((err)=>{ 84 | sails.log.warn('For some reason, could not write bootstrap version .json file. This could be a result of a problem with your configured paths, or, if you are in production, a limitation of your hosting provider related to `pwd`. As a workaround, try updating app.js to explicitly pass in `appPath: __dirname` instead of relying on `chdir`. Current sails.config.appPath: `'+sails.config.appPath+'`. Full error details: '+err.stack+'\n\n(Proceeding anyway this time...)'); 85 | }); 86 | 87 | }; 88 | 89 | var Agenda = require('agenda'); 90 | var schedule = require('node-schedule'); 91 | require('dotenv').config(); 92 | 93 | module.exports.bootstrap = async function(done) { 94 | 95 | let usersCount = await User.find(); 96 | if(usersCount.length === 0){ 97 | await User.createEach([ 98 | { emailAddress: 'admin@example.com', fullName: 'First user', isSuperAdmin: true, password: await sails.helpers.passwords.hashPassword('9TMhdaUSEzksEXF') }, 99 | ]); 100 | await Setting.create({ 101 | app: 'this', 102 | reportToTelegram: false, 103 | telegramToken: '', 104 | telegramChatID: '', 105 | }); 106 | } 107 | 108 | _.extend(sails.hooks.http.app.locals, sails.config.http.locals); 109 | sails.config.crontab.crons().forEach((item) => { 110 | schedule.scheduleJob(item.interval,sails.config.crontab[item.method]); 111 | }); 112 | 113 | const mongoConnectionString = process.env.MONGO_URL; 114 | 115 | const agenda = new Agenda({ db: { address: mongoConnectionString } }); 116 | 117 | // Define a sample job (you can define more jobs as needed) 118 | agenda.define('process link', async (job) => { 119 | let { link, desc, fetchEvery, keywords, acceptedChange, cookie } = job.attrs.data; 120 | 121 | try { 122 | let response = await sails.helpers.sendRequest(link, cookie); 123 | 124 | if (!acceptedChange) { 125 | let response2 = await sails.helpers.sendRequest(link, cookie); 126 | acceptedChange = await sails.helpers.diffCheck(response, response2); 127 | } 128 | 129 | let newTarget = await Target.create({ 130 | description: desc, 131 | link: link, 132 | status: 'unchanged', 133 | acceptedChange: acceptedChange, 134 | fetchEvery: fetchEvery, 135 | keywords: keywords, 136 | cookie: cookie, 137 | }).fetch(); 138 | 139 | let responseFile = 'responses/' + newTarget.id + '.txt'; 140 | const fs = require('fs'); 141 | fs.writeFileSync(responseFile, response); 142 | 143 | sails.log(`Processed link ${link} and stored response.`); 144 | } catch (error) { 145 | sails.log.error(`Error processing link ${link}:`, error); 146 | } 147 | }); 148 | 149 | // Start the agenda processing 150 | await agenda.start(); 151 | 152 | sails.config.agenda = agenda; // Store agenda instance globally if needed 153 | 154 | return done(); 155 | }; 156 | --------------------------------------------------------------------------------