├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── doc ├── .dockerignore ├── .gitattributes ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── METRICS.md └── swagger-ui │ ├── css │ ├── print.css │ ├── reset.css │ ├── screen.css │ ├── style.css │ └── typography.css │ ├── fonts │ ├── DroidSans-Bold.ttf │ └── DroidSans.ttf │ ├── images │ ├── collapse.gif │ ├── expand.gif │ ├── explorer_icons.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-48x48.png │ ├── favicon.ico │ ├── keeper_api.svg │ ├── logo_small.png │ ├── pet_store_api.png │ ├── throbber.gif │ └── wordnik_api.png │ ├── index.html │ ├── lang │ ├── ca.js │ ├── el.js │ ├── en.js │ ├── es.js │ ├── fr.js │ ├── geo.js │ ├── it.js │ ├── ja.js │ ├── ko-kr.js │ ├── pl.js │ ├── pt.js │ ├── ru.js │ ├── tr.js │ ├── translator.js │ └── zh-cn.js │ ├── lib │ ├── backbone-min.js │ ├── es5-shim.js │ ├── handlebars-4.0.5.js │ ├── highlight.9.1.0.pack.js │ ├── highlight.9.1.0.pack_extended.js │ ├── jquery-1.8.0.min.js │ ├── jquery.ba-bbq.min.js │ ├── jquery.slideto.min.js │ ├── jquery.wiggle.min.js │ ├── js-yaml.min.js │ ├── jsoneditor.min.js │ ├── lodash.min.js │ ├── marked.js │ ├── object-assign-pollyfill.js │ ├── sanitize-html.min.js │ └── swagger-oauth.js │ ├── o2c.html │ └── swagger-ui.min.js ├── docker-compose.app.yml ├── docker-compose.yml ├── etc ├── default │ ├── dev.env │ └── staging.env └── systemd │ └── system │ ├── keeper-core-api.service │ ├── keeper-core-job-worker.service │ ├── keeper-data-backup.service │ └── keeper-data-backup.timer ├── features ├── admin.feature ├── api.feature ├── client.feature ├── document-attachments.feature ├── document-basics.feature ├── document-file.feature ├── document-html.feature ├── document-search.feature ├── document-url.feature ├── export.feature ├── graveyard.feature ├── label.feature ├── profile.feature ├── sharing.feature ├── step_definitions │ ├── admin_step_definition.js │ ├── api_step_definition.js │ ├── attachment_step_definition.js │ ├── client_step_definition.js │ ├── common_step_definition.js │ ├── document_step_definition.js │ ├── export_step_definition.js │ ├── graveyard_step_definition.js │ ├── label_step_definition.js │ ├── profile_step_definition.js │ ├── search_step_definition.js │ ├── sharing_step_definition.js │ └── webhook_step_definition.js ├── support │ └── world.js └── webhook.feature ├── gulpfile.js ├── makefiles ├── docker │ ├── cleanup.Makefile │ └── compose.Makefile └── help.Makefile ├── package.json ├── src ├── api │ ├── admin.api.js │ ├── attachment.api.js │ ├── client.api.js │ ├── document.api.js │ ├── export.api.js │ ├── graveyard.api.js │ ├── index.js │ ├── info.js │ ├── label-sharing.api.js │ ├── label.api.js │ ├── profile.api.js │ ├── public.api.js │ ├── sharing.api.js │ ├── swagger │ │ ├── definitions.yaml │ │ ├── parameters.yaml │ │ └── tags.yaml │ └── webhook.api.js ├── app.js ├── cluster.js ├── controller │ ├── admin.ctrl.js │ ├── attachment.ctrl.js │ ├── client.ctrl.js │ ├── document.ctrl.js │ ├── exports.ctrl.js │ ├── index.js │ ├── info.ctrl.js │ ├── label.ctrl.js │ ├── profile.ctrl.js │ ├── sharing.ctrl.js │ └── webhook.ctrl.js ├── dao │ ├── elasticsearch │ │ ├── client.dao.js │ │ ├── common │ │ │ ├── abstract.dao.js │ │ │ └── query-builder.js │ │ ├── document.dao.js │ │ ├── index.js │ │ ├── label.dao.js │ │ ├── sharing.dao.js │ │ ├── user.dao.js │ │ └── webhook.dao.js │ ├── index.js │ ├── mongodb │ │ ├── abstract │ │ │ └── index.js │ │ ├── client.dao.js │ │ ├── document.dao.js │ │ ├── index.js │ │ ├── label.dao.js │ │ ├── sharing.dao.js │ │ ├── user.dao.js │ │ └── webhook.dao.js │ └── searchengine.js ├── decorator │ ├── client.decorator.js │ ├── decorator.stream.js │ ├── document.decorator.js │ ├── index.js │ ├── label.decorator.js │ ├── profile.decorator.js │ ├── sharing.decorator.js │ ├── user.decorator.js │ └── webhook.decorator.js ├── event-broker │ ├── client.js │ └── http │ │ └── index.js ├── event │ ├── document │ │ ├── attachment.evt.js │ │ ├── indexation.evt.js │ │ ├── metrics.evt.js │ │ └── webhook.evt.js │ ├── index.js │ ├── label │ │ └── metrics.evt.js │ ├── sharing │ │ └── metrics.evt.js │ ├── user │ │ ├── broker.evt.js │ │ └── metrics.evt.js │ └── webhook │ │ └── metrics.evt.js ├── extractor │ ├── content.extractor.js │ ├── content │ │ ├── html.cleaner.js │ │ └── html.extractor.js │ ├── file.extractor.js │ ├── index.js │ ├── url.extractor.js │ └── url │ │ ├── bookmark.extractor.js │ │ ├── dailymotion.extractor.js │ │ ├── vimeo.extractor.js │ │ └── youtube.extractor.js ├── helper │ ├── errors.js │ ├── files.js │ ├── globals.js │ ├── hash.js │ ├── index.js │ ├── logger.js │ ├── openid-registration-client.js │ ├── redis.js │ ├── request.js │ ├── template-holder.js │ ├── text-list-holder.js │ ├── thumbnail.js │ ├── url-configuration.js │ └── validators.js ├── job │ ├── embedded.js │ ├── launcher.js │ ├── tasks │ │ ├── abstract-task.js │ │ ├── download.task.js │ │ ├── export-user.task.js │ │ ├── ghostbuster.task.js │ │ ├── import-user.task.js │ │ ├── index.js │ │ └── rebuild-index.task.js │ └── worker.js ├── metrics │ ├── client.js │ └── statsd │ │ └── index.js ├── middleware │ ├── admin.middleware.js │ ├── auth_apikey.middleware.js │ ├── auth_bypass.middleware.js │ ├── auth_jwt.middleware.js │ ├── client.middleware.js │ ├── cors.middleware.js │ ├── document.middleware.js │ ├── error.middleware.js │ ├── graveyard.middleware.js │ ├── index.js │ ├── label.middleware.js │ ├── logger.middleware.js │ ├── multipart.middleware.js │ ├── sharing.middleware.js │ ├── token.middleware.js │ └── webhook.middleware.js ├── service │ ├── attachment.service.js │ ├── client.service.js │ ├── document.service.js │ ├── download.service.js │ ├── index.js │ ├── job.service.js │ ├── label.service.js │ ├── monitoring.service.js │ ├── sharing.service.js │ ├── user.service.js │ └── webhook.service.js └── storage │ ├── index.js │ ├── local.storage.js │ └── s3.storage.js └── var ├── assets ├── gpl.html ├── gpl.txt ├── logo.png └── oss.png ├── cert ├── Makefile ├── staging-pub.pem ├── staging.crt ├── test-pub.pem └── test.pem ├── list └── blacklist.txt ├── scripts └── backup-data ├── stubs └── mappings │ ├── auth_realms_nunux-keeper_clients-registrations_default-02d47a9e-635f-40af-add6-62ce2006c9ee.json │ ├── auth_realms_nunux-keeper_clients-registrations_default_dee8f1f8-f936-4957-ad43-14ba06a7db62-446a8408-f762-4105-b7ae-f4bb1b24e41a.json │ └── auth_realms_nunux-keeper_clients-registrations_default_dee8f1f8-f936-4957-ad43-14ba06a7db62-bd386ab0-bfbf-4860-baec-b6573eb2e38c.json └── templates └── rss.tmpl /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules/ 3 | documentation/ 4 | var/exports 5 | var/cert/staging.pem 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | # vim:ft=yaml: 2 | 3 | # So parent files don't get applied 4 | root: true 5 | 6 | env: 7 | browser: false 8 | node: true 9 | 10 | extends: 11 | - standard 12 | 13 | rules: 14 | semi: [2, never] 15 | no-caller: 1 16 | key-spacing: [0, { align: colon}] 17 | 18 | globals: 19 | __DEV__ : false 20 | __PROD__ : false 21 | __DEBUG__ : false 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | documentation/ 4 | var/exports 5 | var/cert/staging.pem 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | sudo: required 3 | 4 | services: 5 | - docker 6 | 7 | env: 8 | - TARGET= 9 | - TARGET=with-elastic 10 | 11 | script: 12 | - make build deploy ${TARGET} test 13 | 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 2.0.0 5 | ----- 6 | 7 | ### Features 8 | * Authentication delegated to external identity provider (with JWT) 9 | * Create text or HTML documents (from scratch or from an URL) 10 | * Edit online documents 11 | * Search and visualize documents (also in raw mode) 12 | * Create labels (name, color) 13 | * Classify documents with labels 14 | * Remove and restore documents 15 | * Share documents with other users or publicly 16 | * Expose public documents with RSS 17 | * Import and Export documents of an user 18 | * Store attachments on disk or on S3 19 | * Index documents into ElasticSearch 20 | * Online API documentation 21 | * RESTFul API (with HATOAS support) 22 | * Produce metrics (with StatsD) 23 | * BDD testing (with Cucumber) 24 | 25 | ### Improvements 26 | * New internal structure 27 | * Internal event process (for decoupling and extendability) 28 | * Multi database support (MongoDB or ElasticSearch) 29 | * Complete separation with the frontend 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Nunux Keeper core API server. 2 | # 3 | # VERSION 2.0 4 | 5 | FROM node:6-onbuild 6 | 7 | MAINTAINER Nicolas Carlier 8 | 9 | # Create storage directory 10 | RUN bash -c 'mkdir -p /var/opt/app/storage/{upload,exports}' 11 | 12 | # Ports 13 | EXPOSE 3000 8080 14 | 15 | ENTRYPOINT ["/usr/local/bin/npm"] 16 | 17 | CMD ["start"] 18 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ### TODOs 2 | | Filename | line # | TODO 3 | |:------|:------:|:------ 4 | | controller/sharing.ctrl.js | 25 | Add items links 5 | | middleware/cors.middleware.js | 8 | Use whitelisted origin 6 | | service/document.service.js | 131 | check labels 7 | | storage/s3.storage.js | 57 | Implement S3 storage usage 8 | | storage/s3.storage.js | 196 | remove zero length files 9 | | job/tasks/import-user.task.js | 226 | use dedicated dir 10 | 11 | ### FIXMEs 12 | | Filename | line # | FIXME 13 | |:------|:------:|:------ 14 | | storage/s3.storage.js | 122 | S3 copy don't work and copy an empty file :( -------------------------------------------------------------------------------- /doc/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | bower_components 4 | *.swp 5 | -------------------------------------------------------------------------------- /doc/.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | src/**/*.ttf binary 3 | dist/**/*.js binary 4 | dist/**/*.ttf binary 5 | dist/**/*.map binary 6 | dist/**/*.eot binary 7 | dist/**/*.svg binary 8 | dist/**/*.ttf binary 9 | dist/**/*.woff binary 10 | dist/**/*.woff2 binary 11 | dist/**/*.png binary 12 | dist/*.html text 13 | 14 | src/main/html/fonts/*.ttf binary 15 | src/main/html/images/*.png binary 16 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.ipr 3 | *.iml 4 | *.iws 5 | *.flags.json 6 | npm-debug.log 7 | web/ 8 | lib/*.zip 9 | version.properties 10 | .sass-cache 11 | swagger-ui.sublime-workspace 12 | .idea 13 | .project 14 | node_modules/* 15 | /nbproject/private/ 16 | dist/specs/ 17 | test/specs/local/ 18 | -------------------------------------------------------------------------------- /doc/.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/main/javascript/doc.js 3 | dist 4 | lib 5 | .log -------------------------------------------------------------------------------- /doc/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": false, 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "regexp": true, 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "validthis": true, 21 | "globals": { 22 | 23 | // Libraries 24 | "_": false, 25 | "$": false, 26 | "Backbone": false, 27 | "Handlebars": false, 28 | "jQuery": false, 29 | "marked": false, 30 | "SwaggerClient": false, 31 | "hljs": false, 32 | "SwaggerUi": false, 33 | "jsyaml": false, 34 | "define": false, 35 | "sanitizeHtml": false, 36 | 37 | // Global object 38 | // TODO: remove these 39 | "Docs": false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /doc/.npmignore: -------------------------------------------------------------------------------- 1 | *.sublime-* 2 | example.html 3 | *.tgz 4 | .classpath 5 | .project 6 | .npmignore 7 | dist/sample.html 8 | dist/spec.js 9 | node_modules 10 | -------------------------------------------------------------------------------- /doc/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '4.2' 5 | install: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | - npm i -g jshint 9 | - npm install 10 | -------------------------------------------------------------------------------- /doc/METRICS.md: -------------------------------------------------------------------------------- 1 | # Metrics 2 | 3 | This document lists all metrics sent to the external metric system. 4 | 5 | ## Document's metrics 6 | 7 | ``` 8 | keeper_document_total 9 | keeper_document_usage,owner=${owner} 10 | keeper_document_event,action=${action},owner=${owner},id=${id} 11 | ``` 12 | 13 | Sample reports: 14 | 15 | - Total number of documents 16 | - Total number of documents per owner 17 | - Document actions (CRUD) per owner 18 | 19 | 20 | ## Label's metrics 21 | 22 | ``` 23 | keeper_label_total 24 | keeper_label_usage,owner=${owner} 25 | keeper_label_event,action=${action},owner=${owner},id=${id} 26 | ``` 27 | 28 | Sample reports: 29 | 30 | - Total number of labels 31 | - Total number of labels per owner 32 | - Label actions (CRUD) per owner 33 | 34 | ## Sharing metrics 35 | 36 | ``` 37 | keeper_sharing_total 38 | keeper_sharing_usage,owner=${owner} 39 | keeper_sharing_event,action=${action},owner=${owner},id=${id} 40 | ``` 41 | 42 | Sample reports: 43 | 44 | - Total number of sharing 45 | - Total number of sharing per owner 46 | - Sharing actions (CRUD) per owner 47 | 48 | ## Webhook's metrics 49 | 50 | ``` 51 | keeper_webhook_total 52 | keeper_webhook_usage,owner=${owner} 53 | keeper_webhook_event,action=${action},owner=${owner},id=${id} 54 | keeper_webhook_call,status=${status}owner=${owner},id=${id} 55 | ``` 56 | 57 | Sample reports: 58 | 59 | - Total number of webhooks 60 | - Total number of webhooks per owner 61 | - Webhook actions (CRUD) per owner 62 | - Webhook calls 63 | 64 | ## User's metrics 65 | 66 | ``` 67 | keeper_user_total 68 | keeper_user_event,action=${action},uid=${uid} 69 | ``` 70 | 71 | Sample reports: 72 | 73 | - Total number of users 74 | - User actions (CRUD, AuthN rejection) 75 | 76 | ## Storage metrics 77 | 78 | ``` 79 | keeper_storage_total 80 | keeper_storage_usage,owner=${owner} 81 | ``` 82 | 83 | Sample reports: 84 | 85 | - Total storage usage 86 | - Total storage usage per owner 87 | 88 | ## Job's metrics 89 | 90 | ``` 91 | keeper_processed_job,name=${name},status=${status} 92 | ``` 93 | 94 | Sample reports: 95 | 96 | - Job duration by name 97 | 98 | 99 | --- 100 | 101 | -------------------------------------------------------------------------------- /doc/swagger-ui/css/reset.css: -------------------------------------------------------------------------------- 1 | a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:'';content:none}table{border-collapse:collapse;border-spacing:0} -------------------------------------------------------------------------------- /doc/swagger-ui/css/typography.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/css/typography.css -------------------------------------------------------------------------------- /doc/swagger-ui/fonts/DroidSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/fonts/DroidSans-Bold.ttf -------------------------------------------------------------------------------- /doc/swagger-ui/fonts/DroidSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/fonts/DroidSans.ttf -------------------------------------------------------------------------------- /doc/swagger-ui/images/collapse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/images/collapse.gif -------------------------------------------------------------------------------- /doc/swagger-ui/images/expand.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/images/expand.gif -------------------------------------------------------------------------------- /doc/swagger-ui/images/explorer_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/images/explorer_icons.png -------------------------------------------------------------------------------- /doc/swagger-ui/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/images/favicon-16x16.png -------------------------------------------------------------------------------- /doc/swagger-ui/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/images/favicon-32x32.png -------------------------------------------------------------------------------- /doc/swagger-ui/images/favicon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/images/favicon-48x48.png -------------------------------------------------------------------------------- /doc/swagger-ui/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/images/favicon.ico -------------------------------------------------------------------------------- /doc/swagger-ui/images/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/images/logo_small.png -------------------------------------------------------------------------------- /doc/swagger-ui/images/pet_store_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/images/pet_store_api.png -------------------------------------------------------------------------------- /doc/swagger-ui/images/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/images/throbber.gif -------------------------------------------------------------------------------- /doc/swagger-ui/images/wordnik_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/doc/swagger-ui/images/wordnik_api.png -------------------------------------------------------------------------------- /doc/swagger-ui/lang/ca.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Advertència: Obsolet", 6 | "Implementation Notes":"Notes d'implementació", 7 | "Response Class":"Classe de la Resposta", 8 | "Status":"Estatus", 9 | "Parameters":"Paràmetres", 10 | "Parameter":"Paràmetre", 11 | "Value":"Valor", 12 | "Description":"Descripció", 13 | "Parameter Type":"Tipus del Paràmetre", 14 | "Data Type":"Tipus de la Dada", 15 | "Response Messages":"Missatges de la Resposta", 16 | "HTTP Status Code":"Codi d'Estatus HTTP", 17 | "Reason":"Raó", 18 | "Response Model":"Model de la Resposta", 19 | "Request URL":"URL de la Sol·licitud", 20 | "Response Body":"Cos de la Resposta", 21 | "Response Code":"Codi de la Resposta", 22 | "Response Headers":"Capçaleres de la Resposta", 23 | "Hide Response":"Amagar Resposta", 24 | "Try it out!":"Prova-ho!", 25 | "Show/Hide":"Mostrar/Amagar", 26 | "List Operations":"Llista Operacions", 27 | "Expand Operations":"Expandir Operacions", 28 | "Raw":"Cru", 29 | "can't parse JSON. Raw result":"no puc analitzar el JSON. Resultat cru", 30 | "Example Value":"Valor d'Exemple", 31 | "Model Schema":"Esquema del Model", 32 | "Model":"Model", 33 | "apply":"aplicar", 34 | "Username":"Nom d'usuari", 35 | "Password":"Contrasenya", 36 | "Terms of service":"Termes del servei", 37 | "Created by":"Creat per", 38 | "See more at":"Veure més en", 39 | "Contact the developer":"Contactar amb el desenvolupador", 40 | "api version":"versió de la api", 41 | "Response Content Type":"Tipus de Contingut de la Resposta", 42 | "fetching resource":"recollint recurs", 43 | "fetching resource list":"recollins llista de recursos", 44 | "Explore":"Explorant", 45 | "Show Swagger Petstore Example Apis":"Mostrar API d'Exemple Swagger Petstore", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"No es pot llegir del servidor. Potser no teniu la configuració de control d'accés apropiada.", 47 | "Please specify the protocol for":"Si us plau, especifiqueu el protocol per a", 48 | "Can't read swagger JSON from":"No es pot llegir el JSON de swagger des de", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Finalitzada la càrrega del recurs informatiu. Renderitzant Swagger UI", 50 | "Unable to read api":"No es pot llegir l'api", 51 | "from path":"des de la ruta", 52 | "server returned":"el servidor ha retornat" 53 | }); 54 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/en.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Warning: Deprecated", 6 | "Implementation Notes":"Implementation Notes", 7 | "Response Class":"Response Class", 8 | "Status":"Status", 9 | "Parameters":"Parameters", 10 | "Parameter":"Parameter", 11 | "Value":"Value", 12 | "Description":"Description", 13 | "Parameter Type":"Parameter Type", 14 | "Data Type":"Data Type", 15 | "Response Messages":"Response Messages", 16 | "HTTP Status Code":"HTTP Status Code", 17 | "Reason":"Reason", 18 | "Response Model":"Response Model", 19 | "Request URL":"Request URL", 20 | "Response Body":"Response Body", 21 | "Response Code":"Response Code", 22 | "Response Headers":"Response Headers", 23 | "Hide Response":"Hide Response", 24 | "Headers":"Headers", 25 | "Try it out!":"Try it out!", 26 | "Show/Hide":"Show/Hide", 27 | "List Operations":"List Operations", 28 | "Expand Operations":"Expand Operations", 29 | "Raw":"Raw", 30 | "can't parse JSON. Raw result":"can't parse JSON. Raw result", 31 | "Example Value":"Example Value", 32 | "Model Schema":"Model Schema", 33 | "Model":"Model", 34 | "Click to set as parameter value":"Click to set as parameter value", 35 | "apply":"apply", 36 | "Username":"Username", 37 | "Password":"Password", 38 | "Terms of service":"Terms of service", 39 | "Created by":"Created by", 40 | "See more at":"See more at", 41 | "Contact the developer":"Contact the developer", 42 | "api version":"api version", 43 | "Response Content Type":"Response Content Type", 44 | "Parameter content type:":"Parameter content type:", 45 | "fetching resource":"fetching resource", 46 | "fetching resource list":"fetching resource list", 47 | "Explore":"Explore", 48 | "Show Swagger Petstore Example Apis":"Show Swagger Petstore Example Apis", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Can't read from server. It may not have the appropriate access-control-origin settings.", 50 | "Please specify the protocol for":"Please specify the protocol for", 51 | "Can't read swagger JSON from":"Can't read swagger JSON from", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"Finished Loading Resource Information. Rendering Swagger UI", 53 | "Unable to read api":"Unable to read api", 54 | "from path":"from path", 55 | "server returned":"server returned" 56 | }); 57 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/es.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Advertencia: Obsoleto", 6 | "Implementation Notes":"Notas de implementación", 7 | "Response Class":"Clase de la Respuesta", 8 | "Status":"Status", 9 | "Parameters":"Parámetros", 10 | "Parameter":"Parámetro", 11 | "Value":"Valor", 12 | "Description":"Descripción", 13 | "Parameter Type":"Tipo del Parámetro", 14 | "Data Type":"Tipo del Dato", 15 | "Response Messages":"Mensajes de la Respuesta", 16 | "HTTP Status Code":"Código de Status HTTP", 17 | "Reason":"Razón", 18 | "Response Model":"Modelo de la Respuesta", 19 | "Request URL":"URL de la Solicitud", 20 | "Response Body":"Cuerpo de la Respuesta", 21 | "Response Code":"Código de la Respuesta", 22 | "Response Headers":"Encabezados de la Respuesta", 23 | "Hide Response":"Ocultar Respuesta", 24 | "Try it out!":"Pruébalo!", 25 | "Show/Hide":"Mostrar/Ocultar", 26 | "List Operations":"Listar Operaciones", 27 | "Expand Operations":"Expandir Operaciones", 28 | "Raw":"Crudo", 29 | "can't parse JSON. Raw result":"no puede parsear el JSON. Resultado crudo", 30 | "Example Value":"Valor de Ejemplo", 31 | "Model Schema":"Esquema del Modelo", 32 | "Model":"Modelo", 33 | "apply":"aplicar", 34 | "Username":"Nombre de usuario", 35 | "Password":"Contraseña", 36 | "Terms of service":"Términos de Servicio", 37 | "Created by":"Creado por", 38 | "See more at":"Ver más en", 39 | "Contact the developer":"Contactar al desarrollador", 40 | "api version":"versión de la api", 41 | "Response Content Type":"Tipo de Contenido (Content Type) de la Respuesta", 42 | "fetching resource":"buscando recurso", 43 | "fetching resource list":"buscando lista del recurso", 44 | "Explore":"Explorar", 45 | "Show Swagger Petstore Example Apis":"Mostrar Api Ejemplo de Swagger Petstore", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"No se puede leer del servidor. Tal vez no tiene la configuración de control de acceso de origen (access-control-origin) apropiado.", 47 | "Please specify the protocol for":"Por favor, especificar el protocola para", 48 | "Can't read swagger JSON from":"No se puede leer el JSON de swagger desde", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Finalizada la carga del recurso de Información. Mostrando Swagger UI", 50 | "Unable to read api":"No se puede leer la api", 51 | "from path":"desde ruta", 52 | "server returned":"el servidor retornó" 53 | }); 54 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/fr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Avertissement : Obsolète", 6 | "Implementation Notes":"Notes d'implémentation", 7 | "Response Class":"Classe de la réponse", 8 | "Status":"Statut", 9 | "Parameters":"Paramètres", 10 | "Parameter":"Paramètre", 11 | "Value":"Valeur", 12 | "Description":"Description", 13 | "Parameter Type":"Type du paramètre", 14 | "Data Type":"Type de données", 15 | "Response Messages":"Messages de la réponse", 16 | "HTTP Status Code":"Code de statut HTTP", 17 | "Reason":"Raison", 18 | "Response Model":"Modèle de réponse", 19 | "Request URL":"URL appelée", 20 | "Response Body":"Corps de la réponse", 21 | "Response Code":"Code de la réponse", 22 | "Response Headers":"En-têtes de la réponse", 23 | "Hide Response":"Cacher la réponse", 24 | "Headers":"En-têtes", 25 | "Try it out!":"Testez !", 26 | "Show/Hide":"Afficher/Masquer", 27 | "List Operations":"Liste des opérations", 28 | "Expand Operations":"Développer les opérations", 29 | "Raw":"Brut", 30 | "can't parse JSON. Raw result":"impossible de décoder le JSON. Résultat brut", 31 | "Example Value":"Exemple la valeur", 32 | "Model Schema":"Définition du modèle", 33 | "Model":"Modèle", 34 | "apply":"appliquer", 35 | "Username":"Nom d'utilisateur", 36 | "Password":"Mot de passe", 37 | "Terms of service":"Conditions de service", 38 | "Created by":"Créé par", 39 | "See more at":"Voir plus sur", 40 | "Contact the developer":"Contacter le développeur", 41 | "api version":"version de l'api", 42 | "Response Content Type":"Content Type de la réponse", 43 | "fetching resource":"récupération de la ressource", 44 | "fetching resource list":"récupération de la liste de ressources", 45 | "Explore":"Explorer", 46 | "Show Swagger Petstore Example Apis":"Montrer les Apis de l'exemple Petstore de Swagger", 47 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Impossible de lire à partir du serveur. Il se peut que les réglages access-control-origin ne soient pas appropriés.", 48 | "Please specify the protocol for":"Veuillez spécifier un protocole pour", 49 | "Can't read swagger JSON from":"Impossible de lire le JSON swagger à partir de", 50 | "Finished Loading Resource Information. Rendering Swagger UI":"Chargement des informations terminé. Affichage de Swagger UI", 51 | "Unable to read api":"Impossible de lire l'api", 52 | "from path":"à partir du chemin", 53 | "server returned":"réponse du serveur" 54 | }); 55 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/geo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"ყურადღება: აღარ გამოიყენება", 6 | "Implementation Notes":"იმპლემენტაციის აღწერა", 7 | "Response Class":"რესპონს კლასი", 8 | "Status":"სტატუსი", 9 | "Parameters":"პარამეტრები", 10 | "Parameter":"პარამეტრი", 11 | "Value":"მნიშვნელობა", 12 | "Description":"აღწერა", 13 | "Parameter Type":"პარამეტრის ტიპი", 14 | "Data Type":"მონაცემის ტიპი", 15 | "Response Messages":"პასუხი", 16 | "HTTP Status Code":"HTTP სტატუსი", 17 | "Reason":"მიზეზი", 18 | "Response Model":"რესპონს მოდელი", 19 | "Request URL":"მოთხოვნის URL", 20 | "Response Body":"პასუხის სხეული", 21 | "Response Code":"პასუხის კოდი", 22 | "Response Headers":"პასუხის ჰედერები", 23 | "Hide Response":"დამალე პასუხი", 24 | "Headers":"ჰედერები", 25 | "Try it out!":"ცადე !", 26 | "Show/Hide":"გამოჩენა/დამალვა", 27 | "List Operations":"ოპერაციების სია", 28 | "Expand Operations":"ოპერაციები ვრცლად", 29 | "Raw":"ნედლი", 30 | "can't parse JSON. Raw result":"JSON-ის დამუშავება ვერ მოხერხდა. ნედლი პასუხი", 31 | "Example Value":"მაგალითი", 32 | "Model Schema":"მოდელის სტრუქტურა", 33 | "Model":"მოდელი", 34 | "Click to set as parameter value":"პარამეტრისთვის მნიშვნელობის მისანიჭებლად, დააკლიკე", 35 | "apply":"გამოყენება", 36 | "Username":"მოხმარებელი", 37 | "Password":"პაროლი", 38 | "Terms of service":"მომსახურების პირობები", 39 | "Created by":"შექმნა", 40 | "See more at":"ნახე ვრცლად", 41 | "Contact the developer":"დაუკავშირდი დეველოპერს", 42 | "api version":"api ვერსია", 43 | "Response Content Type":"პასუხის კონტენტის ტიპი", 44 | "Parameter content type:":"პარამეტრის კონტენტის ტიპი:", 45 | "fetching resource":"რესურსების მიღება", 46 | "fetching resource list":"რესურსების სიის მიღება", 47 | "Explore":"ნახვა", 48 | "Show Swagger Petstore Example Apis":"ნახე Swagger Petstore სამაგალითო Api", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"სერვერთან დაკავშირება ვერ ხერხდება. შეამოწმეთ access-control-origin.", 50 | "Please specify the protocol for":"მიუთითეთ პროტოკოლი", 51 | "Can't read swagger JSON from":"swagger JSON წაკითხვა ვერ მოხერხდა", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"რესურსების ჩატვირთვა სრულდება. Swagger UI რენდერდება", 53 | "Unable to read api":"api წაკითხვა ვერ მოხერხდა", 54 | "from path":"მისამართიდან", 55 | "server returned":"სერვერმა დააბრუნა" 56 | }); 57 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/it.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Attenzione: Deprecato", 6 | "Implementation Notes":"Note di implementazione", 7 | "Response Class":"Classe della risposta", 8 | "Status":"Stato", 9 | "Parameters":"Parametri", 10 | "Parameter":"Parametro", 11 | "Value":"Valore", 12 | "Description":"Descrizione", 13 | "Parameter Type":"Tipo di parametro", 14 | "Data Type":"Tipo di dato", 15 | "Response Messages":"Messaggi della risposta", 16 | "HTTP Status Code":"Codice stato HTTP", 17 | "Reason":"Motivo", 18 | "Response Model":"Modello di risposta", 19 | "Request URL":"URL della richiesta", 20 | "Response Body":"Corpo della risposta", 21 | "Response Code":"Oggetto della risposta", 22 | "Response Headers":"Intestazioni della risposta", 23 | "Hide Response":"Nascondi risposta", 24 | "Try it out!":"Provalo!", 25 | "Show/Hide":"Mostra/Nascondi", 26 | "List Operations":"Mostra operazioni", 27 | "Expand Operations":"Espandi operazioni", 28 | "Raw":"Grezzo (raw)", 29 | "can't parse JSON. Raw result":"non è possibile parsare il JSON. Risultato grezzo (raw).", 30 | "Model Schema":"Schema del modello", 31 | "Model":"Modello", 32 | "apply":"applica", 33 | "Username":"Nome utente", 34 | "Password":"Password", 35 | "Terms of service":"Condizioni del servizio", 36 | "Created by":"Creato da", 37 | "See more at":"Informazioni aggiuntive:", 38 | "Contact the developer":"Contatta lo sviluppatore", 39 | "api version":"versione api", 40 | "Response Content Type":"Tipo di contenuto (content type) della risposta", 41 | "fetching resource":"recuperando la risorsa", 42 | "fetching resource list":"recuperando lista risorse", 43 | "Explore":"Esplora", 44 | "Show Swagger Petstore Example Apis":"Mostra le api di esempio di Swagger Petstore", 45 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Non è possibile leggere dal server. Potrebbe non avere le impostazioni di controllo accesso origine (access-control-origin) appropriate.", 46 | "Please specify the protocol for":"Si prega di specificare il protocollo per", 47 | "Can't read swagger JSON from":"Impossibile leggere JSON swagger da:", 48 | "Finished Loading Resource Information. Rendering Swagger UI":"Lettura informazioni risorse termianta. Swagger UI viene mostrata", 49 | "Unable to read api":"Impossibile leggere la api", 50 | "from path":"da cartella", 51 | "server returned":"il server ha restituito" 52 | }); 53 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/ja.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"警告: 廃止予定", 6 | "Implementation Notes":"実装メモ", 7 | "Response Class":"レスポンスクラス", 8 | "Status":"ステータス", 9 | "Parameters":"パラメータ群", 10 | "Parameter":"パラメータ", 11 | "Value":"値", 12 | "Description":"説明", 13 | "Parameter Type":"パラメータタイプ", 14 | "Data Type":"データタイプ", 15 | "Response Messages":"レスポンスメッセージ", 16 | "HTTP Status Code":"HTTPステータスコード", 17 | "Reason":"理由", 18 | "Response Model":"レスポンスモデル", 19 | "Request URL":"リクエストURL", 20 | "Response Body":"レスポンスボディ", 21 | "Response Code":"レスポンスコード", 22 | "Response Headers":"レスポンスヘッダ", 23 | "Hide Response":"レスポンスを隠す", 24 | "Headers":"ヘッダ", 25 | "Try it out!":"実際に実行!", 26 | "Show/Hide":"表示/非表示", 27 | "List Operations":"操作一覧", 28 | "Expand Operations":"操作の展開", 29 | "Raw":"未加工", 30 | "can't parse JSON. Raw result":"JSONへ解釈できません. 未加工の結果", 31 | "Example Value":"値の例", 32 | "Model Schema":"モデルスキーマ", 33 | "Model":"モデル", 34 | "Click to set as parameter value":"パラメータ値と設定するにはクリック", 35 | "apply":"実行", 36 | "Username":"ユーザ名", 37 | "Password":"パスワード", 38 | "Terms of service":"サービス利用規約", 39 | "Created by":"Created by", 40 | "See more at":"詳細を見る", 41 | "Contact the developer":"開発者に連絡", 42 | "api version":"APIバージョン", 43 | "Response Content Type":"レスポンス コンテンツタイプ", 44 | "Parameter content type:":"パラメータコンテンツタイプ:", 45 | "fetching resource":"リソースの取得", 46 | "fetching resource list":"リソース一覧の取得", 47 | "Explore":"調査", 48 | "Show Swagger Petstore Example Apis":"SwaggerペットストアAPIの表示", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"サーバから読み込めません. 適切なaccess-control-origin設定を持っていない可能性があります.", 50 | "Please specify the protocol for":"プロトコルを指定してください", 51 | "Can't read swagger JSON from":"次からswagger JSONを読み込めません", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"リソース情報の読み込みが完了しました. Swagger UIを描画しています", 53 | "Unable to read api":"APIを読み込めません", 54 | "from path":"次のパスから", 55 | "server returned":"サーバからの返答" 56 | }); 57 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/ko-kr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"경고:폐기예정됨", 6 | "Implementation Notes":"구현 노트", 7 | "Response Class":"응답 클래스", 8 | "Status":"상태", 9 | "Parameters":"매개변수들", 10 | "Parameter":"매개변수", 11 | "Value":"값", 12 | "Description":"설명", 13 | "Parameter Type":"매개변수 타입", 14 | "Data Type":"데이터 타입", 15 | "Response Messages":"응답 메세지", 16 | "HTTP Status Code":"HTTP 상태 코드", 17 | "Reason":"원인", 18 | "Response Model":"응답 모델", 19 | "Request URL":"요청 URL", 20 | "Response Body":"응답 본문", 21 | "Response Code":"응답 코드", 22 | "Response Headers":"응답 헤더", 23 | "Hide Response":"응답 숨기기", 24 | "Headers":"헤더", 25 | "Try it out!":"써보기!", 26 | "Show/Hide":"보이기/숨기기", 27 | "List Operations":"목록 작업", 28 | "Expand Operations":"전개 작업", 29 | "Raw":"원본", 30 | "can't parse JSON. Raw result":"JSON을 파싱할수 없음. 원본결과:", 31 | "Model Schema":"모델 스키마", 32 | "Model":"모델", 33 | "apply":"적용", 34 | "Username":"사용자 이름", 35 | "Password":"암호", 36 | "Terms of service":"이용약관", 37 | "Created by":"작성자", 38 | "See more at":"추가정보:", 39 | "Contact the developer":"개발자에게 문의", 40 | "api version":"api버전", 41 | "Response Content Type":"응답Content Type", 42 | "fetching resource":"리소스 가져오기", 43 | "fetching resource list":"리소스 목록 가져오기", 44 | "Explore":"탐색", 45 | "Show Swagger Petstore Example Apis":"Swagger Petstore 예제 보기", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"서버로부터 읽어들일수 없습니다. access-control-origin 설정이 올바르지 않을수 있습니다.", 47 | "Please specify the protocol for":"다음을 위한 프로토콜을 정하세요", 48 | "Can't read swagger JSON from":"swagger JSON 을 다음으로 부터 읽을수 없습니다", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"리소스 정보 불러오기 완료. Swagger UI 랜더링", 50 | "Unable to read api":"api를 읽을 수 없습니다.", 51 | "from path":"다음 경로로 부터", 52 | "server returned":"서버 응답함." 53 | }); 54 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/pl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Uwaga: Wycofane", 6 | "Implementation Notes":"Uwagi Implementacji", 7 | "Response Class":"Klasa Odpowiedzi", 8 | "Status":"Status", 9 | "Parameters":"Parametry", 10 | "Parameter":"Parametr", 11 | "Value":"Wartość", 12 | "Description":"Opis", 13 | "Parameter Type":"Typ Parametru", 14 | "Data Type":"Typ Danych", 15 | "Response Messages":"Wiadomości Odpowiedzi", 16 | "HTTP Status Code":"Kod Statusu HTTP", 17 | "Reason":"Przyczyna", 18 | "Response Model":"Model Odpowiedzi", 19 | "Request URL":"URL Wywołania", 20 | "Response Body":"Treść Odpowiedzi", 21 | "Response Code":"Kod Odpowiedzi", 22 | "Response Headers":"Nagłówki Odpowiedzi", 23 | "Hide Response":"Ukryj Odpowiedź", 24 | "Headers":"Nagłówki", 25 | "Try it out!":"Wypróbuj!", 26 | "Show/Hide":"Pokaż/Ukryj", 27 | "List Operations":"Lista Operacji", 28 | "Expand Operations":"Rozwiń Operacje", 29 | "Raw":"Nieprzetworzone", 30 | "can't parse JSON. Raw result":"nie można przetworzyć pliku JSON. Nieprzetworzone dane", 31 | "Model Schema":"Schemat Modelu", 32 | "Model":"Model", 33 | "apply":"użyj", 34 | "Username":"Nazwa użytkownika", 35 | "Password":"Hasło", 36 | "Terms of service":"Warunki używania", 37 | "Created by":"Utworzone przez", 38 | "See more at":"Zobacz więcej na", 39 | "Contact the developer":"Kontakt z deweloperem", 40 | "api version":"wersja api", 41 | "Response Content Type":"Typ Zasobu Odpowiedzi", 42 | "fetching resource":"ładowanie zasobu", 43 | "fetching resource list":"ładowanie listy zasobów", 44 | "Explore":"Eksploruj", 45 | "Show Swagger Petstore Example Apis":"Pokaż Przykładowe Api Swagger Petstore", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Brak połączenia z serwerem. Może on nie mieć odpowiednich ustawień access-control-origin.", 47 | "Please specify the protocol for":"Proszę podać protokół dla", 48 | "Can't read swagger JSON from":"Nie można odczytać swagger JSON z", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Ukończono Ładowanie Informacji o Zasobie. Renderowanie Swagger UI", 50 | "Unable to read api":"Nie można odczytać api", 51 | "from path":"ze ścieżki", 52 | "server returned":"serwer zwrócił" 53 | }); 54 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/pt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Aviso: Depreciado", 6 | "Implementation Notes":"Notas de Implementação", 7 | "Response Class":"Classe de resposta", 8 | "Status":"Status", 9 | "Parameters":"Parâmetros", 10 | "Parameter":"Parâmetro", 11 | "Value":"Valor", 12 | "Description":"Descrição", 13 | "Parameter Type":"Tipo de parâmetro", 14 | "Data Type":"Tipo de dados", 15 | "Response Messages":"Mensagens de resposta", 16 | "HTTP Status Code":"Código de status HTTP", 17 | "Reason":"Razão", 18 | "Response Model":"Modelo resposta", 19 | "Request URL":"URL requisição", 20 | "Response Body":"Corpo da resposta", 21 | "Response Code":"Código da resposta", 22 | "Response Headers":"Cabeçalho da resposta", 23 | "Headers":"Cabeçalhos", 24 | "Hide Response":"Esconder resposta", 25 | "Try it out!":"Tente agora!", 26 | "Show/Hide":"Mostrar/Esconder", 27 | "List Operations":"Listar operações", 28 | "Expand Operations":"Expandir operações", 29 | "Raw":"Cru", 30 | "can't parse JSON. Raw result":"Falha ao analisar JSON. Resulto cru", 31 | "Model Schema":"Modelo esquema", 32 | "Model":"Modelo", 33 | "apply":"Aplicar", 34 | "Username":"Usuário", 35 | "Password":"Senha", 36 | "Terms of service":"Termos do serviço", 37 | "Created by":"Criado por", 38 | "See more at":"Veja mais em", 39 | "Contact the developer":"Contate o desenvolvedor", 40 | "api version":"Versão api", 41 | "Response Content Type":"Tipo de conteúdo da resposta", 42 | "fetching resource":"busca recurso", 43 | "fetching resource list":"buscando lista de recursos", 44 | "Explore":"Explorar", 45 | "Show Swagger Petstore Example Apis":"Show Swagger Petstore Example Apis", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Não é possível ler do servidor. Pode não ter as apropriadas configurações access-control-origin", 47 | "Please specify the protocol for":"Por favor especifique o protocolo", 48 | "Can't read swagger JSON from":"Não é possível ler o JSON Swagger de", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Carregar informação de recurso finalizada. Renderizando Swagger UI", 50 | "Unable to read api":"Não foi possível ler api", 51 | "from path":"do caminho", 52 | "server returned":"servidor retornou" 53 | }); 54 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/ru.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Предупреждение: Устарело", 6 | "Implementation Notes":"Заметки", 7 | "Response Class":"Пример ответа", 8 | "Status":"Статус", 9 | "Parameters":"Параметры", 10 | "Parameter":"Параметр", 11 | "Value":"Значение", 12 | "Description":"Описание", 13 | "Parameter Type":"Тип параметра", 14 | "Data Type":"Тип данных", 15 | "HTTP Status Code":"HTTP код", 16 | "Reason":"Причина", 17 | "Response Model":"Структура ответа", 18 | "Request URL":"URL запроса", 19 | "Response Body":"Тело ответа", 20 | "Response Code":"HTTP код ответа", 21 | "Response Headers":"Заголовки ответа", 22 | "Hide Response":"Спрятать ответ", 23 | "Headers":"Заголовки", 24 | "Response Messages":"Что может прийти в ответ", 25 | "Try it out!":"Попробовать!", 26 | "Show/Hide":"Показать/Скрыть", 27 | "List Operations":"Операции кратко", 28 | "Expand Operations":"Операции подробно", 29 | "Raw":"В сыром виде", 30 | "can't parse JSON. Raw result":"Не удается распарсить ответ:", 31 | "Example Value":"Пример", 32 | "Model Schema":"Структура", 33 | "Model":"Описание", 34 | "Click to set as parameter value":"Нажмите, чтобы испльзовать в качестве значения параметра", 35 | "apply":"применить", 36 | "Username":"Имя пользователя", 37 | "Password":"Пароль", 38 | "Terms of service":"Условия использования", 39 | "Created by":"Разработано", 40 | "See more at":"Еще тут", 41 | "Contact the developer":"Связаться с разработчиком", 42 | "api version":"Версия API", 43 | "Response Content Type":"Content Type ответа", 44 | "Parameter content type:":"Content Type параметра:", 45 | "fetching resource":"Получение ресурса", 46 | "fetching resource list":"Получение ресурсов", 47 | "Explore":"Показать", 48 | "Show Swagger Petstore Example Apis":"Показать примеры АПИ", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Не удается получить ответ от сервера. Возможно, проблема с настройками доступа", 50 | "Please specify the protocol for":"Пожалуйста, укажите протокол для", 51 | "Can't read swagger JSON from":"Не получается прочитать swagger json из", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"Загрузка информации о ресурсах завершена. Рендерим", 53 | "Unable to read api":"Не удалось прочитать api", 54 | "from path":"по адресу", 55 | "server returned":"сервер сказал" 56 | }); 57 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/tr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Uyarı: Deprecated", 6 | "Implementation Notes":"Gerçekleştirim Notları", 7 | "Response Class":"Dönen Sınıf", 8 | "Status":"Statü", 9 | "Parameters":"Parametreler", 10 | "Parameter":"Parametre", 11 | "Value":"Değer", 12 | "Description":"Açıklama", 13 | "Parameter Type":"Parametre Tipi", 14 | "Data Type":"Veri Tipi", 15 | "Response Messages":"Dönüş Mesajı", 16 | "HTTP Status Code":"HTTP Statü Kodu", 17 | "Reason":"Gerekçe", 18 | "Response Model":"Dönüş Modeli", 19 | "Request URL":"İstek URL", 20 | "Response Body":"Dönüş İçeriği", 21 | "Response Code":"Dönüş Kodu", 22 | "Response Headers":"Dönüş Üst Bilgileri", 23 | "Hide Response":"Dönüşü Gizle", 24 | "Headers":"Üst Bilgiler", 25 | "Try it out!":"Dene!", 26 | "Show/Hide":"Göster/Gizle", 27 | "List Operations":"Operasyonları Listele", 28 | "Expand Operations":"Operasyonları Aç", 29 | "Raw":"Ham", 30 | "can't parse JSON. Raw result":"JSON çözümlenemiyor. Ham sonuç", 31 | "Model Schema":"Model Şema", 32 | "Model":"Model", 33 | "apply":"uygula", 34 | "Username":"Kullanıcı Adı", 35 | "Password":"Parola", 36 | "Terms of service":"Servis şartları", 37 | "Created by":"Oluşturan", 38 | "See more at":"Daha fazlası için", 39 | "Contact the developer":"Geliştirici ile İletişime Geçin", 40 | "api version":"api versiyon", 41 | "Response Content Type":"Dönüş İçerik Tipi", 42 | "fetching resource":"kaynak getiriliyor", 43 | "fetching resource list":"kaynak listesi getiriliyor", 44 | "Explore":"Keşfet", 45 | "Show Swagger Petstore Example Apis":"Swagger Petstore Örnek Api'yi Gör", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Sunucudan okuma yapılamıyor. Sunucu access-control-origin ayarlarınızı kontrol edin.", 47 | "Please specify the protocol for":"Lütfen istenen adres için protokol belirtiniz", 48 | "Can't read swagger JSON from":"Swagger JSON bu kaynaktan okunamıyor", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Kaynak baglantısı tamamlandı. Swagger UI gösterime hazırlanıyor", 50 | "Unable to read api":"api okunamadı", 51 | "from path":"yoldan", 52 | "server returned":"sunucuya dönüldü" 53 | }); 54 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/translator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Translator for documentation pages. 5 | * 6 | * To enable translation you should include one of language-files in your index.html 7 | * after . 8 | * For example - 9 | * 10 | * If you wish to translate some new texts you should do two things: 11 | * 1. Add a new phrase pair ("New Phrase": "New Translation") into your language file (for example lang/ru.js). It will be great if you add it in other language files too. 12 | * 2. Mark that text it templates this way New Phrase or . 13 | * The main thing here is attribute data-sw-translate. Only inner html, title-attribute and value-attribute are going to translate. 14 | * 15 | */ 16 | window.SwaggerTranslator = { 17 | 18 | _words:[], 19 | 20 | translate: function(sel) { 21 | var $this = this; 22 | sel = sel || '[data-sw-translate]'; 23 | 24 | $(sel).each(function() { 25 | $(this).html($this._tryTranslate($(this).html())); 26 | 27 | $(this).val($this._tryTranslate($(this).val())); 28 | $(this).attr('title', $this._tryTranslate($(this).attr('title'))); 29 | }); 30 | }, 31 | 32 | _tryTranslate: function(word) { 33 | return this._words[$.trim(word)] !== undefined ? this._words[$.trim(word)] : word; 34 | }, 35 | 36 | learn: function(wordsMap) { 37 | this._words = wordsMap; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /doc/swagger-ui/lang/zh-cn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"警告:已过时", 6 | "Implementation Notes":"实现备注", 7 | "Response Class":"响应类", 8 | "Status":"状态", 9 | "Parameters":"参数", 10 | "Parameter":"参数", 11 | "Value":"值", 12 | "Description":"描述", 13 | "Parameter Type":"参数类型", 14 | "Data Type":"数据类型", 15 | "Response Messages":"响应消息", 16 | "HTTP Status Code":"HTTP状态码", 17 | "Reason":"原因", 18 | "Response Model":"响应模型", 19 | "Request URL":"请求URL", 20 | "Response Body":"响应体", 21 | "Response Code":"响应码", 22 | "Response Headers":"响应头", 23 | "Hide Response":"隐藏响应", 24 | "Headers":"头", 25 | "Try it out!":"试一下!", 26 | "Show/Hide":"显示/隐藏", 27 | "List Operations":"显示操作", 28 | "Expand Operations":"展开操作", 29 | "Raw":"原始", 30 | "can't parse JSON. Raw result":"无法解析JSON. 原始结果", 31 | "Example Value":"示例", 32 | "Click to set as parameter value":"点击设置参数", 33 | "Model Schema":"模型架构", 34 | "Model":"模型", 35 | "apply":"应用", 36 | "Username":"用户名", 37 | "Password":"密码", 38 | "Terms of service":"服务条款", 39 | "Created by":"创建者", 40 | "See more at":"查看更多:", 41 | "Contact the developer":"联系开发者", 42 | "api version":"api版本", 43 | "Response Content Type":"响应Content Type", 44 | "Parameter content type:":"参数类型:", 45 | "fetching resource":"正在获取资源", 46 | "fetching resource list":"正在获取资源列表", 47 | "Explore":"浏览", 48 | "Show Swagger Petstore Example Apis":"显示 Swagger Petstore 示例 Apis", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"无法从服务器读取。可能没有正确设置access-control-origin。", 50 | "Please specify the protocol for":"请指定协议:", 51 | "Can't read swagger JSON from":"无法读取swagger JSON于", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"已加载资源信息。正在渲染Swagger UI", 53 | "Unable to read api":"无法读取api", 54 | "from path":"从路径", 55 | "server returned":"服务器返回" 56 | }); 57 | -------------------------------------------------------------------------------- /doc/swagger-ui/lib/highlight.9.1.0.pack_extended.js: -------------------------------------------------------------------------------- 1 | "use strict";!function(){var h,l;h=hljs.configure,hljs.configure=function(l){var i=l.highlightSizeThreshold;hljs.highlightSizeThreshold=i===+i?i:null,h.call(this,l)},l=hljs.highlightBlock,hljs.highlightBlock=function(h){var i=h.innerHTML,g=hljs.highlightSizeThreshold;(null==g||g>i.length)&&l.call(hljs,h)}}(); -------------------------------------------------------------------------------- /doc/swagger-ui/lib/jquery.slideto.min.js: -------------------------------------------------------------------------------- 1 | !function(i){i.fn.slideto=function(o){return o=i.extend({slide_duration:"slow",highlight_duration:3e3,highlight:!0,highlight_color:"#FFFF99"},o),this.each(function(){obj=i(this),i("body").animate({scrollTop:obj.offset().top},o.slide_duration,function(){o.highlight&&i.ui.version&&obj.effect("highlight",{color:o.highlight_color},o.highlight_duration)})})}}(jQuery); -------------------------------------------------------------------------------- /doc/swagger-ui/lib/jquery.wiggle.min.js: -------------------------------------------------------------------------------- 1 | jQuery.fn.wiggle=function(e){var a={speed:50,wiggles:3,travel:5,callback:null},e=jQuery.extend(a,e);return this.each(function(){var a=this,l=(jQuery(this).wrap('
').css("position","relative"),0);for(i=1;i<=e.wiggles;i++)jQuery(this).animate({left:"-="+e.travel},e.speed).animate({left:"+="+2*e.travel},2*e.speed).animate({left:"-="+e.travel},e.speed,function(){l++,jQuery(a).parent().hasClass("wiggle-wrap")&&jQuery(a).parent().replaceWith(a),l==e.wiggles&&jQuery.isFunction(e.callback)&&e.callback()})})}; -------------------------------------------------------------------------------- /doc/swagger-ui/lib/object-assign-pollyfill.js: -------------------------------------------------------------------------------- 1 | "function"!=typeof Object.assign&&!function(){Object.assign=function(n){"use strict";if(void 0===n||null===n)throw new TypeError("Cannot convert undefined or null to object");for(var t=Object(n),o=1;o 2 | var qp = null; 3 | if(window.location.hash && window.location.hash !== "#_=_") { 4 | qp = location.hash.substring(1); 5 | } 6 | else { 7 | qp = location.search.substring(1); 8 | } 9 | qp = qp ? JSON.parse('{"' + qp.replace(/&/g, '","').replace(/=/g,'":"') + '"}', 10 | function(key, value) { 11 | return key===""?value:decodeURIComponent(value) } 12 | ):{} 13 | 14 | if (window.opener.swaggerUiAuth.tokenUrl) 15 | window.opener.processOAuthCode(qp); 16 | else 17 | window.opener.onOAuthComplete(qp); 18 | 19 | window.close(); 20 | 21 | -------------------------------------------------------------------------------- /docker-compose.app.yml: -------------------------------------------------------------------------------- 1 | version: "2.1" 2 | services: 3 | ####################################### 4 | # Core API 5 | ####################################### 6 | api: 7 | image: "ncarlier/keeper-core-api:latest" 8 | env_file: "etc/default/${ENV:-dev}.env" 9 | command: "${CMD:-start}" 10 | environment: 11 | - APP_DATABASE_URI=${APP_DATABASE_URI:-mongodb://mongo/keeper} 12 | volumes: 13 | - ${PWD}:${APP_SRC_DIR:-/usr/src/app_src} 14 | ports: 15 | - "${PORT:-3000}:3000" 16 | depends_on: 17 | redis: 18 | condition: service_started 19 | mongo: 20 | condition: service_started 21 | elasticsearch: 22 | condition: service_healthy 23 | 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.1" 2 | services: 3 | ####################################### 4 | # Redis 5 | ####################################### 6 | redis: 7 | image: "redis:4" 8 | 9 | ####################################### 10 | # Database (Mongo) 11 | ####################################### 12 | mongo: 13 | image: "mongo:3" 14 | 15 | ####################################### 16 | # Searchengine (ElasticSearch) 17 | ####################################### 18 | elasticsearch: 19 | image: "elasticsearch:5" 20 | environment: 21 | ES_JAVA_OPTS: "-Xms256m -Xmx256m" 22 | healthcheck: 23 | test: "curl --silent --fail localhost:9200/_cluster/health || exit 1" 24 | interval: 5s 25 | timeout: 2s 26 | retries: 15 27 | 28 | ####################################### 29 | # Mock server (wiremock) 30 | ####################################### 31 | wiremock: 32 | image: "rodolpheche/wiremock:latest" 33 | ports: 34 | - "${MOCK_PORT:-8888}:8080" 35 | volumes: 36 | - ${PWD}/var/stubs:/home/wiremock 37 | 38 | -------------------------------------------------------------------------------- /etc/default/staging.env: -------------------------------------------------------------------------------- 1 | ### 2 | # Staging configuration 3 | ### 4 | 5 | # Server port 6 | # Defaults: 3000 7 | #APP_PORT=3000 8 | 9 | # Server base URL 10 | # Defaults: http://localhost:3000 11 | APP_BASE_URL=https://api.nunux.org/keeper 12 | 13 | # Auth server base URL 14 | # Default: none 15 | APP_AUTH_REALM=https://login.nunux.org/auth/realms/nunux-keeper 16 | 17 | # Application logger level 18 | # Defaults: debug 19 | APP_LOG_LEVEL=info 20 | 21 | # Database URI 22 | # Defaults: mongodb://localhost/keeper 23 | APP_DATABASE_URI=mongodb://mongodb/keeper-v2 24 | 25 | # Search engine URI 26 | # Default: elasticsearch://localhost:9200/keeper 27 | APP_SEARCH_ENGINE_URI=elasticsearch://elasticsearch:9200/keeper-v2 28 | 29 | # Redis host 30 | # Defaults: redis://localhost:6379/0 31 | APP_REDIS_URI=redis://redis:6379/6 32 | 33 | # Stats server URI 34 | # Defaults: none 35 | #APP_STATS_SERVER_URI=statsd://telegraf:8125 36 | 37 | # Allow login to auto create users 38 | # Default: false 39 | APP_ALLOW_AUTO_CREATE_USERS=true 40 | 41 | # Allow admin to remove users 42 | # Default: false 43 | #APP_ALLOW_REMOVE_USERS=true 44 | 45 | # Token secret 46 | # Default: random 47 | APP_TOKEN_PUB_KEY=./var/cert/staging-pub.pem 48 | 49 | # Jobs to handle (comma separated list) 50 | # Defaults: all 51 | #APP_JOBS=export-user,import-user,download,ghostbuster,rebuild-index 52 | 53 | # Embedded job worker 54 | # Should not be used in production. 55 | # Defaults: false 56 | #APP_EMBEDDED_WORKER=true 57 | 58 | # Download document's resources 59 | # Default: default 60 | # Values: 61 | # - disabled: no resource downloaded 62 | # - async: async download using queuing system 63 | # - default: direct download 64 | APP_DOWNLOADER=async 65 | 66 | # Storage backend 67 | # Default: local 68 | # Values: 69 | # - local: local file system 70 | # - s3: S3 object storage (UNSTABLE) 71 | #APP_STORAGE=local 72 | 73 | # Local storage directory 74 | # Default: ./storage 75 | APP_STORAGE_LOCAL_DIR=/var/opt/app/storage 76 | 77 | # User agent used by the downloader 78 | # Default: Mozilla/5.0 (compatible; Keeperbot/1.0) 79 | APP_USER_AGENT=Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0 80 | 81 | # Global event brokeer for some app events 82 | # Events: user.create, user.remove 83 | # Defaults: disabled 84 | #APP_EVENT_BROKER_URI=https://test:test@webhook.localnet/keeper-events 85 | 86 | # Initial cleint registration asccess token 87 | # Default: none 88 | #APP_CLIENT_INITIAL_ACCESS_TOKEN=### 89 | 90 | -------------------------------------------------------------------------------- /etc/systemd/system/keeper-core-api.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Nunux Keeper core API service 3 | After=docker.service redis.service mongodb.service elasticsearch.service 4 | Requires=docker.service redis.service mongodb.service elasticsearch.service 5 | 6 | [Install] 7 | WantedBy=multi-user.target 8 | 9 | [Service] 10 | Restart=on-failure 11 | StartLimitInterval=5min 12 | StartLimitBurst=4 13 | OnFailure=unit-status-mail@%n.service 14 | ExecStartPre=-/usr/bin/docker kill %p 15 | ExecStartPre=-/usr/bin/docker rm %p 16 | ExecStart=/usr/bin/docker run --rm --name %p \ 17 | --volume=/var/opt/%p:/var/opt/app \ 18 | --env-file /etc/environment \ 19 | --env-file /etc/default/%p \ 20 | --env-file /etc/default/%p_custom \ 21 | --link mongodb:mongodb \ 22 | --link redis:redis \ 23 | --link elasticsearch:elasticsearch \ 24 | --label traefik.port=3000 \ 25 | --label traefik.frontend.rule=Host:api.nunux.org;PathPrefixStrip:/keeper \ 26 | ncarlier/%p 27 | ExecStop=/usr/bin/docker stop %p 28 | 29 | -------------------------------------------------------------------------------- /etc/systemd/system/keeper-core-job-worker.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Nunux Keeper job worker daemon 3 | After=docker.service 4 | After=docker.service redis.service mongodb.service elasticsearch.service 5 | Requires=docker.service redis.service mongodb.service elasticsearch.service 6 | PartOf=keeper-core-api.service 7 | 8 | [Install] 9 | WantedBy=multi-user.target 10 | 11 | [Service] 12 | Restart=on-failure 13 | StartLimitInterval=5min 14 | StartLimitBurst=4 15 | OnFailure=unit-status-mail@%n.service 16 | ExecStartPre=-/usr/bin/docker kill %p 17 | ExecStartPre=-/usr/bin/docker rm %p 18 | ExecStart=/usr/bin/docker run --rm --name %p \ 19 | --volume=/var/opt/keeper-core-api:/var/opt/app \ 20 | --env-file /etc/environment \ 21 | --env-file /etc/default/keeper-core-api \ 22 | --env-file /etc/default/keeper-core-api_custom \ 23 | --link mongodb:mongodb \ 24 | --link redis:redis \ 25 | --link elasticsearch:elasticsearch \ 26 | --label traefik.enable=false \ 27 | ncarlier/keeper-core-api run job-worker 28 | ExecStop=/usr/bin/docker stop %p 29 | 30 | -------------------------------------------------------------------------------- /etc/systemd/system/keeper-data-backup.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Nunux Keeper backup job 3 | Requires=docker.service 4 | 5 | [Install] 6 | WantedBy=multi-user.target 7 | 8 | [Service] 9 | Nice=19 10 | IOSchedulingClass=2 11 | IOSchedulingPriority=7 12 | ExecStart=/usr/bin/docker run --rm --name %p \ 13 | --entrypoint=/bin/bash \ 14 | --volume /var/opt/backup:/var/opt/backup \ 15 | --volume /var/opt/keeper-core-api:/var/opt/app \ 16 | --env-file /etc/environment \ 17 | --env-file /etc/default/keeper-core-api \ 18 | --env-file /etc/default/keeper-core-api_custom \ 19 | ncarlier/keeper-core-api ./var/scripts/backup-data 20 | -------------------------------------------------------------------------------- /etc/systemd/system/keeper-data-backup.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Daily backup of Nunux Keeper data 3 | 4 | [Timer] 5 | # see systemd.time(7) manual page for other scheduling options 6 | OnCalendar=daily 7 | # run immediately if we missed a backup for some reason 8 | Persistent=true 9 | 10 | [Install] 11 | WantedBy=timers.target 12 | -------------------------------------------------------------------------------- /features/admin.feature: -------------------------------------------------------------------------------- 1 | Feature: Admin API 2 | As an admin user I can access the admin API 3 | 4 | Scenario: Access unauthorized 5 | Given The system is ready 6 | And I am a valid user with the uid "test" 7 | When I get all the users 8 | Then I should be rewarded by a 403 HTTP code 9 | 10 | Scenario: List users 11 | Given I am a valid user with the uid "system" 12 | When I get all the users 13 | Then I should retrieve "system" into the result 14 | 15 | Scenario: Get user details 16 | Given I am a valid user with the uid "system" 17 | When I get data of "system" user 18 | Then I should have "system" into the user uid 19 | 20 | Scenario: Delete user 21 | Given I am a valid user with the uid "system" 22 | When I delete the user "test" 23 | Then I should be rewarded by a 205 HTTP code 24 | When I get all the users 25 | Then I should not retrieve "test" into the result 26 | 27 | Scenario: Delete myself 28 | Given I am a valid user with the uid "system" 29 | When I delete the user "system" 30 | Then I should be rewarded by a 400 HTTP code 31 | 32 | -------------------------------------------------------------------------------- /features/api.feature: -------------------------------------------------------------------------------- 1 | Feature: API 2 | As a anonymous user I can access the base API 3 | 4 | Scenario: Access base API 5 | Given I am an anonymous user 6 | When I access the API "/" 7 | Then I should get the API infos 8 | 9 | Scenario: Access unauthorized API 10 | Given I am an anonymous user 11 | When I access the API "/v2/profiles/current" 12 | Then I should be rewarded by a 401 HTTP code 13 | 14 | -------------------------------------------------------------------------------- /features/client.feature: -------------------------------------------------------------------------------- 1 | Feature: Client API 2 | As a valid user I can use the client API 3 | 4 | Scenario: CRUD operations on clients 5 | Given I am a valid user with the uid "test" 6 | When I create the following client: 7 | | name | test | 8 | | redirectUris | https://httpbin.org | 9 | Then I should retrieve the client 10 | And I should have "test" into the client name 11 | When I update the client with: 12 | | name | updated | 13 | | redirectUris | https://httpbin.org/test | 14 | Then I should retrieve the client 15 | And I should have "updated" into the client name 16 | When I delete the client 17 | Then I should not retrieve the client 18 | 19 | -------------------------------------------------------------------------------- /features/document-attachments.feature: -------------------------------------------------------------------------------- 1 | Feature: Document attachment API 2 | As a valid user I can use the attachment API 3 | 4 | Scenario: Remove document attachment 5 | Given I am a valid user with the uid "test" 6 | When I create the following document: 7 | | files | ./var/assets/oss.png | 8 | Then I should retrieve the document 9 | And I should have "text/html" into the document contentType 10 | And I should have 1 attachment(s) of "image/png" into the document 11 | Then I should retrieve the document 1st attachment 12 | When I delete the document 1st attachment 13 | Then I should retrieve the document 14 | And I should have 0 attachment(s) of "image/png" into the document 15 | When I delete the document 16 | Then I should not retrieve the document 17 | 18 | Scenario: Add document attachment 19 | Given I am a valid user with the uid "test" 20 | When I create the following document: 21 | | files | ./var/assets/oss.png | 22 | Then I should retrieve the document 23 | And I should have "text/html" into the document contentType 24 | And I should have 1 attachment(s) of "image/png" into the document 25 | Then I should retrieve the document 1st attachment 26 | When I add attachment(s) to the document: 27 | | ./var/assets/logo.png | 28 | Then I should retrieve the document 29 | And I should have 2 attachment(s) of "image/png" into the document 30 | Then I should retrieve the document 2nd attachment 31 | When I delete the document 32 | Then I should not retrieve the document 33 | 34 | -------------------------------------------------------------------------------- /features/document-basics.feature: -------------------------------------------------------------------------------- 1 | Feature: Document API 2 | As a valid user I can use the document API 3 | 4 | Scenario: Post a simple text document 5 | Given I am a valid user with the uid "test" 6 | When I create the following document: 7 | | title | Simple text document | 8 | | content | Lorem ipsum dolor sit amet | 9 | | contentType | text/plain; charset=utf-8 | 10 | Then I should retrieve the document 11 | And I should retrieve the raw document 12 | Given I am a valid user with the uid "other" 13 | Then I should not retrieve the document 14 | 15 | Scenario: Update a simple text document 16 | Given I am a valid user with the uid "test" 17 | When I create the following document: 18 | | title | Simple text document | 19 | | content | Lorem ipsum dolor sit amet | 20 | | contentType | text/plain | 21 | And I update the document with: 22 | | title | Simple updated text document | 23 | | content | Updated lorem ipsum... | 24 | Then I should retrieve the document 25 | And I should have "Simple updated text document" into the document title 26 | And I should have "Updated lorem ipsum..." into the document content 27 | 28 | Scenario: Delete a simple text document 29 | Given I am a valid user with the uid "test" 30 | When I create the following document: 31 | | title | Simple text document to delete | 32 | And I delete the document 33 | Then I should not retrieve the document 34 | When I restore the document 35 | Then I should retrieve the document 36 | When I delete the document 37 | Then I should not retrieve the document 38 | 39 | 40 | -------------------------------------------------------------------------------- /features/document-file.feature: -------------------------------------------------------------------------------- 1 | Feature: Document API using file 2 | As a valid user I can use the document API 3 | 4 | Scenario: Upload a text document 5 | Given I am a valid user with the uid "test" 6 | When I create the following document: 7 | | files | ./var/assets/gpl.txt | 8 | Then I should retrieve the document 9 | And I should have "text/plain" into the document contentType 10 | When I delete the document 11 | Then I should not retrieve the document 12 | 13 | Scenario: Upload an image document 14 | Given I am a valid user with the uid "test" 15 | When I create the following document: 16 | | files | ./var/assets/oss.png | 17 | Then I should retrieve the document 18 | And I should have "text/html" into the document contentType 19 | And I should have 1 attachment(s) of "image/png" into the document 20 | Then I should retrieve the document 1st attachment 21 | When I delete the document 22 | Then I should not retrieve the document 23 | 24 | Scenario: Upload multiple file document 25 | Given I am a valid user with the uid "test" 26 | When I create the following document: 27 | | files | ./var/assets/gpl.html | 28 | | files | ./var/assets/oss.png | 29 | Then I should retrieve the document 30 | And I should have "text/html" into the document contentType 31 | And I should have 1 attachment(s) of "image/png" into the document 32 | Then I should retrieve the document 1st attachment 33 | When I delete the document 34 | Then I should not retrieve the document 35 | 36 | -------------------------------------------------------------------------------- /features/document-html.feature: -------------------------------------------------------------------------------- 1 | Feature: Document API with HTML cleanup 2 | As a valid user I can use the document API 3 | 4 | Scenario: Post a not well formated HTML document 5 | Given I am a valid user with the uid "test" 6 | When I create the following html document 7 | """ 8 |

sample

9 | 10 | 11 | test 12 | test 13 | """ 14 | Then I should get the following document content 15 | """ 16 |

sample

17 | 18 | 19 | 20 | test 21 | """ 22 | And I should have "text/html" into the document contentType 23 | And I should have 1 attachment(s) of "image/png" into the document 24 | When I update the document with: 25 | | content |

updated sample

| 26 | Then I should get the following document: 27 | | content |

updated sample

| 28 | And I should have 1 attachment(s) of "image/png" into the document 29 | When I delete the document 30 | Then I should not retrieve the document 31 | 32 | -------------------------------------------------------------------------------- /features/document-search.feature: -------------------------------------------------------------------------------- 1 | Feature: Document search API 2 | As a valid user I can use the document search API 3 | 4 | Scenario: Retrieve a simple text document 5 | Given I am a valid user with the uid "test" 6 | When I create the following document: 7 | | title | Document to find | 8 | | content | With some keywords: foo, bar | 9 | | contentType | text/plain; charset=utf-8 | 10 | Then I should retrieve the document 11 | When I am waiting 1000 ms 12 | And I search documents with: 13 | | q | foo AND bar | 14 | | size | 5 | 15 | Then I should retrieve the document into the search result 16 | When I update the document with: 17 | | content | With some keywords: foo, foo | 18 | And I am waiting 1000 ms 19 | And I search documents with: 20 | | q | foo AND bar | 21 | Then I should not retrieve the document into the search result 22 | When I delete the document 23 | And I am waiting 1000 ms 24 | And I search documents with: 25 | | q | foo AND bar | 26 | Then I should not retrieve the document into the search result 27 | 28 | Scenario: Retrieve a simple html document 29 | Given I am a valid user with the uid "test" 30 | When I create the following html document 31 | """ 32 |

aaa, bbb, ccc

33 | """ 34 | Then I should retrieve the document 35 | When I am waiting 1000 ms 36 | And I search documents with: 37 | | order | desc | 38 | | size | 1 | 39 | Then I should retrieve the document into the search result 40 | When I delete the document 41 | And I am waiting 1000 ms 42 | And I search documents with: 43 | | size | 50 | 44 | Then I should not retrieve the document into the search result 45 | 46 | -------------------------------------------------------------------------------- /features/document-url.feature: -------------------------------------------------------------------------------- 1 | Feature: Document API using url 2 | As a valid user I can use the document API 3 | 4 | Scenario: Post an image URL document 5 | Given I am a valid user with the uid "test" 6 | When I create the following document: 7 | | origin | http://reader.nunux.org/icons/favicon.png?foo=bar | 8 | Then I should retrieve the document 9 | And I should have "text/html" into the document contentType 10 | And I should have "http://reader.nunux.org/icons/favicon.png?foo=bar" into the document origin 11 | And I should have 1 attachment(s) of "image/png" into the document 12 | When I am waiting 1000 ms 13 | Then I should retrieve the document 1st attachment 14 | When I delete the document 15 | Then I should not retrieve the document 16 | 17 | Scenario: Post a HTML URL document 18 | Given I am a valid user with the uid "test" 19 | When I create the following document: 20 | | origin | https://reader.nunux.org | 21 | Then I should retrieve the document 22 | And I should have "text/html; charset=UTF-8" into the document contentType 23 | And I should have "https://reader.nunux.org" into the document origin 24 | When I am waiting 1000 ms 25 | Then I should retrieve the document 1st attachment 26 | When I delete the document 27 | Then I should not retrieve the document 28 | 29 | Scenario: Post a bookmark URL document 30 | Given I am a valid user with the uid "test" 31 | When I create the following document: 32 | | origin | bookmark+https://reader.nunux.org | 33 | Then I should retrieve the document 34 | And I should have "text/html" into the document contentType 35 | And I should have 1 attachment(s) of "image/png" into the document 36 | And I should have "https://reader.nunux.org" into the document origin 37 | When I am waiting 1000 ms 38 | Then I should retrieve the document 1st attachment 39 | When I delete the document 40 | Then I should not retrieve the document 41 | 42 | -------------------------------------------------------------------------------- /features/export.feature: -------------------------------------------------------------------------------- 1 | Feature: Export API 2 | As a valid user I can export my data 3 | 4 | Scenario: Export documents 5 | Given The system is ready 6 | And I am a valid user with the uid "test" 7 | When I create the following document: 8 | | title | Simple text document | 9 | | content | Lorem ipsum dolor sit amet | 10 | | contentType | text/plain; charset=utf-8 | 11 | Then I should retrieve the document 12 | When I shedule an export 13 | Then I should get export status 14 | And I should download the export file 15 | 16 | -------------------------------------------------------------------------------- /features/graveyard.feature: -------------------------------------------------------------------------------- 1 | Feature: Graveyard API 2 | As a valid user I can use the graveyard API 3 | 4 | Scenario: Find ghosts in the graveyard 5 | Given I am a valid user with the uid "test" 6 | When I create the following document: 7 | | title | Simple text document | 8 | | content | Lorem ipsum dolor sit amet | 9 | | contentType | text/plain; charset=utf-8 | 10 | Then I should retrieve the document 11 | When I delete the document 12 | Then I should not retrieve the document 13 | When I am waiting 2000 ms 14 | And I get the graveyard 15 | Then I should retrieve the document into the graveyard 16 | When I delete the document from the graveyard 17 | And I am waiting 1000 ms 18 | And I get the graveyard 19 | Then I should not retrieve the document into the graveyard 20 | 21 | Scenario: Empty the graveyard 22 | Given I am a valid user with the uid "test" 23 | When I create the following document: 24 | | title | Simple text document | 25 | | content | Lorem ipsum dolor sit amet | 26 | | contentType | text/plain; charset=utf-8 | 27 | Then I should retrieve the document 28 | When I delete the document 29 | Then I should not retrieve the document 30 | When I empty the graveyard 31 | And I am waiting 2000 ms 32 | And I get the graveyard 33 | Then I should have no document into the graveyard 34 | 35 | -------------------------------------------------------------------------------- /features/label.feature: -------------------------------------------------------------------------------- 1 | Feature: Label API 2 | As a valid user I can use the label API 3 | 4 | Scenario: Post/Update/Delete/Restore a new label 5 | Given I am a valid user with the uid "test" 6 | When I create the following label: 7 | | label | test | 8 | | color | #f2f2f2 | 9 | And I get my labels 10 | Then I should get the label "test" with "#f2f2f2" as color in my labels 11 | When I update the previous label with value "test_" and color "#f3f3f3" 12 | And I get my labels 13 | Then I should not get the label "test" with "#f2f2f2" as color in my labels 14 | And I should get the label "test_" with "#f3f3f3" as color in my labels 15 | When I delete the previous label 16 | And I get my labels 17 | Then I should not get the label "test_" with "#f3f3f3" as color in my labels 18 | When I restore the previous label 19 | And I get my labels 20 | Then I should get the label "test_" with "#f3f3f3" as color in my labels 21 | When I delete the previous label 22 | And I get my labels 23 | Then I should not get the label "test_" with "#f3f3f3" as color in my labels 24 | 25 | -------------------------------------------------------------------------------- /features/profile.feature: -------------------------------------------------------------------------------- 1 | Feature: Profile API 2 | As a valid user I can use the API 3 | 4 | Scenario: Access my profile 5 | Given I am a valid user with the uid "test" 6 | When I get my profile 7 | Then I should have "test" in my profile uid 8 | 9 | Scenario: Access my profile with bad API key 10 | Given I am a valid user with the uid "test" 11 | And I am using the API key 12 | When I access the API "/v2/profiles/current" 13 | Then I should be rewarded by a 401 HTTP code 14 | 15 | Scenario: Update profile and access profile with good API key 16 | Given I am a valid user with the uid "test" 17 | When I update my profile with: 18 | | resetApiKey | true | 19 | Then I should have an API key in my profile 20 | Given I am using the API key 21 | When I access the API "/v2/profiles/current" 22 | Then I should be rewarded by a 401 HTTP code 23 | When I create the following document: 24 | | title | Simple text document using API key | 25 | | content | Lorem ipsum dolor sit amet | 26 | | contentType | text/plain; charset=utf-8 | 27 | Then I should retrieve the document 28 | 29 | 30 | -------------------------------------------------------------------------------- /features/sharing.feature: -------------------------------------------------------------------------------- 1 | Feature: Sharing API 2 | As a valid user I can use the sharing API 3 | 4 | Scenario: Share a simple text document 5 | Given I am a valid user with the uid "test" 6 | When I create the following label: 7 | | label | shared | 8 | | color | #f2f2f2 | 9 | And I create the following document: 10 | | title | Document to share | 11 | | content | Lorem ipsum dolor sit amet | 12 | | contentType | text/plain; charset=utf-8 | 13 | And I am waiting 1000 ms 14 | And I share the label 15 | Then I should retrieve the sharing 16 | Given I am a valid user with the uid "other" 17 | Then I should not retrieve the document 18 | And I should retrieve the shared label 19 | And I should retrieve the document into the search result 20 | And I should retrieve the shared document 21 | Given I am a valid user with the uid "test" 22 | When I remove the sharing 23 | Then I should not retrieve the sharing 24 | Given I am a valid user with the uid "other" 25 | Then I should not retrieve the document 26 | And I should not retrieve the shared label 27 | And I should not retrieve the shared document 28 | 29 | Scenario: Share a public document 30 | Given I am a valid user with the uid "test" 31 | When I create the following label: 32 | | label | public | 33 | | color | #f2f2f2 | 34 | And I create the following document: 35 | | title | Document to publish | 36 | | content | Lorem ipsum dolor sit amet | 37 | | contentType | text/plain; charset=utf-8 | 38 | And I am waiting 1000 ms 39 | And I share the label 40 | Then I should retrieve the sharing 41 | Given I am an anonymous user 42 | Then I should not retrieve the document 43 | And I should not retrieve the shared label 44 | And I should not retrieve the shared document 45 | And I should not retrieve the public document 46 | Given I am a valid user with the uid "test" 47 | When I update the sharing: 48 | | pub | true | 49 | Then I should retrieve the sharing 50 | Given I am an anonymous user 51 | Then I should not retrieve the shared label 52 | And I should not retrieve the shared document 53 | And I should retrieve the public label 54 | And I should retrieve the public document 55 | And I should retrieve the public feed 56 | 57 | -------------------------------------------------------------------------------- /features/step_definitions/admin_step_definition.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const app = require('../../src/app') 5 | const expect = require('chai').expect 6 | const request = require('supertest') 7 | 8 | const ofAnUserObject = ['id', 'uid', 'date', 'gravatar', 'nbDocuments', 'nbLabels', 'nbSharing', 'storageUsage', '_links'] 9 | 10 | module.exports = function () { 11 | this.When(/^I get all the users$/, function (callback) { 12 | request(app) 13 | .get('/v2/admin/users/') 14 | .use(this.setAuthorizationHeader(this.uid)) 15 | .expect('Content-Type', /json/) 16 | .expect(function (res) { 17 | this.httpStatus = res.status 18 | if (res.status >= 200 && res.status < 300) { 19 | this.myUsers = res.body 20 | } else { 21 | this.myUsers = undefined 22 | } 23 | }.bind(this)) 24 | .end(callback) 25 | }) 26 | 27 | this.When(/^I get data of "([^"]*)" user$/, function (uid, callback) { 28 | request(app) 29 | .get('/v2/admin/users/' + uid) 30 | .set('Content-Type', 'application/json') 31 | .use(this.setAuthorizationHeader(this.uid)) 32 | .expect('Content-Type', /json/) 33 | .expect(function (res) { 34 | expect(res.body).to.contain.all.keys(ofAnUserObject) 35 | this.myUser = res.body 36 | }.bind(this)) 37 | .expect(200, callback) 38 | }) 39 | 40 | this.When(/^I delete the user "([^"]*)"$/, function (uid, callback) { 41 | request(app) 42 | .delete('/v2/admin/users/' + uid) 43 | .set('Content-Type', 'application/json') 44 | .use(this.setAuthorizationHeader(this.uid)) 45 | .expect('Content-Type', /json/) 46 | .expect(function (res) { 47 | this.httpStatus = res.status 48 | }.bind(this)) 49 | .end(callback) 50 | }) 51 | 52 | this.Then(/^I should (not retrieve|retrieve) "([^"]*)" into the result$/, function (get, uid, callback) { 53 | expect(this.myUsers).to.not.be.undefined 54 | const shoulBeRetrieve = get === 'retrieve' 55 | var found = _.find(this.myUsers, function (item) { 56 | expect(item).to.contain.all.keys(ofAnUserObject) 57 | return item.uid === uid 58 | }) 59 | if (shoulBeRetrieve) { 60 | expect(found).to.not.be.undefined 61 | } else { 62 | expect(found).to.be.undefined 63 | } 64 | callback() 65 | }) 66 | 67 | this.Then(/^I should have "([^"]*)" into the user (uid|name)$/, function (value, attr, callback) { 68 | expect(this.myUser).to.not.be.undefined 69 | expect(this.myUser[attr]).to.equals(value) 70 | callback() 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /features/step_definitions/api_step_definition.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const app = require('../../src/app') 4 | const expect = require('chai').expect 5 | const request = require('supertest') 6 | 7 | module.exports = function () { 8 | this.When(/^I access the API "([^"]*)"$/, function (uri, callback) { 9 | request(app) 10 | .get(uri) 11 | .set('Content-Type', 'application/json') 12 | .use(this.setAuthorizationHeader(this.uid, this.apiKey)) 13 | .expect('Content-Type', /json/) 14 | .expect(function (res) { 15 | this.httpBody = res.body 16 | this.httpStatus = res.status 17 | }.bind(this)) 18 | .end(callback) 19 | }) 20 | 21 | this.Then(/^I should get the API infos$/, function (callback) { 22 | expect(this.httpStatus).to.equals(200) 23 | expect(this.httpBody).to.contain.keys('name', 'description', 'version') 24 | callback() 25 | }) 26 | 27 | this.Then(/^I should be rewarded by a (\d+) HTTP code$/, function (code, callback) { 28 | expect(this.httpStatus).to.equals(parseInt(code, 10)) 29 | callback() 30 | }) 31 | 32 | this.Then(/^I should exceed my quota$/, function (callback) { 33 | expect(this.httpStatus).to.equals(403) 34 | expect(this.httpBody).to.contain.key('error') 35 | expect(this.httpBody.error).to.equals('User quota exceeded') 36 | callback() 37 | }) 38 | } 39 | 40 | -------------------------------------------------------------------------------- /features/step_definitions/attachment_step_definition.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const app = require('../../src/app') 4 | const expect = require('chai').expect 5 | const request = require('supertest') 6 | 7 | module.exports = function () { 8 | this.Then(/^I should retrieve the document (\d+)(?:st|nd|rd|th) attachment$/, function (index, callback) { 9 | expect(this.myDocument).to.not.be.undefined 10 | expect(this.myDocument.attachments).to.not.be.undefined 11 | expect(this.myDocument.attachments).to.have.length.of.at.least(index) 12 | const attachment = this.myDocument.attachments[index - 1] 13 | 14 | request(app) 15 | .head('/v2/documents/' + this.myDocument.id + '/files/' + attachment.key) 16 | .use(this.setAuthorizationHeader(this.uid)) 17 | .expect('Content-Type', attachment.contentType) 18 | .expect(200, callback) 19 | }) 20 | 21 | this.Then(/^I should have (\d+) attachment\(s\) of "([^"]*)" into the document$/, function (nb, type, callback) { 22 | expect(this.myDocument).to.not.be.undefined 23 | expect(this.myDocument.attachments).to.not.be.undefined 24 | expect(this.myDocument.attachments).to.have.length(nb) 25 | this.myDocument.attachments.forEach(function (attachment) { 26 | expect(attachment.contentType).to.equal(type) 27 | }) 28 | callback() 29 | }) 30 | 31 | this.When(/^I delete the document (\d+)(?:st|nd|rd|th) attachment$/, function (index, callback) { 32 | expect(this.myDocument).to.not.be.undefined 33 | expect(this.myDocument.attachments).to.not.be.undefined 34 | expect(this.myDocument.attachments).to.have.length.of.at.least(index) 35 | const attachment = this.myDocument.attachments[index - 1] 36 | 37 | request(app) 38 | .delete('/v2/documents/' + this.myDocument.id + '/files/' + attachment.key) 39 | .use(this.setAuthorizationHeader(this.uid)) 40 | .expect(205, callback) 41 | }) 42 | 43 | this.When(/^I add attachment\(s\) to the document:$/, function (attrs, callback) { 44 | expect(this.myDocument).to.not.be.undefined 45 | const req = request(app).post('/v2/documents/' + this.myDocument.id + '/files') 46 | 47 | attrs.raw().forEach(function (attr) { 48 | const file = attr[0] 49 | req.attach('files', file) 50 | }) 51 | 52 | req.use(this.setAuthorizationHeader(this.uid)) 53 | .expect('Content-Type', /json/) 54 | .expect(201, callback) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /features/step_definitions/common_step_definition.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const app = require('../../src/app') 4 | 5 | module.exports = function () { 6 | this.World = require('../support/world.js').World 7 | 8 | this.Given(/^The system is ready$/, {timeout: 10 * 1000}, function (callback) { 9 | app.isReady().then(() => callback(), callback) 10 | }) 11 | 12 | this.Given(/^I am a valid user with the uid "([^"]*)"$/, function (uid, callback) { 13 | this.uid = uid 14 | this.apiKey = null 15 | callback() 16 | }) 17 | 18 | this.Given(/^I am using the API key$/, function (callback) { 19 | this.uid = null 20 | callback() 21 | }) 22 | 23 | this.Given(/^I am an anonymous user$/, function (callback) { 24 | this.uid = null 25 | this.apiKey = null 26 | callback() 27 | }) 28 | 29 | this.When(/^I am waiting (\d+) ms$/, function (wait, callback) { 30 | setTimeout(callback, wait) 31 | }) 32 | } 33 | 34 | -------------------------------------------------------------------------------- /features/step_definitions/export_step_definition.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const app = require('../../src/app') 4 | const expect = require('chai').expect 5 | const request = require('supertest') 6 | 7 | const completeResponse = 'event: complete' 8 | 9 | module.exports = function () { 10 | this.When(/^I shedule an export/, function (callback) { 11 | request(app) 12 | .post('/v2/exports') 13 | .set('Content-Type', 'application/json') 14 | .use(this.setAuthorizationHeader(this.uid)) 15 | .expect('Content-Type', /json/) 16 | .expect((res) => { 17 | expect(res.status).to.equals(202) 18 | }) 19 | .end(callback) 20 | }) 21 | 22 | this.Then(/^I should get export status$/, {timeout: 10 * 1000}, function (callback) { 23 | request(app) 24 | .get('/v2/exports/status') 25 | .set('Content-Type', 'application/json') 26 | .use(this.setAuthorizationHeader(this.uid)) 27 | .expect('Content-Type', 'text/event-stream') 28 | .expect((res) => { 29 | expect(res.status).to.equals(200) 30 | expect(res.text).to.include(completeResponse) 31 | // console.log('RESPONSE TEXT:', res.text) 32 | }) 33 | .end(callback) 34 | }) 35 | 36 | this.Then(/^I should download the export file$/, function (callback) { 37 | request(app) 38 | .head('/v2/exports') 39 | .use(this.setAuthorizationHeader(this.uid)) 40 | .expect('Content-Type', 'application/zip') 41 | .expect(200, callback) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /features/step_definitions/graveyard_step_definition.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const app = require('../../src/app') 5 | const expect = require('chai').expect 6 | const request = require('supertest') 7 | 8 | module.exports = function () { 9 | this.When(/^I empty the graveyard$/, function (callback) { 10 | request(app) 11 | .delete('/v2/graveyard/documents') 12 | .set('Content-Type', 'application/json') 13 | .use(this.setAuthorizationHeader(this.uid)) 14 | .expect(204, callback) 15 | }) 16 | 17 | this.When(/^I get the graveyard$/, function (callback) { 18 | request(app) 19 | .get('/v2/graveyard/documents') 20 | .use(this.setAuthorizationHeader(this.uid)) 21 | .expect('Content-Type', /json/) 22 | .expect(function (res) { 23 | expect(res.status).to.equals(200) 24 | expect(res.body.total).not.to.be.null 25 | this.myGraveyard = res.body 26 | }.bind(this)) 27 | .end(callback) 28 | }) 29 | 30 | this.When(/^I delete the document from the graveyard$/, function (callback) { 31 | expect(this.myDocument).to.not.be.undefined 32 | request(app) 33 | .delete('/v2/graveyard/documents/' + this.myDocument.id) 34 | .use(this.setAuthorizationHeader(this.uid)) 35 | .expect(205, callback) 36 | }) 37 | 38 | this.Then(/^I should (not retrieve|retrieve) the document into the graveyard$/, function (get, callback) { 39 | expect(this.myDocument).to.not.be.undefined 40 | expect(this.myGraveyard).to.not.be.undefined 41 | const shoulBeRetrieve = get === 'retrieve' 42 | const found = _.find(this.myGraveyard.hits, (doc) => { 43 | return doc.id === this.myDocument.id 44 | }) 45 | if (shoulBeRetrieve) { 46 | expect(found).to.not.be.undefined 47 | } else { 48 | expect(found).to.be.undefined 49 | } 50 | callback() 51 | }) 52 | 53 | this.Then(/^I should have no document into the graveyard$/, function (callback) { 54 | expect(this.myGraveyard).to.not.be.undefined 55 | expect(this.myGraveyard.total).to.equals(0) 56 | callback() 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /features/step_definitions/profile_step_definition.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const app = require('../../src/app') 4 | const expect = require('chai').expect 5 | const request = require('supertest') 6 | 7 | const ofAProfileObject = ['id', 'uid', 'date'] 8 | 9 | module.exports = function () { 10 | this.When(/^I get my profile/, function (callback) { 11 | request(app) 12 | .get('/v2/profiles/current?withStats=true') 13 | .set('Content-Type', 'application/json') 14 | .use(this.setAuthorizationHeader(this.uid)) 15 | .expect('Content-Type', /json/) 16 | .expect((res) => { 17 | expect(res.status).to.equals(200) 18 | expect(res.body).to.contain.all.keys(ofAProfileObject) 19 | this.myProfile = res.body 20 | }) 21 | .end(callback) 22 | }) 23 | 24 | this.When(/^I update my profile with:$/, function (attrs, callback) { 25 | const update = {} 26 | attrs.raw().forEach(function (attr) { 27 | const prop = attr[0] 28 | const value = attr[1] 29 | update[prop] = value 30 | }) 31 | request(app) 32 | .put('/v2/profiles/current') 33 | .send(update) 34 | .set('Content-Type', 'application/json') 35 | .use(this.setAuthorizationHeader(this.uid)) 36 | .expect('Content-Type', /json/) 37 | .expect((res) => { 38 | expect(res.status).to.equals(200) 39 | expect(res.body).to.contain.all.keys(ofAProfileObject) 40 | this.myProfile = res.body 41 | if (this.myProfile.apiKey) { 42 | this.apiKey = this.myProfile.apiKey 43 | } 44 | }) 45 | .end(callback) 46 | }) 47 | 48 | this.Then(/^I should have "([^"]*)" in my profile (uid|name)$/, function (value, attr, callback) { 49 | expect(this.myProfile).to.not.be.undefined 50 | expect(this.myProfile[attr]).to.equals(value) 51 | callback() 52 | }) 53 | 54 | this.Then(/^I should have an API key in my profile$/, function (callback) { 55 | expect(this.myProfile).to.not.be.undefined 56 | expect(this.myProfile.apiKey).to.not.be.undefined 57 | callback() 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /features/step_definitions/search_step_definition.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const app = require('../../src/app') 5 | const expect = require('chai').expect 6 | const request = require('supertest') 7 | 8 | module.exports = function () { 9 | this.When(/^I search documents with:$/, function (attrs, callback) { 10 | const query = _.reduce(attrs.raw(), (acc, attr) => { 11 | const prop = attr[0] 12 | const value = attr[1] 13 | acc[prop] = value 14 | return acc 15 | }, {}) 16 | 17 | request(app) 18 | .get('/v2/documents') 19 | .query(query) 20 | .use(this.setAuthorizationHeader(this.uid)) 21 | .expect('Content-Type', /json/) 22 | .expect((res) => { 23 | expect(res.status).to.equals(200) 24 | const docs = res.body 25 | expect(docs.total).not.to.be.null 26 | expect(docs.hits).not.to.be.null 27 | this.myDocuments = docs.hits 28 | }) 29 | .end(callback) 30 | }) 31 | 32 | this.Then(/^I should (not retrieve|retrieve) the document into the search result$/, function (get, callback) { 33 | expect(this.myDocument).to.not.be.undefined 34 | expect(this.myDocuments).to.not.be.undefined 35 | const shoulBeRetrieve = get === 'retrieve' 36 | const found = _.find(this.myDocuments, (doc) => { 37 | return doc.id === this.myDocument.id 38 | }) 39 | if (shoulBeRetrieve) { 40 | expect(found).to.not.be.undefined 41 | } else { 42 | expect(found).to.be.undefined 43 | } 44 | callback() 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /features/support/world.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const jwt = require('jsonwebtoken') 4 | const Chance = require('chance') 5 | 6 | let key = process.env.APP_TOKEN_SECRET || new Chance().hash({length: 16}) 7 | let algorithm = 'HS256' 8 | if (process.env.APP_TOKEN_PRIV_KEY) { 9 | const fs = require('fs') 10 | key = fs.readFileSync(process.env.APP_TOKEN_PRIV_KEY) 11 | algorithm = 'RS256' 12 | } 13 | 14 | function World (/* callback */) { 15 | this.setAuthorizationHeader = function (uid, apiKey) { 16 | if (apiKey) { 17 | return function (request) { 18 | const encodedApiKey = new Buffer(`api:${apiKey}`).toString('base64') 19 | request.set('Authorization', `Basic ${encodedApiKey}`) 20 | return request 21 | } 22 | } 23 | const roles = uid === 'system' ? ['admin'] : [] 24 | const token = uid ? jwt.sign({ 25 | sub: uid, 26 | realm_access: {roles} 27 | }, key, {algorithm}) : null 28 | return function (request) { 29 | if (token) { 30 | request.set('Authorization', `Bearer ${token}`) 31 | } 32 | return request 33 | } 34 | } 35 | 36 | // callback(); // tell Cucumber we're finished and to use 'this' as the world instance 37 | } 38 | 39 | module.exports.World = World 40 | -------------------------------------------------------------------------------- /features/webhook.feature: -------------------------------------------------------------------------------- 1 | Feature: Webhook API 2 | As a valid user I can use the webhook API 3 | 4 | Scenario: CRUD operations on webhooks 5 | Given I am a valid user with the uid "test" 6 | When I create the following webhook: 7 | | url | https://httpbin.org | 8 | | secret | 12345979 | 9 | | active | false | 10 | Then I should retrieve the webhook 11 | And I should have "https://httpbin.org" into the webhook url 12 | When I update the webhook with: 13 | | secret | 987654321 | 14 | Then I should retrieve the webhook 15 | And I should have "987654321" into the webhook secret 16 | When I delete the webhook 17 | Then I should not retrieve the webhook 18 | 19 | Scenario: Search operation on webhooks 20 | Given I am a valid user with the uid "test" 21 | When I create the following webhook: 22 | | url | https://httpbin.org/post?test=1 | 23 | | active | false | 24 | | events | create,update | 25 | | labels | 123,456,789 | 26 | And I create the following webhook: 27 | | url | https://httpbin.org/post?test=3 | 28 | | events | create | 29 | | labels | 123,456,789 | 30 | And I create the following webhook: 31 | | url | https://httpbin.org/post?test=2 | 32 | | events | create,update | 33 | | labels | 123 | 34 | Then I should retrieve 2 active webhook(s) with: 35 | | event | create | 36 | | label | 123 | 37 | Then I should retrieve 1 active webhook(s) with: 38 | | event | update | 39 | Then I should retrieve 0 active webhook(s) with: 40 | | event | update | 41 | | label | 456 | 42 | 43 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gulp = require('gulp') 4 | const cucumber = require('gulp-cucumber') 5 | const eslint = require('gulp-eslint') 6 | const todo = require('gulp-todo') 7 | 8 | // Task to lint the code. 9 | gulp.task('lint', function () { 10 | return gulp.src('src/**/*.js') 11 | .pipe(eslint({useEslintrc: true})) 12 | .pipe(eslint.format()) 13 | .pipe(eslint.failAfterError()) 14 | }) 15 | 16 | // Task to run tests 17 | gulp.task('test', function (done) { 18 | const features = process.env.FEATURES || '*' 19 | gulp.src(`features/${features}.feature`) 20 | .pipe(cucumber({ 21 | steps: 'features/step_definitions/*_step_definition.js', 22 | support: 'features/support/*.js' 23 | })) 24 | .once('error', function (err) { 25 | done(err) 26 | process.kill(process.pid, 'SIGINT') 27 | }) 28 | .once('end', function () { 29 | done() 30 | process.kill(process.pid, 'SIGTERM') 31 | }) 32 | }) 33 | 34 | // Task to generate the TODO file 35 | gulp.task('todo', function () { 36 | gulp.src(['src/**/*.js']) 37 | .pipe(todo()) 38 | .pipe(gulp.dest('./')) 39 | }) 40 | 41 | // Default task. 42 | gulp.task('default', ['lint', 'todo'], function () { 43 | gulp.start('test') 44 | }) 45 | -------------------------------------------------------------------------------- /makefiles/docker/cleanup.Makefile: -------------------------------------------------------------------------------- 1 | .SILENT : 2 | 3 | # Remove dangling Docker images 4 | cleanup: 5 | echo "Removing dangling docker images..." 6 | -docker images -q --filter 'dangling=true' | xargs docker rmi 7 | .PHONY: cleanup 8 | 9 | -------------------------------------------------------------------------------- /makefiles/help.Makefile: -------------------------------------------------------------------------------- 1 | .SILENT: 2 | 3 | ## This help screen 4 | help: 5 | printf "Available targets:\n\n" 6 | awk '/^[a-zA-Z\-\_0-9]+:/ { \ 7 | helpMessage = match(lastLine, /^## (.*)/); \ 8 | if (helpMessage) { \ 9 | helpCommand = substr($$1, 0, index($$1, ":")); \ 10 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 11 | printf "%-15s %s\n", helpCommand, helpMessage; \ 12 | } \ 13 | } \ 14 | { lastLine = $$0 }' $(MAKEFILE_LIST) 15 | .PHONY: help 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Nicolas Carlier ", 3 | "name": "keeper-core-api", 4 | "version": "2.0.0-beta", 5 | "description": "Nunux Keeper core API", 6 | "license": "GPL-3.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "github.com:nunux-keeper/keeper-core-api.git" 10 | }, 11 | "scripts": { 12 | "start": "node --harmony src/cluster", 13 | "debug": "node-inspector & node --harmony --debug src/cluster", 14 | "test": "node --harmony `which gulp`", 15 | "job-worker": "node --harmony src/job/worker", 16 | "job-launcher": "node --harmony src/job/launcher" 17 | }, 18 | "dependencies": { 19 | "JSONStream": "^1.1.3", 20 | "adm-zip": "^0.4.7", 21 | "archiver": "^1.0.0", 22 | "body-parser": "^1.15.2", 23 | "bodybuilder": "^1.24.0", 24 | "bunyan": "^1.8.1", 25 | "chance": "^1.0.3", 26 | "commander": "^2.9.0", 27 | "compression": "^1.6.2", 28 | "cookies": "^0.6.1", 29 | "elasticsearch": "^12.1.3", 30 | "elasticsearch-streams": "0.0.9", 31 | "express": "^4.14.0", 32 | "express-validator": "^2.20.8", 33 | "fs-extra": "^0.30.0", 34 | "gm": "^1.22.0", 35 | "hal": "^0.1.0", 36 | "jsonwebtoken": "^8.0.0", 37 | "knox": "^0.9.2", 38 | "kue": "^0.11.5", 39 | "lodash": "^4.13.1", 40 | "lynx": "^0.2.0", 41 | "method-override": "^2.3.6", 42 | "mime": "^1.3.4", 43 | "mongodb": "^2.2.0", 44 | "multiparty": "^4.1.2", 45 | "newrelic": "^4.0.0", 46 | "node-readability": "^2.2.0", 47 | "redis": "^2.6.2", 48 | "request": "^2.73.0", 49 | "swagger-jsdoc": "^1.8.2", 50 | "webshot": "^0.18.0", 51 | "when": "^3.7.7" 52 | }, 53 | "devDependencies": { 54 | "chai": "^3.5.0", 55 | "cucumber": "^1.2.1", 56 | "eslint-config-standard": "^5.3.1", 57 | "eslint-plugin-promise": "^1.3.2", 58 | "eslint-plugin-standard": "^1.3.2", 59 | "gulp": "^3.9.1", 60 | "gulp-cucumber": "0.0.21", 61 | "gulp-eslint": "^3.0.1", 62 | "gulp-todo": "^4.1.0", 63 | "supertest": "^1.2.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/api/admin.api.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const controller = require('../controller') 4 | const middleware = require('../middleware') 5 | 6 | /** 7 | * Admin API. 8 | */ 9 | module.exports = function (router) { 10 | /** 11 | * @swagger 12 | * /v2/admin/infos: 13 | * get: 14 | * summary: Get server informations and statistics 15 | * tags: 16 | * - Admin 17 | * responses: 18 | * 200: 19 | * description: Success 20 | * schema: 21 | * $ref: "#/definitions/AdminInfos" 22 | * security: 23 | * - authenticated: 24 | * - user 25 | */ 26 | router.get('/admin/infos', middleware.admin.isAdmin, controller.admin.getInfos) 27 | 28 | /** 29 | * @swagger 30 | * /v2/admin/users: 31 | * get: 32 | * summary: Get all users 33 | * tags: 34 | * - Admin 35 | * responses: 36 | * 200: 37 | * description: Success 38 | * schema: 39 | * properties: 40 | * users: 41 | * type: array 42 | * items: 43 | * $ref: "#/definitions/User" 44 | * security: 45 | * - authenticated: 46 | * - user 47 | */ 48 | router.get('/admin/users', middleware.admin.isAdmin, controller.admin.getUsers) 49 | 50 | /** 51 | * @swagger 52 | * /v2/admin/users/{uid}: 53 | * get: 54 | * summary: Get user details 55 | * tags: 56 | * - Admin 57 | * parameters: 58 | * - $ref: '#/parameters/uid' 59 | * responses: 60 | * 200: 61 | * description: Success 62 | * schema: 63 | * $ref: "#/definitions/User" 64 | * security: 65 | * - authenticated: 66 | * - user 67 | */ 68 | router.get('/admin/users/:uid', middleware.admin.isAdmin, controller.admin.getUser) 69 | 70 | /** 71 | * @swagger 72 | * /v2/admin/users/{uid}: 73 | * delete: 74 | * summary: Delete user account 75 | * tags: 76 | * - Admin 77 | * parameters: 78 | * - $ref: '#/parameters/uid' 79 | * responses: 80 | * 205: 81 | * description: Success 82 | * security: 83 | * - authenticated: 84 | * - user 85 | */ 86 | router.delete('/admin/users/:uid', middleware.admin.isAdmin, controller.admin.deleteUser) 87 | } 88 | -------------------------------------------------------------------------------- /src/api/attachment.api.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const controller = require('../controller') 4 | const middleware = require('../middleware') 5 | 6 | /** 7 | * Attachement API. 8 | */ 9 | module.exports = function (router) { 10 | /** 11 | * @swagger 12 | * /v2/documents/{docid}/files/{key}: 13 | * get: 14 | * summary: Download document attachment file 15 | * tags: 16 | * - Document 17 | * - Attachment 18 | * parameters: 19 | * - $ref: '#/parameters/docid' 20 | * - $ref: '#/parameters/key' 21 | * - $ref: '#/parameters/imageSize' 22 | * responses: 23 | * 200: 24 | * description: Success 25 | * schema: 26 | * type: file 27 | * security: 28 | * - authenticated: 29 | * - user 30 | */ 31 | router.get('/documents/:docid/files/:key', middleware.document, controller.attachment.get) 32 | 33 | /** 34 | * @swagger 35 | * /v2/documents/{docid}/files/{key}: 36 | * delete: 37 | * summary: Remove document attachment file 38 | * tags: 39 | * - Document 40 | * - Attachment 41 | * parameters: 42 | * - $ref: '#/parameters/docid' 43 | * - $ref: '#/parameters/key' 44 | * responses: 45 | * 205: 46 | * description: Success 47 | * default: 48 | * description: Unexpected error 49 | * schema: 50 | * $ref: '#/definitions/Error' 51 | * security: 52 | * - authenticated: 53 | * - user 54 | */ 55 | router.delete('/documents/:docid/files/:key', middleware.document, controller.attachment.del) 56 | 57 | /** 58 | * @swagger 59 | * /v2/documents/{docid}/files: 60 | * post: 61 | * summary: Upload document attachment file(s). 62 | * tags: 63 | * - Document 64 | * - Attachment 65 | * consumes: 66 | * - multipart/form-data 67 | * parameters: 68 | * - $ref: '#/parameters/docid' 69 | * - name: body 70 | * description: Attachment files 71 | * in: formData 72 | * required: true 73 | * type: file 74 | * responses: 75 | * 201: 76 | * description: Success 77 | * schema: 78 | * $ref: "#/definitions/Attachment" 79 | * default: 80 | * description: Unexpected error 81 | * schema: 82 | * $ref: '#/definitions/Error' 83 | * security: 84 | * - authenticated: 85 | * - user 86 | */ 87 | router.post('/documents/:docid/files', middleware.document, controller.attachment.post) 88 | } 89 | -------------------------------------------------------------------------------- /src/api/export.api.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const controller = require('../controller') 4 | 5 | /** 6 | * Export API. 7 | */ 8 | module.exports = function (router) { 9 | /** 10 | * @swagger 11 | * /v2/exports: 12 | * get: 13 | * summary: Download export file 14 | * tags: 15 | * - Export 16 | * responses: 17 | * 200: 18 | * description: Success 19 | * schema: 20 | * type: file 21 | * security: 22 | * - authenticated: 23 | * - user 24 | */ 25 | router.get('/exports', controller.exports.download) 26 | 27 | /** 28 | * @swagger 29 | * /v2/exports: 30 | * post: 31 | * summary: Schedule an export of all documents 32 | * tags: 33 | * - Export 34 | * responses: 35 | * 202: 36 | * description: Success 37 | * security: 38 | * - authenticated: 39 | * - user 40 | */ 41 | router.post('/exports', controller.exports.schedule) 42 | 43 | /** 44 | * @swagger 45 | * /v2/exports/status: 46 | * get: 47 | * summary: Get export status 48 | * description: | 49 | * The response is an event stream of the export job. 50 | * 51 | * example: 'data: {progress: 20, data: {}}' 52 | * tags: 53 | * - Export 54 | * produces: 55 | * - text/event-stream 56 | * responses: 57 | * 200: 58 | * description: Success 59 | * security: 60 | * - authenticated: 61 | * - user 62 | */ 63 | router.get('/exports/status', controller.exports.getStatus) 64 | } 65 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const logger = require('../helper').logger 5 | const express = require('express') 6 | 7 | const router = express.Router() 8 | 9 | // Dynamic loading API... 10 | require('fs').readdirSync(__dirname).forEach((file) => { 11 | if (/^[a-z-]+\.api\.js$/.test(file)) { 12 | const name = path.basename(file, '.api.js') 13 | logger.debug('Loading %s API...', name) 14 | require(path.join(__dirname, file))(router) 15 | } 16 | }) 17 | 18 | /** 19 | * API. 20 | */ 21 | module.exports = router 22 | -------------------------------------------------------------------------------- /src/api/info.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const express = require('express') 4 | const controller = require('../controller') 5 | 6 | /** 7 | * Info API. 8 | */ 9 | module.exports = function () { 10 | const router = express.Router() 11 | 12 | /** 13 | * @swagger 14 | * /: 15 | * get: 16 | * summary: Get API informations 17 | * tags: 18 | * - API informations 19 | * responses: 20 | * 200: 21 | * description: Success 22 | * schema: 23 | * $ref: '#/definitions/Info' 24 | */ 25 | router.get('/', controller.info.get) 26 | 27 | return router 28 | } 29 | -------------------------------------------------------------------------------- /src/api/profile.api.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const controller = require('../controller') 4 | 5 | /** 6 | * Profile API. 7 | */ 8 | module.exports = function (router) { 9 | /** 10 | * @swagger 11 | * /v2/profiles/current: 12 | * get: 13 | * summary: Get current profile informations 14 | * tags: 15 | * - Profile 16 | * parameters: 17 | * - name: withStats 18 | * description: Add profile statistics 19 | * in: query 20 | * required: false 21 | * type: boolean 22 | * responses: 23 | * 200: 24 | * description: Success 25 | * schema: 26 | * $ref: "#/definitions/Profile" 27 | * security: 28 | * - authenticated: 29 | * - user 30 | */ 31 | router.get('/profiles/current', controller.profile.get) 32 | 33 | /** 34 | * @swagger 35 | * /v2/profiles/current: 36 | * put: 37 | * summary: Update current profile informations 38 | * tags: 39 | * - Profile 40 | * parameters: 41 | * - name: body 42 | * description: Profile values to update 43 | * in: body 44 | * required: true 45 | * schema: 46 | * $ref: '#/definitions/ProfilePayload' 47 | * responses: 48 | * 200: 49 | * description: Success 50 | * schema: 51 | * $ref: "#/definitions/Profile" 52 | * security: 53 | * - authenticated: 54 | * - user 55 | */ 56 | router.put('/profiles/current', controller.profile.update) 57 | } 58 | -------------------------------------------------------------------------------- /src/api/public.api.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const controller = require('../controller') 4 | const middleware = require('../middleware') 5 | 6 | /** 7 | * Public API. 8 | */ 9 | module.exports = function (router) { 10 | /** 11 | * @swagger 12 | * /v2/public/{sid}: 13 | * get: 14 | * summary: Get public documents. 15 | * tags: 16 | * - Sharing 17 | * parameters: 18 | * - $ref: '#/parameters/sid' 19 | * - $ref: '#/parameters/q' 20 | * - $ref: '#/parameters/from' 21 | * - $ref: '#/parameters/size' 22 | * - $ref: '#/parameters/order' 23 | * - $ref: '#/parameters/output' 24 | * responses: 25 | * 200: 26 | * description: Success 27 | * schema: 28 | * $ref: "#/definitions/SearchResult" 29 | */ 30 | router.get('/public/:sid', 31 | middleware.sharing.get, 32 | middleware.sharing.assertPublic, 33 | controller.sharing.getDocuments) 34 | 35 | /** 36 | * @swagger 37 | * /v2/public/{sid}/{docid}: 38 | * get: 39 | * summary: Get public document 40 | * tags: 41 | * - Sharing 42 | * parameters: 43 | * - $ref: '#/parameters/sid' 44 | * - $ref: '#/parameters/docid' 45 | * responses: 46 | * 202: 47 | * description: Success 48 | * schema: 49 | * $ref: '#/definitions/Document' 50 | */ 51 | router.get('/public/:sid/:docid', 52 | middleware.sharing.get, 53 | middleware.sharing.assertPublic, 54 | middleware.document, 55 | controller.sharing.getDocument) 56 | 57 | /** 58 | * @swagger 59 | * /v2/public/{sid}/{docid}/files/{key}: 60 | * get: 61 | * summary: Get public document's files 62 | * tags: 63 | * - Sharing 64 | * - Document 65 | * - Attachment 66 | * parameters: 67 | * - $ref: '#/parameters/sid' 68 | * - $ref: '#/parameters/docid' 69 | * - $ref: '#/parameters/key' 70 | * - $ref: '#/parameters/imageSize' 71 | * responses: 72 | * 200: 73 | * description: Success 74 | * schema: 75 | * type: file 76 | */ 77 | router.get('/public/:sid/:docid/files/:key', 78 | middleware.sharing.get, 79 | middleware.sharing.assertPublic, 80 | middleware.document, 81 | controller.attachment.get) 82 | } 83 | -------------------------------------------------------------------------------- /src/api/swagger/tags.yaml: -------------------------------------------------------------------------------- 1 | tags: 2 | - name: API informations 3 | - name: Profile 4 | - name: Label 5 | - name: Document 6 | - name: Attachment 7 | - name: Sharing 8 | - name: Graveyard 9 | - name: Webhook 10 | - name: Client 11 | - name: Admin 12 | -------------------------------------------------------------------------------- /src/cluster.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | /** 6 | * Keeper core API 7 | * Copyright (c) 2015 @ncarlier 8 | * All Rights Reserved. 9 | */ 10 | 11 | process.title = 'keeper-core-api' 12 | 13 | const cluster = require('cluster') 14 | const logger = require('./helper').logger 15 | const globals = require('./helper').globals 16 | 17 | if (globals.ENV === 'production' && cluster.isMaster) { 18 | const numWorkers = require('os').cpus().length 19 | 20 | logger.debug(`Master cluster setting up ${numWorkers} workers...`) 21 | 22 | for (let i = 0; i < numWorkers; i++) { 23 | cluster.fork() 24 | } 25 | 26 | cluster.on('online', (worker) => { 27 | logger.debug(`Worker ${worker.process.pid} is online`) 28 | }) 29 | 30 | cluster.on('exit', (worker, code, signal) => { 31 | logger.debug(`Worker ${worker.process.pid}' died with code: ${code}, and signal: ${signal}`) 32 | logger.debug('Starting a new worker') 33 | cluster.fork() 34 | }) 35 | } else { 36 | const app = require('./app') 37 | app.isReady().then(() => { 38 | app.listen(app.get('port'), function () { 39 | logger.info( 40 | '%s web server listening on port %s (%s mode)', 41 | globals.NAME, 42 | app.get('port'), 43 | globals.ENV 44 | ) 45 | }) 46 | }) 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/controller/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../helper').logger 4 | const path = require('path') 5 | 6 | // Dynamic loading Controllers... 7 | const controllers = {} 8 | require('fs').readdirSync(__dirname).forEach((file) => { 9 | if (/^[a-z_]+\.ctrl\.js$/.test(file)) { 10 | const name = path.basename(file, '.ctrl.js') 11 | logger.debug('Loading %s controller...', name) 12 | controllers[name] = require(path.join(__dirname, file)) 13 | } 14 | }) 15 | 16 | module.exports = controllers 17 | -------------------------------------------------------------------------------- /src/controller/info.ctrl.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hal = require('hal') 4 | const monitoringService = require('../service').monitoring 5 | const globals = require('../helper').globals 6 | const urlConfig = require('../helper').urlConfig 7 | 8 | module.exports = { 9 | /** 10 | * Get API informations. 11 | */ 12 | get: function (req, res, next) { 13 | monitoringService.monitor() 14 | .then(function (ok) { 15 | const resource = new hal.Resource({ 16 | name: globals.NAME, 17 | description: globals.DESCRIPTION, 18 | version: globals.VERSION, 19 | apiVersion: urlConfig.apiVersion.substring(1), 20 | env: globals.ENV 21 | }, urlConfig.baseUrl) 22 | resource.link('documentation', urlConfig.resolve('/api-docs/', true)) 23 | resource.link('documentation.json', urlConfig.resolve('/api-docs.json', true)) 24 | if (globals.AUTH_REALM) { 25 | resource.link('auth-realm', globals.AUTH_REALM) 26 | } 27 | res.status(ok ? 200 : 503).json(resource) 28 | }, next) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/controller/profile.ctrl.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require('crypto') 4 | const errors = require('../helper').errors 5 | const logger = require('../helper').logger 6 | const decorator = require('../decorator') 7 | const userService = require('../service').user 8 | 9 | module.exports = { 10 | /** 11 | * Get current profile data. 12 | */ 13 | get: function (req, res/*, next*/) { 14 | req.sanitizeQuery('withStats').toBoolean() 15 | const decorators = [ 16 | decorator.profile.privacy(), 17 | decorator.profile.hash(), 18 | decorator.profile.hal() 19 | ] 20 | if (req.query.withStats) { 21 | decorators.push(decorator.profile.stats()) 22 | } 23 | decorator.decorate( 24 | req.user, 25 | ...decorators 26 | ) 27 | .then((resource) => { 28 | res.json(resource) 29 | }) 30 | }, 31 | 32 | /** 33 | * Update current profile data. 34 | */ 35 | update: function (req, res, next) { 36 | req.sanitizeBody('resetApiKey').toBoolean() 37 | const validationErrors = req.validationErrors(true) 38 | if (validationErrors) { 39 | return next(new errors.BadRequest(null, validationErrors)) 40 | } 41 | 42 | const update = {} 43 | 44 | if (req.body.resetApiKey) { 45 | logger.info('Reset user API key: %j', req.user.uid) 46 | update.apiKey = crypto.randomBytes(20).toString('hex') 47 | } 48 | 49 | userService.update(req.user, update) 50 | .then((user) => { 51 | return decorator.decorate( 52 | req.user, 53 | decorator.profile.privacy(), 54 | decorator.profile.hash(), 55 | decorator.profile.hal() 56 | ) 57 | }) 58 | .then(function (resource) { 59 | if (req.body.resetApiKey) { 60 | resource.apiKey = update.apiKey 61 | } 62 | res.json(resource) 63 | }, next) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/dao/elasticsearch/client.dao.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AbstractElasticsearchDao = require('./common/abstract.dao') 4 | 5 | /** 6 | * Client DAO. 7 | * @module client.dao 8 | */ 9 | class ClientDao extends AbstractElasticsearchDao { 10 | constructor (client, index) { 11 | super(client, index, 'client') 12 | } 13 | 14 | getMapping () { 15 | return { 16 | properties: { 17 | name: {type: 'string', store: 'yes', index: 'not_analyzed'}, 18 | clientId: {type: 'string', store: 'yes', index: 'not_analyzed'}, 19 | secret: {type: 'string', store: 'yes', index: 'not_analyzed'}, 20 | redirectUris: {type: 'string', store: 'yes', index: 'not_analyzed'}, 21 | webOrigins: {type: 'string', store: 'yes', index: 'not_analyzed'}, 22 | registrationAccessToken: {type: 'string', store: 'yes', index: 'not_analyzed'}, 23 | owner: {type: 'string', store: 'yes', index: 'not_analyzed'}, 24 | cdate: {type: 'date', store: 'yes', format: 'date_optional_time'}, 25 | mdate: {type: 'date', store: 'yes', format: 'date_optional_time'} 26 | } 27 | } 28 | } 29 | } 30 | 31 | module.exports = ClientDao 32 | 33 | -------------------------------------------------------------------------------- /src/dao/elasticsearch/common/query-builder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../../../helper').logger 4 | const Bodybuilder = require('bodybuilder') 5 | 6 | /** 7 | * User DAO. 8 | * @module user.dao 9 | */ 10 | class QueryBuilder { 11 | constructor () { 12 | this.body = new Bodybuilder() 13 | } 14 | 15 | size (size) { 16 | if (size) { 17 | this.body.size(size) 18 | } 19 | return this 20 | } 21 | 22 | from (from) { 23 | if (from) { 24 | this.body.from(from) 25 | } 26 | return this 27 | } 28 | 29 | sort (field, order = 'desc') { 30 | if (field) { 31 | this.body.sort(field, order) 32 | } 33 | return this 34 | } 35 | 36 | fields (fields) { 37 | if (fields) { 38 | this.body.rawOption('_source', fields) 39 | } 40 | return this 41 | } 42 | 43 | terms (terms) { 44 | if (terms) { 45 | Object.keys(terms).forEach((t) => { 46 | this.body.query('term', t, terms[t]) 47 | }) 48 | } 49 | return this 50 | } 51 | 52 | filters (filters) { 53 | if (filters) { 54 | Object.keys(filters).forEach((f) => { 55 | this.body.filter('term', f, filters[f]) 56 | }) 57 | } 58 | return this 59 | } 60 | 61 | fulltext (q, fields) { 62 | if (q) { 63 | if (fields) { 64 | this.body.query('query_string', fields, q) 65 | } else { 66 | this.body.query('query_string', q) 67 | } 68 | } 69 | return this 70 | } 71 | 72 | debug (debug = false) { 73 | if (debug) { 74 | logger.debug('Builded query:', JSON.stringify(this.build())) 75 | console.log('QUERY', JSON.stringify(this.build(), null, 2)) 76 | } 77 | return this 78 | } 79 | 80 | build () { 81 | const result = this.body.build('v2') 82 | if (!result.query) { 83 | result.query = {match_all: {}} 84 | } 85 | return result 86 | } 87 | } 88 | 89 | module.exports = QueryBuilder 90 | 91 | -------------------------------------------------------------------------------- /src/dao/elasticsearch/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const url = require('url') 4 | const path = require('path') 5 | const logger = require('../../helper').logger 6 | const globals = require('../../helper').globals 7 | const elasticsearch = require('elasticsearch') 8 | 9 | module.exports = function (uri) { 10 | const useAsMainDatabaseEngine = uri === globals.DATABASE_URI 11 | const u = url.parse(uri) 12 | const daos = {} 13 | const indexName = u.pathname.substring(1) 14 | const client = new elasticsearch.Client({ 15 | host: u.host 16 | }) 17 | 18 | daos.shutdown = function () { 19 | // NoOp shudown 20 | return Promise.resolve() 21 | } 22 | 23 | // Dynamic loading DAOs... 24 | const daosToConfigure = require('fs').readdirSync(__dirname).reduce((acc, file) => { 25 | if (/^[a-z_]+\.dao\.js$/.test(file)) { 26 | const name = path.basename(file, '.dao.js') 27 | if (!useAsMainDatabaseEngine && name !== 'document') { 28 | // Skip other DAO if not use as main database engine 29 | return acc 30 | } 31 | logger.debug('Loading %s ElasticSearch DAO..', name) 32 | const Dao = require(path.join(__dirname, file)) 33 | daos[name] = new Dao(client, indexName, useAsMainDatabaseEngine) 34 | acc.push(daos[name]) 35 | } 36 | return acc 37 | }, []) 38 | 39 | const configured = client.indices.exists({ 40 | index: indexName 41 | }).then(function (exists) { 42 | if (!exists) { 43 | logger.debug('Creating index %s ...', indexName) 44 | return client.indices.create({ 45 | index: indexName 46 | }).then(function (r) { 47 | logger.info('Index %s created:', indexName, r) 48 | return Promise.resolve(r) 49 | }) 50 | } else { 51 | logger.debug('Index %s exists', indexName) 52 | return Promise.resolve() 53 | } 54 | }).catch(function (err) { 55 | logger.error('Unable to create index index %s ...', indexName, err) 56 | return Promise.reject(err) 57 | }) 58 | 59 | daos.isReady = () => configured.then(() => { 60 | if (!daosToConfigure) { 61 | return Promise.resolve() 62 | } 63 | return Promise.all(daosToConfigure.map((dao) => dao.configure())) 64 | }) 65 | 66 | return daos 67 | } 68 | -------------------------------------------------------------------------------- /src/dao/elasticsearch/label.dao.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AbstractElasticsearchDao = require('./common/abstract.dao') 4 | 5 | /** 6 | * Label DAO. 7 | * @module label.dao 8 | */ 9 | class LabelDao extends AbstractElasticsearchDao { 10 | constructor (client, index) { 11 | super(client, index, 'label') 12 | } 13 | 14 | getMapping () { 15 | return { 16 | properties: { 17 | label : {type: 'string', store: 'yes', index: 'not_analyzed'}, 18 | color : {type: 'string', store: 'yes', index: 'not_analyzed'}, 19 | owner : {type: 'string', store: 'yes', index: 'not_analyzed'}, 20 | ghost : {type: 'boolean', store: 'yes'}, 21 | sharing : {type: 'string', store: 'yes', index: 'not_analyzed'}, 22 | date : {type: 'date', store: 'yes', format: 'date_optional_time'} 23 | } 24 | } 25 | } 26 | } 27 | 28 | module.exports = LabelDao 29 | 30 | -------------------------------------------------------------------------------- /src/dao/elasticsearch/sharing.dao.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AbstractElasticsearchDao = require('./common/abstract.dao') 4 | 5 | /** 6 | * Sharing DAO. 7 | * @module sharing.dao 8 | */ 9 | class SharingDao extends AbstractElasticsearchDao { 10 | constructor (client, index) { 11 | super(client, index, 'sharing') 12 | } 13 | 14 | getMapping () { 15 | return { 16 | properties: { 17 | owner: {type: 'string', store: 'yes', index: 'not_analyzed'}, 18 | targetLabel: {type: 'string', store: 'yes', index: 'not_analyzed'}, 19 | pub: {type: 'boolean', store: 'yes'}, 20 | date : {type: 'date', store: 'yes', format: 'date_optional_time'}, 21 | startDate : {type: 'date', store: 'yes', format: 'date_optional_time'}, 22 | endDate : {type: 'date', store: 'yes', format: 'date_optional_time'} 23 | } 24 | } 25 | } 26 | } 27 | 28 | module.exports = SharingDao 29 | 30 | -------------------------------------------------------------------------------- /src/dao/elasticsearch/user.dao.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AbstractElasticsearchDao = require('./common/abstract.dao') 4 | 5 | /** 6 | * User DAO. 7 | * @module user.dao 8 | */ 9 | class UserDao extends AbstractElasticsearchDao { 10 | constructor (client, index) { 11 | super(client, index, 'user') 12 | } 13 | 14 | getMapping () { 15 | return { 16 | properties: { 17 | ip: {type: 'string', store: 'yes', index: 'not_analyzed'}, 18 | uid: {type: 'string', store: 'yes', index: 'not_analyzed'}, 19 | apiKey: {type: 'string', store: 'yes', index: 'not_analyzed'}, 20 | name: {type: 'string', store: 'yes', index: 'not_analyzed'}, 21 | email: {type: 'string', store: 'yes', index: 'not_analyzed'}, 22 | exportRequest: {type: 'integer', store: 'yes', index: 'not_analyzed'}, 23 | date: {type: 'date', store: 'yes', format: 'date_optional_time'} 24 | } 25 | } 26 | } 27 | 28 | /** 29 | * Find user by its UID. 30 | * @param {String} uid UID. 31 | * @return {Object} the user 32 | */ 33 | findByUid (uid) { 34 | return this.find({uid: uid}, {size: 1}).then((users) => { 35 | const user = users.length ? users[0] : null 36 | return Promise.resolve(user) 37 | }) 38 | } 39 | 40 | /** 41 | * Find user by its API key. 42 | * @param {String} key API key. 43 | * @return {Object} the user 44 | */ 45 | findByApiKey (key) { 46 | return this.find({apiKey: key}, {size: 1}).then((users) => { 47 | const user = users.length ? users[0] : null 48 | return Promise.resolve(user) 49 | }) 50 | } 51 | } 52 | 53 | module.exports = UserDao 54 | 55 | -------------------------------------------------------------------------------- /src/dao/elasticsearch/webhook.dao.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AbstractElasticsearchDao = require('./common/abstract.dao') 4 | 5 | /** 6 | * Webhook DAO. 7 | * @module webhook.dao 8 | */ 9 | class WebhookDao extends AbstractElasticsearchDao { 10 | constructor (client, index) { 11 | super(client, index, 'webhook') 12 | } 13 | 14 | getMapping () { 15 | return { 16 | properties: { 17 | url : {type: 'string', store: 'yes', index: 'not_analyzed'}, 18 | secret : {type: 'string', store: 'yes', index: 'not_analyzed'}, 19 | events : {type: 'string', store: 'yes', index: 'not_analyzed'}, 20 | labels : {type: 'string', store: 'yes', index: 'not_analyzed'}, 21 | active : {type: 'boolean', store: 'yes'}, 22 | owner : {type: 'string', store: 'yes', index: 'not_analyzed'}, 23 | cdate : {type: 'date', store: 'yes', format: 'date_optional_time'}, 24 | mdate : {type: 'date', store: 'yes', format: 'date_optional_time'} 25 | } 26 | } 27 | } 28 | } 29 | 30 | module.exports = WebhookDao 31 | 32 | -------------------------------------------------------------------------------- /src/dao/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const url = require('url') 4 | const globals = require('../helper').globals 5 | 6 | const provider = url.parse(globals.DATABASE_URI).protocol.slice(0, -1) 7 | 8 | module.exports = require(`./${provider}`)(globals.DATABASE_URI) 9 | 10 | -------------------------------------------------------------------------------- /src/dao/mongodb/client.dao.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AbstractMongodbDao = require('./abstract') 4 | 5 | /** 6 | * Client DAO. 7 | * @module client.dao 8 | */ 9 | class ClientDao extends AbstractMongodbDao { 10 | constructor (client) { 11 | super(client, 'client') 12 | } 13 | } 14 | 15 | module.exports = ClientDao 16 | -------------------------------------------------------------------------------- /src/dao/mongodb/document.dao.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AbstractMongodbDao = require('./abstract') 4 | const SearchEngine = require('../searchengine') 5 | 6 | /** 7 | * Document DAO. 8 | * @module document.dao 9 | */ 10 | class DocumentDao extends AbstractMongodbDao { 11 | constructor (client) { 12 | super(client, 'document') 13 | } 14 | 15 | /** 16 | * Search documents. 17 | * @param {Object} query Search query. 18 | * @param {Object} params Search params. 19 | * @return {Array} the documents 20 | */ 21 | search (query, params) { 22 | // Delegate search to the searchengine. 23 | return SearchEngine.search(query, params) 24 | } 25 | } 26 | 27 | module.exports = DocumentDao 28 | 29 | -------------------------------------------------------------------------------- /src/dao/mongodb/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const logger = require('../../helper').logger 5 | const MongoClient = require('mongodb').MongoClient 6 | 7 | module.exports = function (uri) { 8 | const daos = {} 9 | const client = MongoClient.connect(uri) 10 | .then((db) => { 11 | logger.info('MongodDB connection success') 12 | db.on('close', function () { 13 | logger.info('MongodDB connection close') 14 | }) 15 | db.on('error', function (err) { 16 | logger.error('MongodDB error', err) 17 | }) 18 | db.on('timeout', function (err) { 19 | logger.error('MongodDB timeout', err) 20 | throw err 21 | }) 22 | 23 | return Promise.resolve(db) 24 | }) 25 | .catch((err) => { 26 | logger.fatal('MongodDB connection error!') 27 | throw err 28 | }) 29 | 30 | daos.shutdown = function () { 31 | logger.debug('Closing MongodDB connections...') 32 | return client.then((db) => db.close()) 33 | } 34 | 35 | // Dynamic loading DAOs... 36 | const daosToConfigure = require('fs').readdirSync(__dirname).reduce((acc, file) => { 37 | if (/^[a-z_]+\.dao\.js$/.test(file)) { 38 | const name = path.basename(file, '.dao.js') 39 | logger.debug('Loading %s MongoDB DAO..', name) 40 | const Dao = require(path.join(__dirname, file)) 41 | daos[name] = new Dao(client) 42 | acc.push(daos[name]) 43 | } 44 | return acc 45 | }, []) 46 | 47 | daos.isReady = () => client.then((db) => { 48 | logger.debug('Configuring %s database...', db.databaseName) 49 | if (!daosToConfigure) { 50 | return Promise.resolve() 51 | } 52 | return Promise.all(daosToConfigure.map((dao) => dao.configure())) 53 | }) 54 | 55 | return daos 56 | } 57 | -------------------------------------------------------------------------------- /src/dao/mongodb/label.dao.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AbstractMongodbDao = require('./abstract') 4 | 5 | /** 6 | * Label DAO. 7 | * @module label.dao 8 | */ 9 | class LabelDao extends AbstractMongodbDao { 10 | constructor (client) { 11 | super(client, 'label') 12 | } 13 | } 14 | 15 | module.exports = LabelDao 16 | -------------------------------------------------------------------------------- /src/dao/mongodb/sharing.dao.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AbstractMongodbDao = require('./abstract') 4 | 5 | /** 6 | * Sharing DAO. 7 | * @module sharng.dao 8 | */ 9 | class SharingDao extends AbstractMongodbDao { 10 | constructor (client) { 11 | super(client, 'sharing') 12 | } 13 | } 14 | 15 | module.exports = SharingDao 16 | -------------------------------------------------------------------------------- /src/dao/mongodb/user.dao.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AbstractMongodbDao = require('./abstract') 4 | const logger = require('../../helper').logger 5 | 6 | /** 7 | * User DAO. 8 | * @module user.dao 9 | */ 10 | class UserDao extends AbstractMongodbDao { 11 | constructor (client) { 12 | super(client, 'user') 13 | } 14 | 15 | configure () { 16 | // Creating unique constraint for UID attribute... 17 | return this.getCollection().then((collection) => { 18 | logger.debug('Configuring collection: %s ...', this.collection) 19 | return collection.createIndex({uid:1}, {unique: true}) 20 | }) 21 | } 22 | 23 | /** 24 | * Find user by its UID. 25 | * @param {String} uid UID. 26 | * @return {Object} the user 27 | */ 28 | findByUid (uid) { 29 | return this.getCollection().then((collection) => { 30 | return collection.findOne({uid: uid}).then((user) => { 31 | // logger.debug('findByUid::user', user) 32 | return Promise.resolve(this.objectMapper(user)) 33 | }) 34 | }) 35 | } 36 | 37 | /** 38 | * Find user by its API key. 39 | * @param {String} key API key. 40 | * @return {Object} the user 41 | */ 42 | findByApiKey (key) { 43 | return this.getCollection().then((collection) => { 44 | return collection.findOne({apiKey: key}).then((user) => { 45 | // logger.debug('findByApiKey::key', key) 46 | return Promise.resolve(this.objectMapper(user)) 47 | }) 48 | }) 49 | } 50 | } 51 | 52 | module.exports = UserDao 53 | 54 | -------------------------------------------------------------------------------- /src/dao/mongodb/webhook.dao.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AbstractMongodbDao = require('./abstract') 4 | 5 | /** 6 | * Webhook DAO. 7 | * @module webhook.dao 8 | */ 9 | class WebhookDao extends AbstractMongodbDao { 10 | constructor (client) { 11 | super(client, 'webhook') 12 | } 13 | } 14 | 15 | module.exports = WebhookDao 16 | -------------------------------------------------------------------------------- /src/decorator/client.decorator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const hal = require('hal') 5 | const urlConfig = require('../helper').urlConfig 6 | 7 | /** 8 | * Add HAL data into the client. 9 | * @param {Object} client Client DTO 10 | * @return {Promise} promise of the dto 11 | */ 12 | const decorateWithHalData = function (client) { 13 | const resource = new hal.Resource(client, urlConfig.resolve(`/clients/${client.id}`)) 14 | resource.link('all', urlConfig.resolve('/clients')) 15 | return Promise.resolve(resource) 16 | } 17 | 18 | /** 19 | * Remove private data from client. 20 | * @param {Object} client Client DTO 21 | * @return {Promise} promise of the dto 22 | */ 23 | const decorateWithoutPrivateData = function (client) { 24 | return Promise.resolve(_.omit(client, 'registrationAccessToken')) 25 | } 26 | 27 | module.exports = { 28 | /** 29 | * Decorate client DTO by removing private datas. 30 | * @return {Function} decorator function 31 | */ 32 | privacy: function () { 33 | return decorateWithoutPrivateData 34 | }, 35 | 36 | /** 37 | * Decorate client DTO with HAL data. 38 | * @return {Function} decorator function 39 | */ 40 | hal: function () { 41 | return decorateWithHalData 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/decorator/decorator.stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const decorator = require('.') 4 | const Transform = require('stream').Transform 5 | const util = require('util') 6 | 7 | const DecoratorStream = function (decorators) { 8 | this.decorators = decorators 9 | Transform.call(this, {objectMode: true}) 10 | } 11 | 12 | util.inherits(DecoratorStream, Transform) 13 | 14 | DecoratorStream.prototype._transform = function (chunk, encoding, callback) { 15 | // console.log('DecoratorStream._transform::chunk', chunk) 16 | decorator.decorate(chunk, ...this.decorators) 17 | .then((obj) => { 18 | // console.log('DecoratorStream._transform::obj', obj) 19 | this.push(obj) 20 | callback() 21 | }) 22 | .catch(callback) 23 | } 24 | 25 | module.exports = DecoratorStream 26 | -------------------------------------------------------------------------------- /src/decorator/document.decorator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const hal = require('hal') 5 | const urlConfig = require('../helper').urlConfig 6 | 7 | /** 8 | * Remove private data from document. 9 | * @param {Object} sharing Sharing DTO 10 | * @return {Promise} promise of the dto 11 | */ 12 | const decorateWithoutPrivateData = function (doc) { 13 | return Promise.resolve(_.omit(doc, 'owner', 'ghost', 'labels')) 14 | } 15 | 16 | /** 17 | * Add HAL data into the document. 18 | * @param {Object} sharing Sharing DTO 19 | * @return {Promise} promise of the dto 20 | */ 21 | const decorateWithHalData = function (doc) { 22 | const resource = new hal.Resource(doc, urlConfig.resolve(`/documents/${doc.id}`)) 23 | resource.link('search', urlConfig.resolve('/documents')) 24 | resource.link('raw', urlConfig.resolve(`/documents/${doc.id}?raw`)) 25 | return Promise.resolve(resource) 26 | } 27 | 28 | module.exports = { 29 | /** 30 | * Decorate document DTO by removing private datas. 31 | * @return {Function} decorator function 32 | */ 33 | privacy: function () { 34 | return decorateWithoutPrivateData 35 | }, 36 | 37 | /** 38 | * Decorate document DTO with HAL data. 39 | * @return {Function} decorator function 40 | */ 41 | hal: function () { 42 | return decorateWithHalData 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/decorator/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../helper').logger 4 | const path = require('path') 5 | const pipeline = require('when/pipeline') 6 | 7 | // Dynamic loading Decorators... 8 | const decorators = {} 9 | require('fs').readdirSync(__dirname).forEach((file) => { 10 | if (/^[a-z_]+\.decorator\.js$/.test(file)) { 11 | var name = path.basename(file, '.decorator.js') 12 | logger.debug('Loading %s decorator...', name) 13 | decorators[name] = require(path.join(__dirname, file)) 14 | } 15 | }) 16 | 17 | /** 18 | * Decorate an object with suplied decorators. 19 | * @param {Object} obj Object to decorate 20 | * @param {Function[]} ... Decorator functions 21 | * @return {Promise} promise of the decoration 22 | */ 23 | decorators.decorate = function (obj, ...decorators) { 24 | if (!decorators || decorators.length === 0) { 25 | return Promise.resolve(obj) 26 | } 27 | return pipeline(decorators, obj) 28 | } 29 | 30 | module.exports = decorators 31 | -------------------------------------------------------------------------------- /src/decorator/label.decorator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hal = require('hal') 4 | const urlConfig = require('../helper').urlConfig 5 | 6 | /** 7 | * Add HAL data into the label. 8 | * @param {Object} label Label DTO 9 | * @return {Promise} promise of the dto 10 | */ 11 | const decorateWithHalData = function (label) { 12 | const resource = new hal.Resource(label, urlConfig.resolve(`/labels/${label.id}`)) 13 | resource.link('all', urlConfig.resolve('/labels')) 14 | return Promise.resolve(resource) 15 | } 16 | 17 | module.exports = { 18 | /** 19 | * Decorate label DTO with HAL data. 20 | * @return {Function} decorator function 21 | */ 22 | hal: function () { 23 | return decorateWithHalData 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/decorator/sharing.decorator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hal = require('hal') 4 | const urlConfig = require('../helper').urlConfig 5 | 6 | /** 7 | * Add HAL data into the sharing. 8 | * @param {Object} sharing Sharing DTO 9 | * @return {Promise} promise of the dto 10 | */ 11 | const decorateWithHalData = function (sharing) { 12 | const resource = new hal.Resource(sharing, urlConfig.resolve(`/labels/${sharing.targetLabel}/sharing`)) 13 | resource.link('all', urlConfig.resolve('/sharing')) 14 | return Promise.resolve(resource) 15 | } 16 | 17 | module.exports = { 18 | /** 19 | * Decorate sharing DTO with HAL data. 20 | * @return {Function} decorator function 21 | */ 22 | hal: function () { 23 | return decorateWithHalData 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/decorator/webhook.decorator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hal = require('hal') 4 | const urlConfig = require('../helper').urlConfig 5 | 6 | /** 7 | * Add HAL data into the webhook. 8 | * @param {Object} webhook Webhook DTO 9 | * @return {Promise} promise of the dto 10 | */ 11 | const decorateWithHalData = function (webhook) { 12 | const resource = new hal.Resource(webhook, urlConfig.resolve(`/webhooks/${webhook.id}`)) 13 | resource.link('all', urlConfig.resolve('/webhooks')) 14 | return Promise.resolve(resource) 15 | } 16 | 17 | module.exports = { 18 | /** 19 | * Decorate webhook DTO with HAL data. 20 | * @return {Function} decorator function 21 | */ 22 | hal: function () { 23 | return decorateWithHalData 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/event-broker/client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const url = require('url') 4 | const logger = require('../helper').logger 5 | const globals = require('../helper').globals 6 | 7 | /** 8 | * Event broker client. 9 | * @module event-broker/client 10 | */ 11 | class EventBrokerClient { 12 | /** 13 | * Constructor. 14 | */ 15 | constructor () { 16 | // Disable the client if the feature is not configured. 17 | this.disabled = !globals.EVENT_BROKER_URI 18 | if (this.disabled) { 19 | logger.debug('No event broker configured.') 20 | return 21 | } 22 | let providerName = url.parse(globals.EVENT_BROKER_URI).protocol.slice(0, -1) 23 | providerName = providerName.replace(/s$/, '') 24 | this.provider = require(`./${providerName}`)(globals.EVENT_BROKER_URI) 25 | } 26 | 27 | /** 28 | * Emit event. 29 | * @param {String} topic Event topic 30 | * @param {Object} payload Event payload 31 | */ 32 | emit (topic, payload) { 33 | if (this.disabled) { 34 | return 35 | } 36 | this.provider.emit(topic, payload) 37 | } 38 | } 39 | 40 | module.exports = new EventBrokerClient() 41 | -------------------------------------------------------------------------------- /src/event-broker/http/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const request = require('request') 4 | const logger = require('../../helper').logger 5 | 6 | module.exports = function (uri) { 7 | logger.debug('Loading HTTP provider for event broker...') 8 | 9 | const client = { 10 | emit: function (topic, payload) { 11 | logger.debug(`Emitting event ${topic}...`, payload) 12 | request({ 13 | url: uri, 14 | method: 'POST', 15 | json: {topic, payload} 16 | }, function (err, res, body) { 17 | if (err || res.statusCode > 299) { 18 | logger.error('Unable to emit event to the HTTP broker', err || body) 19 | } else { 20 | logger.debug(`Event ${topic} emited`, payload) 21 | } 22 | }) 23 | } 24 | } 25 | 26 | return client 27 | } 28 | -------------------------------------------------------------------------------- /src/event/document/attachment.evt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../../helper').logger 4 | const storage = require('../../storage') 5 | const downloadService = require('../../service/download.service') 6 | 7 | /** 8 | * Download document's attachments. 9 | */ 10 | const downloadAttachments = function (doc) { 11 | const attachments = doc.attachments.filter(attachment => attachment.origin) 12 | if (attachments && attachments.length) { 13 | logger.debug('Downloading document attachments...', doc.id) 14 | downloadService.download(attachments, storage.getContainerName(doc.owner, 'documents', doc.id, 'files')) 15 | } 16 | } 17 | 18 | /** 19 | * Synchronize document's attachments. 20 | */ 21 | const synchronizeAttachments = function (doc) { 22 | logger.debug('Synchronizing document attachments...', doc.id) 23 | const container = storage.getContainerName(doc.owner, 'documents', doc.id, 'files') 24 | storage.cleanContainer(container, doc.attachments) 25 | .then(function () { 26 | downloadAttachments(doc) 27 | }) 28 | .catch(function (err) { 29 | logger.error('Error while synchronizing attachments', doc.id, err) 30 | }) 31 | } 32 | 33 | /** 34 | * Remove document's attachments. 35 | */ 36 | /* 37 | const removeAttachments = function(doc) { 38 | logger.debug('Removing document attachments...', doc.id); 39 | storage.remove(storage.getContainerName(doc.owner, 'documents', doc.id)); 40 | }; 41 | */ 42 | 43 | /** 44 | * Document event handler. 45 | */ 46 | module.exports = function (documentEventHandler) { 47 | documentEventHandler.on('create', downloadAttachments) 48 | documentEventHandler.on('update', synchronizeAttachments) 49 | // documentEventHandler.on('remove', removeAttachments) 50 | // Let's the action to the ghostbuster 51 | } 52 | -------------------------------------------------------------------------------- /src/event/document/indexation.evt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const searchengine = require('../../dao/searchengine') 4 | 5 | /** 6 | * Document event handler. 7 | */ 8 | module.exports = function (documentEventHandler) { 9 | // Exit if disabled... 10 | if (searchengine.disabled) { 11 | return 12 | } 13 | 14 | documentEventHandler.on('create', (doc) => searchengine.indexDocument(doc)) 15 | documentEventHandler.on('update', (doc) => searchengine.reindexDocument(doc)) 16 | documentEventHandler.on('remove', (doc) => searchengine.reindexDocument(doc)) 17 | documentEventHandler.on('restore', (doc) => searchengine.reindexDocument(doc)) 18 | documentEventHandler.on('destroy', (doc) => searchengine.unindexDocument(doc)) 19 | } 20 | -------------------------------------------------------------------------------- /src/event/document/metrics.evt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const metrics = require('../../metrics/client') 4 | 5 | /** 6 | * Document event handler. 7 | */ 8 | module.exports = function (documentEventHandler) { 9 | // Exit if disabled... 10 | if (metrics.disabled) { 11 | return 12 | } 13 | 14 | documentEventHandler.on('fetch', (evt) => { 15 | metrics.increment(`document_event,action=fetch,id=${evt.doc.id},owner=${evt.doc.owner},viewer=${evt.viewer}`) 16 | }) 17 | 18 | documentEventHandler.on('create', (doc) => { 19 | metrics.increment(`document_event,action=create,owner=${doc.owner}`) 20 | }) 21 | 22 | documentEventHandler.on('update', (doc) => { 23 | metrics.increment(`document_event,action=update,owner=${doc.owner}`) 24 | }) 25 | 26 | documentEventHandler.on('remove', (doc) => { 27 | metrics.increment(`document_event,action=remove,owner=${doc.owner}`) 28 | }) 29 | 30 | documentEventHandler.on('restore', (doc) => { 31 | metrics.increment(`document_event,action=restore,owner=${doc.owner}`) 32 | }) 33 | 34 | documentEventHandler.on('destroy', (doc) => { 35 | metrics.increment(`document_event,action=destroy,owner=${doc.owner}`) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/event/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const logger = require('../helper').logger 6 | const EventEmitter = require('events').EventEmitter 7 | 8 | // Dynamic loading Event handlers... 9 | const eventHandlers = {} 10 | fs.readdirSync(__dirname).forEach((event) => { 11 | const dir = path.join(__dirname, event) 12 | if (fs.statSync(dir).isDirectory()) { 13 | eventHandlers[event] = new EventEmitter() 14 | 15 | fs.readdirSync(dir).forEach((file) => { 16 | if (/^[a-z_]+\.evt\.js$/.test(file)) { 17 | const name = path.basename(file, '.evt.js') 18 | logger.debug('Loading %s event handler for %s events...', name, event) 19 | require(path.join(dir, file))(eventHandlers[event]) 20 | } 21 | }) 22 | } 23 | }) 24 | 25 | module.exports = eventHandlers 26 | -------------------------------------------------------------------------------- /src/event/label/metrics.evt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const metrics = require('../../metrics/client') 4 | 5 | /** 6 | * Label event handler. 7 | */ 8 | module.exports = function (labelEventHandler) { 9 | // Exit if disabled... 10 | if (metrics.disabled) { 11 | return 12 | } 13 | 14 | labelEventHandler.on('create', (label) => { 15 | metrics.increment(`label_event,action=create,owner=${label.owner}`) 16 | }) 17 | 18 | labelEventHandler.on('update', (label) => { 19 | metrics.increment(`label_event,action=update,owner=${label.owner}`) 20 | }) 21 | 22 | labelEventHandler.on('remove', (label) => { 23 | metrics.increment(`label_event,action=remove,owner=${label.owner}`) 24 | }) 25 | 26 | labelEventHandler.on('restore', (label) => { 27 | metrics.increment(`label_event,action=restore,owner=${label.owner}`) 28 | }) 29 | 30 | labelEventHandler.on('destroy', (label) => { 31 | metrics.increment(`label_event,action=destroy,owner=${label.owner}`) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/event/sharing/metrics.evt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const metrics = require('../../metrics/client') 4 | 5 | /** 6 | * Sharing event handler. 7 | */ 8 | module.exports = function (sharingEventHandler) { 9 | // Exit if disabled... 10 | if (metrics.disabled) { 11 | return 12 | } 13 | 14 | sharingEventHandler.on('create', (sharing) => { 15 | metrics.increment(`sharing_event,action=create,owner=${sharing.owner}`) 16 | }) 17 | 18 | sharingEventHandler.on('update', (sharing) => { 19 | metrics.increment(`sharing_event,action=update,owner=${sharing.owner}`) 20 | }) 21 | 22 | sharingEventHandler.on('remove', (sharing) => { 23 | metrics.increment(`sharing_event,action=remove,owner=${sharing.owner}`) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/event/user/broker.evt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const broker = require('../../event-broker/client') 4 | 5 | /** 6 | * User event handler. 7 | */ 8 | module.exports = function (userEventHandler) { 9 | // Exit if disabled... 10 | if (broker.disabled) { 11 | return 12 | } 13 | 14 | userEventHandler.on('create', (user) => { 15 | broker.emit('user.create', user) 16 | }) 17 | 18 | userEventHandler.on('remove', (user) => { 19 | broker.emit('user.remove', user) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/event/user/metrics.evt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const metrics = require('../../metrics/client') 4 | 5 | /** 6 | * User event handler. 7 | */ 8 | module.exports = function (userEventHandler) { 9 | // Exit if disabled... 10 | if (metrics.disabled) { 11 | return 12 | } 13 | 14 | userEventHandler.on('create', (user) => { 15 | metrics.increment(`user_event,action=create,uid=${user.uid}`) 16 | }) 17 | 18 | userEventHandler.on('update', (user) => { 19 | metrics.increment(`user_event,action=update,uid=${user.uid}`) 20 | }) 21 | 22 | userEventHandler.on('remove', (user) => { 23 | metrics.increment(`user_event,action=remove,uid=${user.uid}`) 24 | }) 25 | 26 | userEventHandler.on('unauthorized', (user) => { 27 | metrics.increment(`user_event,action=login-failed,uid=${user.uid}`) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/event/webhook/metrics.evt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const metrics = require('../../metrics/client') 4 | 5 | /** 6 | * Webhook event handler. 7 | */ 8 | module.exports = function (webhookEventHandler) { 9 | // Exit if disabled... 10 | if (metrics.disabled) { 11 | return 12 | } 13 | 14 | webhookEventHandler.on('create', (webhook) => { 15 | metrics.increment(`webhook_event,action=create,owner=${webhook.owner},id=${webhook.id}`) 16 | }) 17 | 18 | webhookEventHandler.on('update', (webhook) => { 19 | metrics.increment(`webhook_event,action=update,owner=${webhook.owner},id=${webhook.id}`) 20 | }) 21 | 22 | webhookEventHandler.on('remove', (webhook) => { 23 | metrics.increment(`webhook_event,action=remove,owner=${webhook.owner},id=${webhook.id}`) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/extractor/content.extractor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const path = require('path') 5 | const logger = require('../helper').logger 6 | 7 | // Load content extractors 8 | const contentExtractors = {} 9 | require('fs').readdirSync(path.join(__dirname, 'content')).forEach((file) => { 10 | if (/^[a-z_]+\.extractor\.js$/.test(file)) { 11 | const name = path.basename(file, '.extractor.js') 12 | logger.debug('Loading %s content extractor..', name) 13 | contentExtractors[name] = require(path.join(__dirname, 'content', file)) 14 | } 15 | }) 16 | 17 | const extractContent = function (doc, meta) { 18 | const extractor = _.find(contentExtractors, function (ext) { 19 | return ext.support(doc) 20 | }) 21 | if (extractor) { 22 | return extractor.extract(doc, meta) 23 | } else { 24 | logger.debug('No content extractor found for content type: %s. Using none.', doc.contentType) 25 | return Promise.resolve(doc) 26 | } 27 | } 28 | 29 | /** 30 | *Content extractor. 31 | * @module content.extractor 32 | */ 33 | module.exports = { 34 | /** 35 | * Extract content of a document. 36 | * @param {Document} doc 37 | * @return {Promise} Promise of the document with extracted content. 38 | */ 39 | extract: function (doc) { 40 | const textAttachment = doc.attachments.find(att => /^text\//.test(att.contentType)) 41 | if (textAttachment) { 42 | // The attachment is the doc 43 | logger.debug('Extracting content from the attachment...', textAttachment.key) 44 | doc.contentType = textAttachment.contentType 45 | return new Promise(function (resolve, reject) { 46 | const bufs = [] 47 | textAttachment.stream.on('data', function (d) { bufs.push(d) }) 48 | textAttachment.stream.on('error', reject) 49 | textAttachment.stream.on('end', function () { 50 | doc.content = Buffer.concat(bufs).toString() 51 | _.remove(doc.attachments, function (att) { 52 | if (att.key === textAttachment.key) { 53 | logger.debug('Removing content attachment...', textAttachment.key) 54 | return true 55 | } 56 | return false 57 | }) 58 | // We assume that the content is a full web page so we ask to the extractor 59 | // to try to detect the main content. 60 | extractContent(doc, {detectMainContent: true}).then(resolve, reject) 61 | }) 62 | }) 63 | } else { 64 | logger.debug('Extracting content form the document...') 65 | return extractContent(doc, {detectMainContent: false}) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/extractor/file.extractor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const hash = require('../helper').hash 5 | const logger = require('../helper').logger 6 | 7 | /** 8 | * Add attachment. 9 | * @param {Document} doc The document 10 | * @param {File} file The file to add as attachment 11 | * @return {Document} update doc. 12 | */ 13 | var addAttachment = function (doc, file) { 14 | logger.debug('Add file attachment %s to document...', file.originalFilename) 15 | doc.attachments.push({ 16 | key: hash.hashFilename(file.originalFilename), 17 | stream: fs.createReadStream(file.path), 18 | contentType: file.headers['content-type'] 19 | }) 20 | return doc 21 | } 22 | 23 | /** 24 | * File content extractor. 25 | * @module file 26 | */ 27 | module.exports = { 28 | /** 29 | * Extract uploaded content of a document. 30 | * @param {Document} doc 31 | * @return {Promise} Promise of the document with extracted content. 32 | */ 33 | extract: function (doc) { 34 | if (doc.files) { 35 | logger.debug('Using File extractor.') 36 | doc.files.forEach(function (file) { 37 | addAttachment(doc, file) 38 | }) 39 | delete doc.files 40 | } 41 | return Promise.resolve(doc) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/extractor/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../helper').logger 4 | const path = require('path') 5 | 6 | // Dynamic loading extractors... 7 | const extractors = {} 8 | require('fs').readdirSync(__dirname).forEach((file) => { 9 | if (/^[a-z_]+\.extractor\.js$/.test(file)) { 10 | const name = path.basename(file, '.extractor.js') 11 | logger.debug('Loading %s extractor..', name) 12 | extractors[name] = require(path.join(__dirname, file)) 13 | } 14 | }) 15 | 16 | module.exports = extractors 17 | 18 | -------------------------------------------------------------------------------- /src/extractor/url.extractor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const hash = require('../helper').hash 5 | const logger = require('../helper').logger 6 | const request = require('../helper').request 7 | 8 | // Load URL extractors 9 | const urlExtractors = {} 10 | require('fs').readdirSync(path.join(__dirname, 'url')).forEach((file) => { 11 | if (/^[a-z_]+\.extractor\.js$/.test(file)) { 12 | const name = path.basename(file, '.extractor.js') 13 | logger.debug('Loading %s URL extractor..', name) 14 | urlExtractors[name] = require(path.join(__dirname, 'url', file)) 15 | } 16 | }) 17 | 18 | const defaultExtractor = { 19 | extract: function (doc) { 20 | logger.debug('Using default URL extractor.') 21 | return new Promise(function (resolve, reject) { 22 | request.head(doc.origin, function (err, res) { 23 | if (err) { 24 | return reject(err) 25 | } 26 | 27 | doc.attachments.push({ 28 | key: hash.hashUrl(doc.origin), 29 | stream: request.get(doc.origin), 30 | contentType: res.headers['content-type'], 31 | contentLength: res.headers['content-length'], 32 | origin: doc.origin 33 | }) 34 | 35 | resolve(doc) 36 | }) 37 | }) 38 | } 39 | } 40 | 41 | /** 42 | * URL content extractor. 43 | * @module url 44 | */ 45 | module.exports = { 46 | /** 47 | * Extract online content of a document. 48 | * Redirect to proper extractor regarding content-type. 49 | * If content-type is not supported, document is return as is. 50 | * @param {Document} doc 51 | * @return {Promise} Promise of the document with extracted content. 52 | */ 53 | extract: function (doc) { 54 | if (doc.origin && !doc.content) { 55 | let extractor = null 56 | for (let ext in urlExtractors) { 57 | if (urlExtractors[ext].detect(doc)) { 58 | extractor = urlExtractors[ext] 59 | break 60 | } 61 | } 62 | if (extractor) { 63 | return extractor.extract(doc) 64 | } else { 65 | return defaultExtractor.extract(doc) 66 | } 67 | } else { 68 | return Promise.resolve(doc) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/extractor/url/bookmark.extractor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const validator = require('validator') 5 | const logger = require('../../helper').logger 6 | const errors = require('../../helper').errors 7 | const hash = require('../../helper').hash 8 | const thumbnail = require('../../helper').thumbnail 9 | const request = require('../../helper').request 10 | 11 | /** 12 | * Bookmark extractor. 13 | * @module url 14 | */ 15 | module.exports = { 16 | /** 17 | * Extract thumbnail of an online HTML document. 18 | * @param {Document} doc 19 | * @return {Promise} Promise of the document with extracted content. 20 | */ 21 | extract: function (doc) { 22 | logger.debug('Using Bookmark extractor.') 23 | doc.origin = doc.origin.substring(9) 24 | if (!validator.isURL(doc.origin)) { 25 | return Promise.reject(new errors.BadRequest('URL not valid: ' + doc.origin)) 26 | } 27 | 28 | return new Promise(function (resolve, reject) { 29 | request.head(doc.origin, function (err, res) { 30 | if (err) { 31 | return reject(err) 32 | } 33 | const contentType = res.headers['content-type'] 34 | if (!/text\/html/.test(contentType)) { 35 | return reject(new errors.BadRequest('Target document is not a regular HTML page.')) 36 | } 37 | return thumbnail.page(doc.origin) 38 | .then(function (thumbnailFile) { 39 | logger.debug('Page thumbnailed: ' + thumbnailFile) 40 | if (!doc.title) { 41 | doc.title = doc.origin.replace(/.*?:\/\//g, '') 42 | } 43 | doc.contentType = 'text/html' 44 | const attachment = { 45 | key: hash.hashUrl(doc.origin, 'png'), 46 | stream: fs.createReadStream(thumbnailFile), 47 | contentType: 'image/png' 48 | } 49 | doc.attachments.push(attachment) 50 | doc.content = `` 51 | return Promise.resolve(doc) 52 | }) 53 | .then(resolve, reject) 54 | }) 55 | }) 56 | }, 57 | 58 | /** 59 | * Detect if the document origin is a cwbookmarkDailymotion URL. 60 | * @param {Document} doc 61 | * @return {Boolean} True if the URL is a bookmark 62 | */ 63 | detect: function (doc) { 64 | return doc.origin.lastIndexOf('bookmark+http', 0) === 0 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/extractor/url/dailymotion.extractor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../../helper').logger 4 | const url = require('url') 5 | 6 | /** 7 | * Dailymotion URL content extractor. 8 | * @module youtube 9 | */ 10 | module.exports = { 11 | /** 12 | * Extract content of Dailymotion URL. 13 | * @param {Document} doc 14 | * @return {Promise} Promise of the document with extracted content. 15 | */ 16 | extract: function (doc) { 17 | logger.debug('Using Dailymotion URL extractor.') 18 | 19 | const u = url.parse(doc.origin) 20 | const v = u.pathname.split('/')[2] 21 | doc.content = `` 23 | doc.title = 'Dailymotion video: ' + v 24 | doc.contentType = 'text/html' 25 | return Promise.resolve(doc) 26 | }, 27 | 28 | /** 29 | * Detect if the document origin is a Dailymotion URL. 30 | * @param {Document} doc 31 | * @return {Boolean} True if the URL is from Dailymotion. 32 | */ 33 | detect: function (doc) { 34 | return doc.origin.lastIndexOf('http://www.dailymotion.com/video/', 0) === 0 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/extractor/url/vimeo.extractor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../../helper').logger 4 | const url = require('url') 5 | 6 | /** 7 | * Vimeo URL content extractor. 8 | * @module vimeo 9 | */ 10 | module.exports = { 11 | /** 12 | * Extract content of Vimeo URL. 13 | * @param {Document} doc 14 | * @return {Promise} Promise of the document with extracted content. 15 | */ 16 | extract: function (doc) { 17 | logger.debug('Using Vimeo URL extractor.') 18 | 19 | const u = url.parse(doc.origin) 20 | const v = u.pathname.split('/')[1] 21 | doc.content = `` 24 | doc.title = 'Vimeo video: ' + v 25 | doc.contentType = 'text/html' 26 | return Promise.resolve(doc) 27 | }, 28 | 29 | /** 30 | * Detect if the document origin is a Vimeo URL. 31 | * @param {Document} doc 32 | * @return {Boolean} True if the URL is from Vimeo. 33 | */ 34 | detect: function (doc) { 35 | return doc.origin.lastIndexOf('http://vimeo.com', 0) === 0 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/extractor/url/youtube.extractor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../../helper').logger 4 | const url = require('url') 5 | 6 | /** 7 | * Youtube URL content extractor. 8 | * @module youtube 9 | */ 10 | module.exports = { 11 | /** 12 | * Extract content of Youtube URL. 13 | * @param {Document} doc 14 | * @return {Promise} Promise of the document with extracted content. 15 | */ 16 | extract: function (doc) { 17 | logger.debug('Using Youtube URL extractor.') 18 | 19 | const u = url.parse(doc.origin, true) 20 | const v = u.query.v 21 | doc.content = `` 24 | doc.title = 'Youtube video: ' + v 25 | doc.contentType = 'text/html' 26 | return Promise.resolve(doc) 27 | }, 28 | 29 | /** 30 | * Detect if the document origin is a Youtube URL. 31 | * @param {Document} doc 32 | * @return {Boolean} True if the URL is from Youtube. 33 | */ 34 | detect: function (doc) { 35 | return doc.origin.lastIndexOf('https://www.youtube.com/watch', 0) === 0 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/helper/errors.js: -------------------------------------------------------------------------------- 1 | /*eslint no-caller: "error"*/ 2 | 3 | const errorBuilder = function (code, defaultMessage) { 4 | return function (msg, meta) { 5 | this.status = code 6 | this.meta = meta 7 | this.message = msg || defaultMessage 8 | Error.call(this, this.message) 9 | // Error.captureStackTrace(this, arguments.callee) 10 | } 11 | } 12 | 13 | module.exports = { 14 | BadRequest: errorBuilder(400, 'Bad request'), 15 | Unauthorized: errorBuilder(401, 'Unauthorized'), 16 | Forbidden: errorBuilder(403, 'Forbidden'), 17 | NotFound: errorBuilder(404, 'Not found'), 18 | InternalError: errorBuilder(500, 'Internal Server Error') 19 | } 20 | -------------------------------------------------------------------------------- /src/helper/globals.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Chance = require('chance') 4 | const pkg = require('../../package.json') 5 | 6 | /** 7 | * Globals variables. 8 | * @module globals 9 | */ 10 | const globals = { 11 | // App name 12 | NAME: pkg.name, 13 | // App description 14 | DESCRIPTION: pkg.description, 15 | // App version 16 | VERSION: pkg.version, 17 | // App env 18 | ENV: process.env.NODE_ENV || 'development', 19 | // Auth realm 20 | AUTH_REALM: process.env.APP_AUTH_REALM, 21 | // Database URI 22 | DATABASE_URI: process.env.APP_DATABASE_URI || 'mongodb://mongodb/keeper', 23 | // Search engine URI 24 | SEARCH_ENGINE_URI: process.env.APP_SEARCH_ENGINE_URI || 'elasticsearch://elasticsearch/keeper', 25 | // Stats server URI 26 | STATS_SERVER_URI: process.env.APP_STATS_SERVER_URI || false, 27 | // Event broker URI 28 | EVENT_BROKER_URI: process.env.APP_EVENT_BROKER_URI || false, 29 | // Secret use to encypt token 30 | TOKEN_SECRET: process.env.APP_TOKEN_SECRET || new Chance().hash({length: 16}), 31 | // Secret use to encypt token 32 | TOKEN_PUB_KEY: process.env.APP_TOKEN_PUB_KEY, 33 | // Allow login to auto create users 34 | ALLOW_AUTO_CREATE_USERS: process.env.APP_ALLOW_AUTO_CREATE_USERS === 'true', 35 | // Allow admin to remove users 36 | ALLOW_REMOVE_USERS: process.env.APP_ALLOW_REMOVE_USERS === 'true', 37 | // Use embedded worker 38 | EMBEDDED_WORKER: process.env.APP_EMBEDDED_WORKER === 'true', 39 | // Initial client registration access token 40 | CLIENT_INITIAL_ACCESS_TOKEN: process.env.APP_CLIENT_INITIAL_ACCESS_TOKEN, 41 | // Export format (internal, portable) 42 | EXPORT_FORMAT: process.env.APP_EXPORT_FORMAT || 'internal' 43 | } 44 | 45 | module.exports = globals 46 | -------------------------------------------------------------------------------- /src/helper/hash.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require('crypto') 4 | 5 | /** 6 | * Get a hash of a value. 7 | * @param {String} value 8 | * @returns {String} hash 9 | */ 10 | const getHash = function (value) { 11 | return crypto.createHash('md5').update(value).digest('hex') 12 | } 13 | 14 | /** 15 | * Get a hashed name. 16 | * The name can be a file name or an url. 17 | * @param {String} name Name to has 18 | * @param {String} forcedExt Forced extension to set 19 | * @returns {String} hash 20 | */ 21 | const getHashName = function (name, forcedExt) { 22 | // Clean query if URL 23 | const cleanName = name.replace(/\?.*$/, '') 24 | let ext = forcedExt 25 | if (!ext) { 26 | // Try to extract extension 27 | ext = cleanName.split('.').pop() 28 | if (ext) { 29 | ext = ext.match(/^[a-zA-Z0-9]+/)[0] 30 | } 31 | } 32 | // Return hash 33 | return getHash(cleanName) + (ext ? '.' + ext : '') 34 | } 35 | 36 | /** 37 | * Hash helper. 38 | * @module hash 39 | */ 40 | module.exports = { 41 | hash: getHash, 42 | hashUrl: getHashName, 43 | hashFilename: getHashName 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/helper/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Helpers. 5 | * @module helpers 6 | */ 7 | module.exports = { 8 | errors: require('./errors'), 9 | files: require('./files'), 10 | globals: require('./globals'), 11 | urlConfig: require('./url-configuration'), 12 | hash: require('./hash'), 13 | logger: require('./logger'), 14 | request: require('./request'), 15 | thumbnail: require('./thumbnail'), 16 | validators: require('./validators'), 17 | templateHolder: require('./template-holder'), 18 | OpenIDRegistrationClient: require('./openid-registration-client') 19 | } 20 | -------------------------------------------------------------------------------- /src/helper/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const bunyan = require('bunyan') 4 | 5 | // Init. logger. 6 | const logger = bunyan.createLogger({ 7 | name: process.title, 8 | stream: process.stdout, 9 | level: process.env.APP_LOG_LEVEL || 'error' 10 | }) 11 | 12 | /** 13 | * Logger helper. 14 | * @module logger 15 | */ 16 | module.exports = logger 17 | 18 | -------------------------------------------------------------------------------- /src/helper/openid-registration-client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const request = require('request') 4 | 5 | function clean (obj) { 6 | if (!obj) { 7 | return 8 | } 9 | for (const propName in obj) { 10 | if (obj[propName] === null || obj[propName] === undefined) { 11 | delete obj[propName] 12 | } 13 | } 14 | } 15 | 16 | class OpenIDRegistrationClient { 17 | constructor (endpoint, token) { 18 | this.registrationEndpoint = endpoint 19 | this.initialAccessToken = token 20 | } 21 | 22 | errorHandler (err) { 23 | if (err.error_description) { 24 | err = err.error_description 25 | } 26 | return err 27 | } 28 | 29 | request (params) { 30 | const {method, token, json, uri} = params 31 | clean(json) 32 | const url = uri ? `${this.registrationEndpoint}/${uri}` : this.registrationEndpoint 33 | const headers = {} 34 | if (token) { 35 | headers.Authorization = `Bearer ${token}` 36 | } 37 | return new Promise((resolve, reject) => { 38 | // console.log('OpenIDRegistrationClient::request >', {url, method, headers, json}) 39 | request({url, method, headers, json}, (err, res, body) => { 40 | if (err || res.statusCode >= 400) { 41 | // console.log('OpenIDRegistrationClient::request *', err, body) 42 | return reject(this.errorHandler(err || body)) 43 | } 44 | // console.log('OpenIDRegistrationClient::request <', params, body) 45 | return resolve(body) 46 | }) 47 | }) 48 | } 49 | 50 | register (metadata) { 51 | return this.request({ 52 | method: 'POST', 53 | token: this.initialAccessToken, 54 | json: metadata 55 | }) 56 | } 57 | 58 | get (clientId, registrationAccessToken) { 59 | return this.request({ 60 | method: 'GET', 61 | uri: clientId, 62 | token: registrationAccessToken 63 | }) 64 | } 65 | 66 | update (clientId, registrationAccessToken, metadata) { 67 | metadata.clientId = clientId 68 | return this.request({ 69 | method: 'PUT', 70 | uri: clientId, 71 | token: registrationAccessToken, 72 | json: metadata 73 | }) 74 | } 75 | 76 | remove (clientId, registrationAccessToken) { 77 | return this.request({ 78 | method: 'DELETE', 79 | uri: clientId, 80 | token: registrationAccessToken 81 | }) 82 | } 83 | 84 | } 85 | 86 | /** 87 | * OpenID registration client. 88 | * @module openid-registration-client 89 | */ 90 | module.exports = OpenIDRegistrationClient 91 | 92 | -------------------------------------------------------------------------------- /src/helper/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const redis = require('redis') 4 | const url = require('url') 5 | 6 | class RedisHelper { 7 | constructor (uri = 'redis://localhost:6379/1') { 8 | this.uri = url.parse(uri) 9 | } 10 | 11 | createClient () { 12 | const options = { 13 | port: this.uri.port, 14 | host: this.uri.hostname 15 | } 16 | if (this.uri.auth) { 17 | options.password = this.uri.auth.split(':')[1] 18 | } 19 | const client = redis.createClient(options) 20 | if (this.uri.pathname) { 21 | client.select(this.uri.pathname.substring(1)) 22 | } 23 | return client 24 | } 25 | 26 | } 27 | 28 | /** 29 | * Get Redis URI. 30 | * @return {String} Redis string URI 31 | */ 32 | const getRedisUri = function () { 33 | switch (true) { 34 | case process.env.APP_REDIS_URI !== undefined: 35 | return process.env.APP_REDIS_URI 36 | case process.env.OPENREDIS_URL !== undefined: 37 | return process.env.OPENREDIS_URL 38 | case process.env.REDISCLOUD_URL !== undefined: 39 | return process.env.REDISCLOUD_URL 40 | default: 41 | return 'redis://localhost:6379/1' 42 | } 43 | } 44 | 45 | /** 46 | * Redis helper. 47 | * @module redis 48 | */ 49 | module.exports = new RedisHelper(getRedisUri()) 50 | 51 | -------------------------------------------------------------------------------- /src/helper/request.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const request = require('request') 4 | 5 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' 6 | 7 | const customRequest = request.defaults({ 8 | gzip: true, 9 | headers: { 10 | 'User-Agent': process.env.APP_USER_AGENT || 'Mozilla/5.0 (compatible; Keeperbot/1.0)' 11 | } 12 | }) 13 | 14 | module.exports = customRequest 15 | 16 | -------------------------------------------------------------------------------- /src/helper/template-holder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const logger = require('./logger') 7 | 8 | const varDir = path.normalize(path.join(__dirname, '..', '..', 'var')) 9 | const templates = {} 10 | 11 | const dir = path.join(varDir, 'templates') 12 | if (fs.statSync(dir).isDirectory()) { 13 | fs.readdirSync(dir).forEach((file) => { 14 | if (/^[a-z_-]+\.tmpl$/.test(file)) { 15 | const name = path.basename(file, '.tmpl') 16 | logger.debug('Loading %s template file...', name) 17 | fs.readFile(path.join(dir, file), 'utf8', (err, data) => { 18 | if (err) { 19 | return logger.error('Unable to load %s template file', name) 20 | } 21 | templates[name] = _.template(data) 22 | }) 23 | } 24 | }) 25 | } 26 | 27 | /** 28 | * Templates holder. 29 | */ 30 | module.exports = templates 31 | 32 | -------------------------------------------------------------------------------- /src/helper/text-list-holder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const logger = require('./logger') 6 | 7 | const varDir = path.normalize(path.join(__dirname, '..', '..', 'var')) 8 | const db = {} 9 | 10 | /** 11 | * Generic function to read lines of text file. 12 | * @param {Stream} input input stream 13 | * @param {Function} func function to apply on each line 14 | */ 15 | const readLines = function (input, func) { 16 | let remaining = '' 17 | let nbLines = 0 18 | input.on('data', function (data) { 19 | remaining += data 20 | let index = remaining.indexOf('\n') 21 | let last = 0 22 | while (index > -1) { 23 | const line = remaining.substring(last, index) 24 | last = index + 1 25 | func(line) 26 | 27 | nbLines++ 28 | index = remaining.indexOf('\n', last) 29 | } 30 | 31 | remaining = remaining.substring(last) 32 | }) 33 | 34 | input.on('end', function () { 35 | if (remaining.length > 0) { 36 | func(remaining) 37 | nbLines++ 38 | } 39 | logger.debug('Text list loaded: %d items added.', nbLines) 40 | }) 41 | } 42 | 43 | const dir = path.join(varDir, 'list') 44 | if (fs.statSync(dir).isDirectory()) { 45 | fs.readdirSync(dir).forEach((file) => { 46 | if (/^[a-z_-]+\.txt$/.test(file)) { 47 | const name = path.basename(file, '.txt') 48 | logger.debug('Loading %s text file...', name) 49 | db[name] = new Set() 50 | 51 | const input = fs.createReadStream(path.join(dir, file)) 52 | readLines(input, (line) => { 53 | db[name].add(line) 54 | }) 55 | } 56 | }) 57 | } 58 | 59 | /** 60 | * Text list holder. 61 | */ 62 | module.exports = db 63 | 64 | -------------------------------------------------------------------------------- /src/helper/url-configuration.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const url = require('url') 4 | 5 | class UrlConfiguration { 6 | constructor () { 7 | this._port = process.env.APP_PORT || 3000 8 | this._apiVersion = 2 9 | this._baseUrl = process.env.APP_BASE_URL || `http://localhost:${this._port}` 10 | this._url = url.parse(this._baseUrl) 11 | } 12 | 13 | get apiVersion () { 14 | return `v${this._apiVersion}` 15 | } 16 | 17 | get port () { 18 | return this._port 19 | } 20 | 21 | get host () { 22 | return this._url.host 23 | } 24 | 25 | get hostname () { 26 | return this._url.hostname 27 | } 28 | 29 | get basePath () { 30 | return this._url.pathname 31 | } 32 | 33 | get basePathWithVersion () { 34 | return `${this._url.pathname}${this.apiVersion}` 35 | } 36 | 37 | get baseUrl () { 38 | return this._baseUrl 39 | } 40 | 41 | get baseUrlWithVersion () { 42 | return `${this.baseUrl}/${this.apiVersion}` 43 | } 44 | 45 | resolve (path, skipVersion) { 46 | return skipVersion ? `${this.baseUrl}${path}` : `${this.baseUrlWithVersion}${path}` 47 | } 48 | } 49 | 50 | module.exports = new UrlConfiguration() 51 | -------------------------------------------------------------------------------- /src/helper/validators.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const url = require('url') 4 | 5 | const textListHolder = require('./text-list-holder') 6 | 7 | var validators = {} 8 | 9 | /** 10 | * Test if string is into the blacklist. 11 | * @param {String} str 12 | * @return Boolean the test result 13 | */ 14 | validators.isBlacklisted = function (str) { 15 | const u = url.parse(str) 16 | return textListHolder.blacklist.has(u.hostname) 17 | } 18 | 19 | /** 20 | * Test if string is a supported content type. 21 | * @param {String} str 22 | * @return Boolean the test result 23 | */ 24 | validators.isSupportedContentType = function (str) { 25 | return /^text\//.test(str) 26 | } 27 | 28 | /** 29 | * Test if string is a valid Document ID. 30 | * @param {String} str 31 | */ 32 | validators.isDocId = function (str) { 33 | return /^[0-9a-fA-F]{24}$/.test(str) 34 | } 35 | 36 | /** 37 | * Test a valid array of which each element is valid. 38 | * @param {String} arr the array to validate 39 | * @param {Function} validator Validator iterator 40 | * @return Boolean the test result 41 | */ 42 | validators.isArrayOf = function (arr, validator) { 43 | return Array.isArray(arr) && arr.every(validator) 44 | } 45 | 46 | /** 47 | * Validators helper. 48 | * @module validators 49 | */ 50 | module.exports = validators 51 | 52 | -------------------------------------------------------------------------------- /src/job/embedded.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Dynamic loading jobs... 4 | const jobs = process.env.APP_JOBS 5 | 6 | const Tasks = require('./tasks') 7 | const worker = new Tasks(jobs ? jobs.split(',') : []) 8 | 9 | module.exports = worker 10 | -------------------------------------------------------------------------------- /src/job/launcher.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const appInfo = require('../../package.json') 6 | const program = require('commander') 7 | const logger = require('../helper/logger') 8 | const assert = require('assert') 9 | 10 | process.title = 'keeper-job-launcher' 11 | 12 | function collectParams (val, params) { 13 | const [name, value] = val.split('=') 14 | params[name] = value 15 | return params 16 | } 17 | 18 | program.version(appInfo.version) 19 | .description('Launch a job.') 20 | .option('-v, --verbose', 'Verbose flag') 21 | .option('-d, --debug', 'Debug flag') 22 | .option('-j, --job ', 'Job name') 23 | .option('-p, --param [name=value]', 'Job parameters', collectParams, {}) 24 | .parse(process.argv) 25 | 26 | // Set logger level 27 | const defaultLevel = process.env.APP_LOG_LEVEL || 'error' 28 | const level = program.debug ? 'debug' : program.verbose ? 'info' : defaultLevel 29 | logger.level(level) 30 | 31 | assert(program.job, 'Job name parameter not defined') 32 | assert(program.param, 'Job parameter not defined') 33 | 34 | const jobService = require('../service/job.service') 35 | 36 | const params = Object.assign({ 37 | title: `${program.job} job started by the launcher` 38 | }, program.param) 39 | 40 | logger.debug('Queuing job %s with params %j', program.job, params) 41 | jobService.launch(program.job, params, jobService.priority.HIGH) 42 | .then(job => { 43 | let progress = -1 44 | job.on('complete', function (result) { 45 | logger.info('Job %d completed with', job.id, result) 46 | process.exit(0) 47 | }).on('failed attempt', function (err, attempts) { 48 | logger.error('Job %d failed after %d attempts:', job.id, attempts, err) 49 | process.exit(1) 50 | }).on('failed', function (err) { 51 | logger.error('Job %d failed:', job.id, err) 52 | process.exit(1) 53 | }).on('progress', function (_progress) { 54 | if (progress !== _progress) { 55 | progress = _progress 56 | logger.debug('Job %d progression: %d%', job.id, progress) 57 | } 58 | }) 59 | }) 60 | 61 | -------------------------------------------------------------------------------- /src/job/tasks/abstract-task.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../../helper').logger 4 | const jobService = require('../../service').job 5 | const metrics = require('../../metrics/client') 6 | 7 | /** 8 | * Abstarct definition of a job task. 9 | */ 10 | class AbstractTask { 11 | constructor (jobName) { 12 | this.name = jobName 13 | jobService.queue.process(jobName, this.start.bind(this)) 14 | } 15 | 16 | /** 17 | * Job launcher. 18 | * @param {Object} job to process 19 | * @param {Function} done Completion callback 20 | */ 21 | start (job, done) { 22 | logger.info(`Starting ${this.name} ...`) 23 | const t0 = new Date() 24 | this.process(job, (err, result) => { 25 | const t1 = new Date() 26 | let sts = 'success' 27 | if (err) { 28 | sts = 'error' 29 | logger.error(`Error during ${this.name} job processing`, err) 30 | } else { 31 | logger.info(`${this.name} job done.`) 32 | } 33 | metrics.timing(`processed_job,name=${this.name},status=${sts}`, t1 - t0) 34 | done(err, result) 35 | }) 36 | } 37 | 38 | /** 39 | * Task processing. 40 | * @param {Object} job to process 41 | * @return {Promise} job result 42 | */ 43 | process (job) { 44 | return Promise.reject('Not implemented') 45 | } 46 | } 47 | 48 | module.exports = AbstractTask 49 | -------------------------------------------------------------------------------- /src/job/tasks/download.task.js: -------------------------------------------------------------------------------- 1 | const AbstractTask = require('./abstract-task.js') 2 | const downloadService = require('../../service/download.service') 3 | 4 | class DownloadTask extends AbstractTask { 5 | /** 6 | * Download task. 7 | * @param {Object} job to process 8 | * @param {Function} done Completion callback 9 | */ 10 | process (job, done) { 11 | const { resources, container } = job.data 12 | downloadService.promiseDownload(resources, container) 13 | .then(() => { 14 | job.log('Resources downloaded') 15 | done() 16 | }, (err) => { 17 | job.log('Unable to download resources', err) 18 | done(err) 19 | }) 20 | } 21 | } 22 | 23 | module.exports = DownloadTask 24 | -------------------------------------------------------------------------------- /src/job/tasks/ghostbuster.task.js: -------------------------------------------------------------------------------- 1 | const storage = require('../../storage') 2 | const documentDao = require('../../dao').document 3 | const searchengine = require('../../dao/searchengine') 4 | 5 | const AbstractTask = require('./abstract-task.js') 6 | 7 | class GhostbusterTask extends AbstractTask { 8 | /** 9 | * Ghostbuster task. 10 | * Remove ghost documents from the graveyard. 11 | * @param {Object} job to process 12 | * @param {Function} done Completion callback 13 | */ 14 | process (job, done) { 15 | const limit = job.data.limit || 100 16 | const hours = job.data.hours || 2 17 | const expirationDate = new Date() 18 | expirationDate.setTime(expirationDate.getTime() - (hours * 60 * 60 * 1000)) 19 | 20 | documentDao.find({ghost: true, date: {$lte: expirationDate}}, {limit: limit}) 21 | .then((ghosts) => { 22 | if (!ghosts.length) { 23 | return Promise.resolve([]) 24 | } 25 | job.log(`Deleting ${ghosts.length} ghost(s)...`) 26 | if (ghosts.length >= limit) { 27 | job.log('It remains some ghosts!') 28 | } 29 | const tasks = [] 30 | ghosts.forEach((ghost) => { 31 | const container = storage.getContainerName(ghost.owner, 'documents', ghost.id) 32 | const deleted = storage.remove(container) 33 | deleted.then(() => searchengine.unindexDocument(ghost)) 34 | .then(() => documentDao.remove(ghost)) 35 | tasks.push(deleted) 36 | }) 37 | return Promise.all(tasks) 38 | }) 39 | .then((t) => { 40 | job.log(`${t.length} ghosts deleted`) 41 | done(null, {nb: t.length}) 42 | }, (err) => { 43 | job.log('Unable to delete ghosts', err) 44 | done(err) 45 | }) 46 | } 47 | } 48 | 49 | module.exports = GhostbusterTask 50 | -------------------------------------------------------------------------------- /src/job/tasks/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const logger = require('../../helper').logger 5 | const globals = require('../../helper').globals 6 | const services = require('../../service') 7 | 8 | class TaskWorker { 9 | constructor (jobs = []) { 10 | this.names = new Set(jobs) 11 | this.loaded = this.init() 12 | } 13 | 14 | init () { 15 | return globals.EMBEDDED_WORKER 16 | ? Promise.resolve() 17 | : services.isReady() 18 | } 19 | 20 | stop () { 21 | return globals.EMBEDDED_WORKER 22 | ? Promise.resolve() 23 | : services.shutdown() 24 | } 25 | 26 | start () { 27 | this.loaded 28 | .then(() => { 29 | this.tasks = require('fs').readdirSync(__dirname).reduce((acc, file) => { 30 | if (/^[a-z_-]+\.task\.js$/.test(file)) { 31 | const name = path.basename(file, '.task.js') 32 | if (this.names.size === 0 || this.names.has(name)) { 33 | logger.debug('Loading %s task...', name) 34 | const Task = require(path.join(__dirname, file)) 35 | acc.push(new Task(name)) 36 | } 37 | } 38 | return acc 39 | }, []) 40 | logger.info('Ready to process tasks.') 41 | }) 42 | } 43 | } 44 | 45 | module.exports = TaskWorker 46 | -------------------------------------------------------------------------------- /src/job/tasks/rebuild-index.task.js: -------------------------------------------------------------------------------- 1 | const stream = require('stream') 2 | const logger = require('../../helper/logger') 3 | const documentDao = require('../../dao').document 4 | const searchengine = require('../../dao/searchengine') 5 | 6 | const AbstractTask = require('./abstract-task.js') 7 | 8 | class IndexationStream extends stream.Writable { 9 | constructor (job, total) { 10 | super({objectMode: true}) 11 | this.job = job 12 | this.counter = 0 13 | this.total = total 14 | } 15 | 16 | _write (chunk, encoding, next) { 17 | const { refresh = false, create = true } = this.job.data 18 | searchengine.reindexDocument(chunk, {refresh, create}) 19 | .then(() => { 20 | logger.debug('Document %s re-indexed (%d/%d).', chunk.id, this.counter, this.total) 21 | this.job.progress(this.counter++, this.total) 22 | next() 23 | }, next) 24 | } 25 | } 26 | 27 | class RebuildIndexTask extends AbstractTask { 28 | /** 29 | * Rebuild the searchengine index. 30 | * @param {Object} job to process 31 | * @param {Function} done Completion callback 32 | */ 33 | process (job, done) { 34 | let total = 0 35 | documentDao.count() 36 | .then((nb) => { 37 | total = nb 38 | return documentDao.stream() 39 | }) 40 | .then((s) => { 41 | const idxStream = new IndexationStream(job, total) 42 | return new Promise((resolve, reject) => { 43 | s.pipe(idxStream) 44 | .on('error', reject) 45 | .once('finish', () => resolve(idxStream.counter)) 46 | }) 47 | }) 48 | .then((nb) => { 49 | job.log(`${nb} documents re-indexed`) 50 | done(null, {nb}) 51 | }, (err) => { 52 | job.log('Unable to re-index documents', err) 53 | done(err) 54 | }) 55 | } 56 | } 57 | 58 | module.exports = RebuildIndexTask 59 | -------------------------------------------------------------------------------- /src/job/worker.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const appInfo = require('../../package.json') 6 | const program = require('commander') 7 | const logger = require('../helper/logger') 8 | 9 | process.title = 'keeper-job-worker' 10 | 11 | program.version(appInfo.version) 12 | .description('Job worker.') 13 | .option('-v, --verbose', 'Verbose flag') 14 | .option('-d, --debug', 'Debug flag') 15 | .option('-j, --jobs ', 'Comma delimited list of job to handle') 16 | .parse(process.argv) 17 | 18 | // Set logger level 19 | const defaultLevel = process.env.APP_LOG_LEVEL || 'error' 20 | const level = program.debug ? 'debug' : program.verbose ? 'info' : defaultLevel 21 | logger.level(level) 22 | 23 | const jobs = program.jobs || process.env.APP_JOBS 24 | 25 | const Tasks = require('./tasks') 26 | const worker = new Tasks(jobs ? jobs.split(',') : []) 27 | 28 | logger.info('Starting job worker...') 29 | worker.start() 30 | 31 | ;['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => { 32 | process.on(signal, () => { 33 | worker.stop().then(() => { 34 | logger.info('Job worker stopped.') 35 | process.exit(0) 36 | }, (err) => { 37 | logger.error('Error during job shutdown.', err) 38 | process.exit(1) 39 | }) 40 | setTimeout(function () { 41 | logger.error('Could not shutdown gracefully, forcefully shutting down!') 42 | process.exit(1) 43 | }, 10000) 44 | }) 45 | }) 46 | 47 | -------------------------------------------------------------------------------- /src/metrics/client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const url = require('url') 4 | const logger = require('../helper').logger 5 | const globals = require('../helper').globals 6 | 7 | const PREFIX = 'keeper' 8 | 9 | /** 10 | * Stats server client. 11 | * @module stats/client 12 | */ 13 | class StatsServerClient { 14 | /** 15 | * Constructor. 16 | */ 17 | constructor () { 18 | // Disable the event handler if the search feature is not delegated. 19 | this.disabled = !globals.STATS_SERVER_URI 20 | if (this.disabled) { 21 | logger.debug('No metric provider configured.') 22 | return 23 | } 24 | const providerName = url.parse(globals.STATS_SERVER_URI).protocol.slice(0, -1) 25 | this.provider = require(`./${providerName}`)(globals.STATS_SERVER_URI) 26 | } 27 | 28 | /** 29 | * Increment a metric. 30 | * @param {String} name Metric name 31 | */ 32 | increment (name) { 33 | if (this.disabled) { 34 | return 35 | } 36 | this.provider.increment(`${PREFIX}_${name}`) 37 | } 38 | 39 | /** 40 | * Set a metric. 41 | * @param {String} name Metric name 42 | * @param {Integer} value Metric value 43 | */ 44 | set (name, value) { 45 | if (this.disabled) { 46 | return 47 | } 48 | this.provider.set(`${PREFIX}_${name}`, value) 49 | } 50 | 51 | /** 52 | * Set a gauge. 53 | * @param {String} name Gauge name 54 | * @param {Integer} value Gauge value 55 | */ 56 | gauge (name, value) { 57 | if (this.disabled) { 58 | return 59 | } 60 | this.provider.gauge(`${PREFIX}_${name}`, value) 61 | } 62 | 63 | /** 64 | * Mesure timing. 65 | * @param {String} name Timer name 66 | * @param {Integer} value Timer value 67 | */ 68 | timing (name, value) { 69 | if (this.disabled) { 70 | return 71 | } 72 | this.provider.timing(`${PREFIX}_${name}`, value) 73 | } 74 | } 75 | 76 | module.exports = new StatsServerClient() 77 | -------------------------------------------------------------------------------- /src/metrics/statsd/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const url = require('url') 4 | const Lynx = require('lynx') 5 | const logger = require('../../helper').logger 6 | 7 | module.exports = function (uri) { 8 | const u = url.parse(uri) 9 | logger.debug('Loading StatsD provider for metrics...') 10 | 11 | const client = new Lynx(u.hostname, u.port, { 12 | scope: 'keeper', 13 | on_error: logger.err 14 | }) 15 | 16 | return client 17 | } 18 | -------------------------------------------------------------------------------- /src/middleware/admin.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const errors = require('../helper').errors 4 | 5 | /** 6 | * Middleware to protect admin resources. 7 | */ 8 | module.exports = { 9 | isAdmin: function (req, res, next) { 10 | if (!req.user.admin) { 11 | return next(new errors.Forbidden()) 12 | } 13 | return next() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/middleware/auth_apikey.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const errors = require('../helper').errors 4 | const logger = require('../helper').logger 5 | const userService = require('../service').user 6 | 7 | /** 8 | * Middleware to handle API key. 9 | */ 10 | module.exports = function (allowed) { 11 | return function (req, res, next) { 12 | // Ignore the middleware if already authenticated or bypassed 13 | if (req.user || req.authBypassed) { 14 | return next() 15 | } 16 | 17 | const authHeader = req.get('Authorization') 18 | if (authHeader && authHeader.startsWith('Basic ')) { 19 | // Extract credentials from the header 20 | const [login, apiKey] = Buffer.from(authHeader.substr(6), 'base64').toString('ascii').split(':') 21 | if (login !== 'api' || !apiKey) { 22 | return next(new errors.Unauthorized('Bad API key')) 23 | } 24 | // Only allow configured path and the method. 25 | if (!(allowed.path.find((p) => req.path.match(p)) && 26 | allowed.method.find((m) => req.method === m))) { 27 | return next(new errors.Unauthorized('Path or method unauthorized')) 28 | } 29 | userService.loginWithApiKey(apiKey, { 30 | ip: req.headers['x-forwarded-for'] || req.connection.remoteAddress 31 | }).then((user) => { 32 | req.user = user 33 | next() 34 | }).catch((e) => { 35 | logger.error('Unable to login', e) 36 | return next(new errors.Unauthorized(e)) 37 | }) 38 | } else { 39 | return next() 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/middleware/auth_bypass.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../helper').logger 4 | 5 | /** 6 | * Authentification is bypassed for public URL. 7 | */ 8 | module.exports = function (exceptions) { 9 | return function (req, res, next) { 10 | req.authBypassed = false 11 | // Ignore other auth middlewares if the path and the method match an exception 12 | if (exceptions.path.find((p) => req.path.match(p)) && 13 | exceptions.method.find((m) => req.method === m)) { 14 | logger.debug('%s:%s match the bypass. Other auth middlewares are ignored.', req.method, req.path) 15 | req.authBypassed = true 16 | } 17 | return next() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/middleware/auth_jwt.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const jwt = require('jsonwebtoken') 4 | const Cookies = require('cookies') 5 | const errors = require('../helper').errors 6 | const logger = require('../helper').logger 7 | const globals = require('../helper').globals 8 | const urlConfig = require('../helper').urlConfig 9 | const userService = require('../service').user 10 | 11 | let key = globals.TOKEN_SECRET 12 | let algorithm = 'HS256' 13 | if (globals.TOKEN_PUB_KEY) { 14 | const fs = require('fs') 15 | key = fs.readFileSync(globals.TOKEN_PUB_KEY) 16 | algorithm = 'RS256' 17 | } 18 | 19 | /** 20 | * Middleware to handle JWT. 21 | */ 22 | module.exports = function () { 23 | return function (req, res, next) { 24 | // Ignore the middleware if already authenticated or bypassed 25 | if (req.user || req.authBypassed) { 26 | return next() 27 | } 28 | 29 | let token = null 30 | let setCookie = false 31 | const cookies = new Cookies(req, res) 32 | const authHeader = req.get('Authorization') 33 | if (authHeader && authHeader.startsWith('Bearer ')) { 34 | // Extract token from the header 35 | token = authHeader.substr(7) 36 | // Set access token cookie 37 | setCookie = true 38 | } else { 39 | // Try to extract token from cookie 40 | token = cookies.get('access_token') 41 | } 42 | if (!token) { 43 | return next(new errors.Unauthorized('Missing access token')) 44 | } 45 | jwt.verify(token, key, {algorithm: algorithm}, function (err, decoded) { 46 | if (err) { 47 | return next(new errors.Unauthorized(err)) 48 | } 49 | userService.login({ 50 | uid: decoded.preferred_username || decoded.sub, 51 | name: decoded.name, 52 | email: decoded.email, 53 | date: new Date(), 54 | ip: req.headers['x-forwarded-for'] || req.connection.remoteAddress 55 | }).then((user) => { 56 | req.user = user 57 | if (decoded.realm_access && decoded.realm_access.roles) { 58 | req.user.admin = decoded.realm_access.roles.indexOf('admin') > -1 59 | } 60 | if (setCookie) { 61 | cookies.set('access_token', token, { 62 | domain: urlConfig.hostname, 63 | path: urlConfig.basePath, 64 | httpOnly: true, 65 | secure: false // u.protocol === 'https:' 66 | }) 67 | } 68 | next() 69 | }).catch((e) => { 70 | logger.error('Unable to login', e) 71 | return next(new errors.Unauthorized(e)) 72 | }) 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/middleware/client.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const errors = require('../helper').errors 4 | const clientService = require('../service').client 5 | 6 | /** 7 | * Middleware to get client form path params. 8 | */ 9 | module.exports = function (req, res, next) { 10 | clientService.get(req.params.clientId) 11 | .then(client => { 12 | if (!client) { 13 | return next(new errors.NotFound('Client not found.')) 14 | } 15 | // Only allow to see own client. 16 | if (client.owner !== req.user.id) { 17 | return next(new errors.Forbidden()) 18 | } 19 | 20 | if (!req.requestData) { 21 | req.requestData = {} 22 | } 23 | req.requestData.client = client 24 | next() 25 | }, next) 26 | } 27 | -------------------------------------------------------------------------------- /src/middleware/cors.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Middleware to handle Cross-Origin Resource Sharing requests. 5 | */ 6 | module.exports = function () { 7 | return function (req, res, next) { 8 | // TODO Use whitelisted origin 9 | res.header('Access-Control-Allow-Origin', req.headers.origin) 10 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS') 11 | res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With') 12 | res.header('Access-Control-Allow-Credentials', 'true') 13 | 14 | // intercept OPTIONS method 15 | if (req.method === 'OPTIONS') { 16 | res.sendStatus(200) 17 | } else { 18 | next() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/middleware/document.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const errors = require('../helper').errors 5 | const documentService = require('../service').document 6 | 7 | /** 8 | * Middleware to get document form path params. 9 | */ 10 | module.exports = function (req, res, next) { 11 | documentService.get(req.params.docid) 12 | .then(function (doc) { 13 | if (!doc) { 14 | return next(new errors.NotFound('Document not found.')) 15 | } 16 | if (req.requestData && req.requestData.sharing) { 17 | // Get a shared document 18 | const sharing = req.requestData.sharing 19 | if (!_.includes(doc.labels, sharing.targetLabel)) { 20 | return next(new errors.BadRequest('Document dont\'t match')) 21 | } 22 | } else if (doc.owner !== req.user.id) { 23 | // Only allow to see own document. 24 | return next(new errors.Forbidden()) 25 | } 26 | // Only allow to see a non ghost document. 27 | if (doc.ghost) { 28 | return next(new errors.NotFound('Document not existing anymore.')) 29 | } 30 | 31 | if (!req.requestData) { 32 | req.requestData = {} 33 | } 34 | req.requestData.document = doc 35 | next() 36 | }, next) 37 | } 38 | -------------------------------------------------------------------------------- /src/middleware/error.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const logger = require('../helper').logger 5 | 6 | /** 7 | * Middleware to handle errors. 8 | */ 9 | module.exports = function () { 10 | return function (err, req, res, next) { 11 | res.status(err.status || 500) 12 | if (res.statusCode >= 400 && res.statusCode !== 404) { 13 | logger.error(err) 14 | } 15 | const error = _.isString(err) ? err : (_.isObject(err) ? err.message : 'Unknown Error') 16 | res.json({ 17 | error: error, 18 | meta: err.meta 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/middleware/graveyard.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Middleware to to add ghost param to the query. 5 | */ 6 | module.exports = { 7 | ghost: function (req, res, next) { 8 | if (!req.query) { 9 | req.query = {} 10 | } 11 | req.query.ghost = true 12 | return next() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/middleware/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../helper').logger 4 | const path = require('path') 5 | 6 | // Dynamic loading middlewares... 7 | const middlewares = {} 8 | require('fs').readdirSync(__dirname).forEach((file) => { 9 | if (/^[a-z_]+\.middleware\.js$/.test(file)) { 10 | const name = path.basename(file, '.middleware.js') 11 | logger.debug('Loading %s middleware...', name) 12 | middlewares[name] = require(path.join(__dirname, file)) 13 | } 14 | }) 15 | 16 | module.exports = middlewares 17 | -------------------------------------------------------------------------------- /src/middleware/label.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const errors = require('../helper').errors 4 | const labelService = require('../service').label 5 | 6 | /** 7 | * Middleware to get label form path params. 8 | */ 9 | module.exports = function (req, res, next) { 10 | labelService.get(req.params.labelId) 11 | .then(function (label) { 12 | if (!label) { 13 | return next(new errors.NotFound('Label not found.')) 14 | } 15 | // Only allow to see own label. 16 | if (label.owner !== req.user.id) { 17 | return next(new errors.Forbidden()) 18 | } 19 | 20 | if (!req.requestData) { 21 | req.requestData = {} 22 | } 23 | req.requestData.label = label 24 | next() 25 | }, next) 26 | } 27 | -------------------------------------------------------------------------------- /src/middleware/logger.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../helper').logger 4 | 5 | /** 6 | * Middleware to handle request logs. 7 | */ 8 | module.exports = function () { 9 | return function (req, res, next) { 10 | const start = new Date() 11 | const end = res.end 12 | res.end = function (chunk, encoding) { 13 | const responseTime = (new Date()).getTime() - start.getTime() 14 | end.call(res, chunk, encoding) 15 | const contentLength = parseInt(res.getHeader('Content-Length'), 10) 16 | const data = { 17 | res: res, 18 | req: req, 19 | responseTime: responseTime, 20 | contentLength: isNaN(contentLength) ? 0 : contentLength 21 | } 22 | logger.info('%s %s %d %dms - %d', data.req.method, data.req.url, data.res.statusCode, data.responseTime, data.contentLength) 23 | } 24 | next() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/middleware/multipart.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const os = require('os') 4 | const path = require('path') 5 | const multiparty = require('multiparty') 6 | const logger = require('../helper').logger 7 | 8 | const uploadDir = process.env.APP_STORAGE_LOCAL_DIR ? path.join(process.env.APP_STORAGE_LOCAL_DIR, 'upload') : os.tmpdir() 9 | 10 | /** 11 | * Middleware to handle multipart/form-data requests. 12 | * @module multipart 13 | */ 14 | module.exports = function () { 15 | return function (req, res, next) { 16 | const ct = req.header('Content-Type') 17 | if (req.method === 'POST' && /^multipart\/form-data/.test(ct)) { 18 | const form = new multiparty.Form({uploadDir: uploadDir}) 19 | 20 | req.files = [] 21 | req.fields = {} 22 | 23 | form.on('error', next) 24 | form.on('file', function (name, file) { 25 | req.files.push(file) 26 | }) 27 | form.on('field', function (name, value) { 28 | req.fields[name] = value 29 | if (name === 'document') { 30 | try { 31 | req.body = JSON.parse(value) 32 | } catch (e) { 33 | logger.error('Unable to parse document field.', e) 34 | } 35 | } 36 | }) 37 | form.on('close', function () { 38 | next() 39 | }) 40 | 41 | form.parse(req) 42 | } else { 43 | next() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/middleware/webhook.middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const errors = require('../helper').errors 4 | const webhookService = require('../service').webhook 5 | 6 | /** 7 | * Middleware to get webhook form path params. 8 | */ 9 | module.exports = function (req, res, next) { 10 | webhookService.get(req.params.webhookId) 11 | .then(webhook => { 12 | if (!webhook) { 13 | return next(new errors.NotFound('Webhook not found.')) 14 | } 15 | // Only allow to see own webhook. 16 | if (webhook.owner !== req.user.id) { 17 | return next(new errors.Forbidden()) 18 | } 19 | 20 | if (!req.requestData) { 21 | req.requestData = {} 22 | } 23 | req.requestData.webhook = webhook 24 | next() 25 | }, next) 26 | } 27 | -------------------------------------------------------------------------------- /src/service/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const logger = require('../helper').logger 5 | const daos = require('../dao') 6 | const searchengine = require('../dao/searchengine') 7 | 8 | // Dynamic loading services... 9 | const services = {} 10 | require('fs').readdirSync(__dirname).forEach((file) => { 11 | if (/^[a-z_]+\.service\.js$/.test(file)) { 12 | const name = path.basename(file, '.service.js') 13 | logger.debug('Loading %s service...', name) 14 | services[name] = require(path.join(__dirname, file)) 15 | } 16 | }) 17 | 18 | /** 19 | * Shutdown services. 20 | * @return {Promise} shudtdown promise 21 | */ 22 | services.shutdown = function () { 23 | // Shutdown DAOs and Job queue... 24 | return daos.shutdown() 25 | .then(() => this.job.shutdown()) 26 | } 27 | 28 | /** 29 | * Whait until the services are available. 30 | * @return {Promise} readyness promise 31 | */ 32 | services.isReady = function () { 33 | return daos.isReady().then(() => searchengine.isReady()) 34 | } 35 | 36 | module.exports = services 37 | -------------------------------------------------------------------------------- /src/service/job.service.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | const logger = require('../helper').logger 5 | const redis = require('../helper/redis') 6 | 7 | const kue = require('kue') 8 | 9 | class JobService { 10 | constructor () { 11 | this.priority = { 12 | HIGH: 'high', 13 | NORMAL: 'normal', 14 | LOW: 'low' 15 | } 16 | this.queue = kue.createQueue({ 17 | redis: { 18 | createClientFactory: function () { 19 | return redis.createClient() 20 | } 21 | } 22 | }) 23 | } 24 | 25 | shutdown () { 26 | return new Promise((resolve, reject) => { 27 | this.queue.shutdown(5000, (err) => { 28 | if (err) { 29 | return reject(err) 30 | } 31 | return resolve() 32 | }) 33 | }) 34 | } 35 | 36 | launch (name, params = {}, priority = 'normal', removeOnComplete = false) { 37 | logger.debug('Launching %s job with params', name, params) 38 | return new Promise((resolve, reject) => { 39 | const job = this.queue 40 | .create(name, params) 41 | .priority(priority) 42 | .removeOnComplete(removeOnComplete) 43 | .save(err => err ? reject(err) : resolve(job)) 44 | }) 45 | } 46 | 47 | get (id) { 48 | return new Promise((resolve, reject) => { 49 | kue.Job.get(id, (err, job) => { 50 | if (err) { 51 | if (err.message && err.message.match(/^job "[\d]+" doesnt exist$/)) { 52 | return resolve(null) 53 | } 54 | return reject(err) 55 | } 56 | return resolve(job) 57 | }) 58 | }) 59 | } 60 | } 61 | 62 | module.exports = new JobService() 63 | -------------------------------------------------------------------------------- /src/service/monitoring.service.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Monitoring services. 5 | * @module monitoring.service 6 | */ 7 | const MonitoringService = {} 8 | 9 | MonitoringService.monitor = function () { 10 | return Promise.resolve(true) 11 | } 12 | 13 | module.exports = MonitoringService 14 | 15 | -------------------------------------------------------------------------------- /src/storage/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../helper').logger 4 | const path = require('path') 5 | 6 | // Dynamic loading storage driver... 7 | const STORAGE_DRIVER = process.env.APP_STORAGE 8 | let driver = null 9 | require('fs').readdirSync(__dirname).forEach(function (file) { 10 | if (file === `${STORAGE_DRIVER}.storage.js`) { 11 | logger.debug('Loading %s storage driver...', STORAGE_DRIVER) 12 | driver = require(path.join(__dirname, file)) 13 | } 14 | }) 15 | 16 | if (!driver) { 17 | logger.debug('Storage driver not found. Using local...') 18 | driver = require('./local.storage.js') 19 | } 20 | 21 | module.exports = driver 22 | -------------------------------------------------------------------------------- /var/assets/gpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GNU General Public License v3.0 - GNU Project - Free Software Foundation (FSF) 6 | 8 | 9 | 10 |

NUNUX Keeper

11 |

Copyright (C) 2014 Nicolas Carlier

12 | 13 |

14 | This program is free software: you can redistribute it and/or modify 15 | it under the terms of the GNU General Public License as published by 16 | the Free Software Foundation, either version 3 of the License, or 17 | (at your option) any later version. 18 |

19 |

20 | This program is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | GNU General Public License for more details. 24 |

25 |

26 | You should have received a copy of the GNU General Public License 27 | along with this program. If not, see http://www.gnu.org/licenses. 28 |

29 | 30 | 31 | -------------------------------------------------------------------------------- /var/assets/gpl.txt: -------------------------------------------------------------------------------- 1 | NUNUX Keeper 2 | Copyright (C) 2014 Nicolas Carlier 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /var/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/var/assets/logo.png -------------------------------------------------------------------------------- /var/assets/oss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nunux-keeper/keeper-core-api/8b43fecc2887657cd73915218e96c02dc5c89be3/var/assets/oss.png -------------------------------------------------------------------------------- /var/cert/Makefile: -------------------------------------------------------------------------------- 1 | .SILENT : 2 | .PHONY : all help clean 3 | 4 | KEY_LENGHT:=1024 5 | 6 | DAYS:=1460 7 | 8 | all: help 9 | 10 | ## This help screen 11 | help: 12 | printf "Available targets:\n\n" 13 | awk '/^[a-zA-Z\-\_0-9\.%\/@]+:/ { \ 14 | helpMessage = match(lastLine, /^## (.*)/); \ 15 | if (helpMessage) { \ 16 | helpCommand = substr($$1, 0, index($$1, ":")); \ 17 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 18 | printf "%-15s %s\n", helpCommand, helpMessage; \ 19 | } \ 20 | } \ 21 | { lastLine = $$0 }' $(MAKEFILE_LIST) 22 | 23 | ## Delete all generated certificates 24 | clean: 25 | @echo "Deleting all generated files..." 26 | rm *.pem 27 | 28 | ## Generate keys 29 | %.pem: 30 | @echo "Generating $@ private key" 31 | openssl genrsa -out $*.pem $(KEY_LENGHT) 32 | @echo "Extracting $@ public key" 33 | openssl rsa -in $*.pem -pubout -out $*-pub.pem 34 | 35 | ## Generate certificate 36 | %.crt: 37 | @echo "Generating $@ certificate" 38 | openssl req -new -key $*.pem -out $*.csr -subj '/CN=$*/' 39 | openssl req -x509 -sha256 -days $(DAYS) -key $*.pem -in $*.csr -out $*.crt 40 | rm $*.csr 41 | 42 | -------------------------------------------------------------------------------- /var/cert/staging-pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7kgrRHj3XvKylJmt4uoqXN/EU 3 | K8+GCuWEvWk2uaOoIJhsqqTsxR7Y4CLkdAQS05y3YJK/FT4wNIMmyMZGCCvZd6iO 4 | LA4u7DuoQs3/WTQfwQzu7ZLDK1xvo9Pk8wMZdAQ8uCYaeffNpVLOcSzX3v7mEoBa 5 | MOsTa4smYTcSs9/G3QIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /var/cert/staging.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIB8jCCAVugAwIBAgIJAMFO52dMKMH9MA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV 3 | BAMMB3N0YWdpbmcwHhcNMTYwNzAxMTUwOTM5WhcNMjAwNjMwMTUwOTM5WjASMRAw 4 | DgYDVQQDDAdzdGFnaW5nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7kgrR 5 | Hj3XvKylJmt4uoqXN/EUK8+GCuWEvWk2uaOoIJhsqqTsxR7Y4CLkdAQS05y3YJK/ 6 | FT4wNIMmyMZGCCvZd6iOLA4u7DuoQs3/WTQfwQzu7ZLDK1xvo9Pk8wMZdAQ8uCYa 7 | effNpVLOcSzX3v7mEoBaMOsTa4smYTcSs9/G3QIDAQABo1AwTjAdBgNVHQ4EFgQU 8 | F9rzt9UyCDVtMy2VOTWXBsaDYsMwHwYDVR0jBBgwFoAUF9rzt9UyCDVtMy2VOTWX 9 | BsaDYsMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQBPJCtPzVn0FMi2 10 | fzaNzJN8RCfDI8dcCYF6VS7iyS8yJYXESsIJhT9m4Z608jSYxYUFAAUYWILF1H5k 11 | DxcNqq8LKeGE7oFlAKnNbKbs4JeYHRm9x3TAewXmW5D/0RKRnBNwyoAxwZRMtwNA 12 | huSNfFIV0EhThqJGrJDBFlIa4jnX5A== 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /var/cert/test-pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOERVHxeAsm4sB5AUJe+BX9fg6 3 | orm4QTtvvd3FrIEhUa/AoF2mGFgSVT41LWty5cWMNvfN96sYp+26fUwbqdkM8IXH 4 | HkSmLbunXp5LXBLpK+PNs/f001qtX3yZY2M7lb+MTBluxuIQQBJ7sH+K36vyvs1b 5 | o7dFClkbhCEh7Cfd+QIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /var/cert/test.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWwIBAAKBgQDOERVHxeAsm4sB5AUJe+BX9fg6orm4QTtvvd3FrIEhUa/AoF2m 3 | GFgSVT41LWty5cWMNvfN96sYp+26fUwbqdkM8IXHHkSmLbunXp5LXBLpK+PNs/f0 4 | 01qtX3yZY2M7lb+MTBluxuIQQBJ7sH+K36vyvs1bo7dFClkbhCEh7Cfd+QIDAQAB 5 | AoGAUEU1Te0Vwyqf/o+NKsGXhYwqMuUahVzbw05pqkvlTU5dsSKG+GF5RULxZZOY 6 | zHwW9OSgqRn2Cze3EjMmx4vlIUNLKB9EVhjPt0xgvUs/VWxrAOOMPqNXaY42yWao 7 | A+ij/DPFTKXfricC7qQmRMstL1YUO+UgQY5hE4Z1vhSf6oECQQDms/zhfU/mTHvH 8 | uaA/e8lhj9h3fAXEudLH+Lsv4HniUINt6MPYxc5pbTgmDrzCRJf/0lQ6qFqocbwj 9 | ktRvrYxpAkEA5KmIwGYshZGGQoyG+Ll+J4/w9XnQ6u3HO1253bLYKBktl5v9vPDT 10 | k2Nf43p+GTZae47RsBLepL4rU9Pwvs/TEQJATPrJ6sPRoVrsApzh58z6SV+iL1xm 11 | 4FnRB4DrlPImOWn8MPR2RSLb2Tnkfm50da5FucsI5/m9Jm+tA4G++PTD4QJAZc+G 12 | cAQONrqAyQjA3+XXYc4ZP3MprjCI0Mn+g9OpotDzF9oAtCuWSL2qaCKWGqu0xebl 13 | PjxonXl+2Sg4vPzTwQJALIfN8E7oBTu+8Tj1P++/roEr9c63rCjWacZ0fNd1VTgI 14 | c58mSCxRdf09BAzaREIuv7C+tdCFtYeFUi3KdOx9dA== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /var/scripts/backup-data: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -d "$APP_STORAGE_LOCAL_DIR" ]; then 4 | echo "App data directory (APP_STORAGE_LOCAL_DIR=$APP_STORAGE_LOCAL_DIR) doesn't exist." 5 | exit 1 6 | fi 7 | 8 | if [ ! -d "$APP_BACKUP_DIR" ]; then 9 | echo "Backup directory (APP_BACKUP_DIR=$APP_BACKUP_DIR) doesn't exist." 10 | exit 1 11 | fi 12 | 13 | BACKUP_TARGET_DIR=$APP_BACKUP_DIR/keeper 14 | if [ ! -d "$BACKUP_TARGET_DIR" ]; then 15 | echo "Target backup directory not found ... creating" 16 | mkdir -p $BACKUP_TARGET_DIR 17 | fi 18 | 19 | DATE=`date +%Y-%m-%d` 20 | 21 | BACKUP_FILE=$BACKUP_TARGET_DIR/keeper-data-${DATE}.tgz 22 | 23 | echo "Backup data to: $BACKUP_FILE ..." 24 | 25 | tar czf $BACKUP_FILE $APP_STORAGE_LOCAL_DIR --exclude=$APP_STORAGE_LOCAL_DIR/exports 26 | if [ $? != 0 ]; then 27 | echo "Error, unable to backup files" 28 | exit 1 29 | fi 30 | 31 | echo "Removing old backup(s)..." 32 | (ls -t $BACKUP_TARGET_DIR/keeper-data*|head -n 2;ls $BACKUP_TARGET_DIR/keeper-data*)|sort|uniq -u|xargs rm -rv 33 | 34 | echo "Backup: [done]" 35 | -------------------------------------------------------------------------------- /var/stubs/mappings/auth_realms_nunux-keeper_clients-registrations_default_dee8f1f8-f936-4957-ad43-14ba06a7db62-bd386ab0-bfbf-4860-baec-b6573eb2e38c.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "bd386ab0-bfbf-4860-baec-b6573eb2e38c", 3 | "name" : "auth_realms_nunux-keeper_clients-registrations_default_dee8f1f8-f936-4957-ad43-14ba06a7db62", 4 | "request" : { 5 | "url" : "/auth/realms/nunux-keeper/clients-registrations/default/dee8f1f8-f936-4957-ad43-14ba06a7db62", 6 | "method" : "DELETE" 7 | }, 8 | "response" : { 9 | "status" : 204, 10 | "headers" : { 11 | "Server" : "nginx/1.6.2", 12 | "Date" : "Sun, 03 Dec 2017 23:13:13 GMT", 13 | "Connection" : "keep-alive" 14 | } 15 | }, 16 | "uuid" : "bd386ab0-bfbf-4860-baec-b6573eb2e38c", 17 | "persistent" : true 18 | } -------------------------------------------------------------------------------- /var/templates/rss.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= _links.self.href %> 4 | <![CDATA[Nunux Keeper public feed]]> 5 | Nunux Keeper 6 | <% if (hits.length) { %> 7 | <%= hits[0].date %> 8 | <% } %> 9 | 10 | See source copyright 11 | https://keeper.nunux.org/icons/icon-192x192.png 12 | Nunux Keeper 13 | <% _.forEach(hits, function(hit) { %> 14 | 15 | <%= hit.id %> 16 | <![CDATA[<%= hit.title %>]]> 17 | <%= hit.date %> 18 | 19 | ]]> 20 | 21 | <% }); %> 22 | 23 | --------------------------------------------------------------------------------