├── .dockerignore ├── .env.example ├── .gitignore ├── .gitmodules ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── Vagrantfile ├── assets ├── README.md ├── app │ └── index.js ├── css │ └── style.css ├── default.json ├── favicon.png ├── index.html └── js │ ├── download.js │ └── main.js ├── cache └── .gitkeep ├── controller ├── changePassword.js ├── checkSession.js ├── createSession.js ├── createUser.js ├── deleteSession.js ├── deleteUser.js ├── index.js ├── renderMembersPage.js ├── renderResume.js ├── showStats.js ├── updateTheme.js └── upsertResume.js ├── csrf.js ├── editor ├── .gitignore ├── Gruntfile.js ├── README.md ├── css │ ├── bootstrap.css │ └── style.css ├── favicon.png ├── index.html ├── js │ ├── libs.min.js │ ├── libs │ │ ├── download.js │ │ ├── handlebars.runtime.js │ │ ├── jquery.js │ │ ├── jquery │ │ │ ├── bootstrap.js │ │ │ ├── jquery-ui.js │ │ │ └── jquery.trackpad-scroll-emulator.js │ │ ├── lodash.js │ │ └── lodash │ │ │ └── backbone.js │ └── main.js ├── json-builder │ ├── .gitignore │ ├── Gruntfile.js │ ├── README.md │ ├── build.sh │ ├── css │ │ └── json-builder.css │ ├── package.json │ ├── src │ │ ├── builder.js │ │ └── builder.templates.js │ └── templates │ │ ├── array.tpl │ │ ├── object.tpl │ │ └── string.tpl ├── json │ ├── resume.json │ └── schema.json └── package.json ├── lib ├── mongoose-connection │ └── index.js └── redis-connection │ └── index.js ├── middleware └── allow-cross-domain.js ├── models ├── resume.js └── user.js ├── newrelic.js ├── package.json ├── provision ├── .profile_additions ├── bootstrap.sh ├── constants.sh └── utils.sh ├── server.js ├── template-helper.js ├── templates ├── email │ └── welcome.html ├── home.template ├── layout.template ├── members.template ├── noresume.template └── password.template └── test ├── account-test.js ├── fixtures.js ├── index.js ├── integration ├── changePassword.js ├── checkSession.js ├── createSession.js ├── createUser.js ├── deleteSession.js ├── deleteUser.js ├── index-test.js ├── renderResume.js ├── showStats.js ├── updateTheme.js └── upsertResume.js ├── resume.json └── utils.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MONGOHQ_URL= 2 | POSTMARK_API_KEY= 3 | PUSHER_KEY= 4 | PUSHER_SECRET= 5 | REDISTOGO_URL= 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .vagrant 4 | cache/* 5 | 6 | # Environment 7 | .env* 8 | !.env.example 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "editor"] 2 | path = editor 3 | url = https://github.com/erming/resume-editor 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: trusty 3 | node_js: 4 | - "6" 5 | - "8" 6 | services: 7 | - mongodb 8 | - redis-server 9 | env: 10 | - MONGOHQ_URL=mongodb://localhost:27017/jsonresume-tests 11 | git: 12 | submodules: false 13 | depth: 10 14 | before_install: 15 | - git submodule update --init --recursive 16 | before_script: 17 | - echo Give MongoDB time to start accepting connections 18 | - sleep 15 19 | notifications: 20 | email: 21 | on_success: never 22 | on_failure: always 23 | webhooks: 24 | urls: 25 | - https://webhooks.gitter.im/e/45ac156371db25c81ab9 26 | on_start: always 27 | sudo: false # use container-based architecture; starts up faster 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | From ubuntu:16.04 2 | 3 | MAINTAINER Peter Dave Hello 4 | ENV DEBIAN_FRONTEND noninteractive 5 | 6 | # Pick a Ubuntu apt mirror site for better speed 7 | # ref: https://launchpad.net/ubuntu/+archivemirrors 8 | ENV UBUNTU_APT_SITE ubuntu.cs.utah.edu 9 | ENV UBUNTU_APT_SITE ftp.ubuntu-tw.org 10 | 11 | # Disable src package source 12 | RUN sed -i 's/^deb-src\ /\#deb-src\ /g' /etc/apt/sources.list 13 | 14 | # Replace origin apt pacakge site with the mirror site 15 | RUN sed -E -i "s/([a-z]+.)?archive.ubuntu.com/$UBUNTU_APT_SITE/g" /etc/apt/sources.list 16 | RUN sed -i "s/security.ubuntu.com/$UBUNTU_APT_SITE/g" /etc/apt/sources.list 17 | 18 | RUN apt update && \ 19 | apt upgrade -y && \ 20 | apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \ 21 | locales \ 22 | mongodb-server \ 23 | mongodb-clients \ 24 | redis-server \ 25 | coreutils \ 26 | util-linux \ 27 | bsdutils \ 28 | file \ 29 | openssl \ 30 | ca-certificates \ 31 | ssh \ 32 | wget \ 33 | patch \ 34 | sudo \ 35 | htop \ 36 | dstat \ 37 | vim \ 38 | tmux \ 39 | curl \ 40 | git \ 41 | jq \ 42 | bash-completion && \ 43 | apt clean && \ 44 | rm -rf /var/lib/apt/lists/* 45 | 46 | RUN locale-gen en_US.UTF-8 47 | 48 | COPY . /registry-server 49 | WORKDIR /registry-server 50 | 51 | RUN git submodule update --init --recursive --depth 1 52 | RUN service mongodb start && mongo 127.0.0.1:27017/jsonresume --eval "db.resumes.insert({})" 53 | RUN curl -o- https://cdn.rawgit.com/creationix/nvm/v0.33.2/install.sh | bash && \ 54 | bash -c 'source $HOME/.nvm/nvm.sh && \ 55 | nvm install 4 && \ 56 | nvm cache clear && \ 57 | npm install --prefix "/registry-server"' 58 | 59 | EXPOSE 3000 5000 60 | 61 | ENTRYPOINT /bin/bash 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 JSON Resume 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Resume Registry Server 2 | 3 | [![Join the chat at https://gitter.im/jsonresume/public](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jsonresume/public?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/jsonresume/registry-server.svg?branch=master)](https://travis-ci.org/jsonresume/registry-server) [![Dependency Status](https://david-dm.org/jsonresume/registry-server.svg)](https://david-dm.org/jsonresume/registry-server) [![devDependency Status](https://david-dm.org/jsonresume/registry-server/dev-status.svg)](https://david-dm.org/jsonresume/registry-server#info=devDependencies) 4 | [![Issue Stats](https://www.issuestats.com/github/jsonresume/registry-server/badge/pr?style=flat)](https://www.issuestats.com/github/jsonresume/registry-server) [![Issue Stats](https://www.issuestats.com/github/jsonresume/registry-server/badge/issue?style=flat)](https://www.issuestats.com/github/jsonresume/registry-server) 5 | 6 | 7 | 8 | ## Installation 9 | 10 | Requirements: MongoDB, Redis 11 | 12 | 1. Clone the repository 13 | 1. `npm install` 14 | 1. `git submodule update --init --recursive --depth 1` 15 | 1. `mongo 127.0.0.1:27017/jsonresume --eval "db.resumes.insert({})"` 16 | 1. `POSTMARK_API_KEY=1234567889 MONGOHQ_URL=mongodb://127.0.0.1:27017/jsonresume node server.js` 17 | 18 | *Alternatively:* 19 | 20 | Requirements: Vagrant, VirtualBox(or other providers) 21 | 22 | 1. Clone the repository 23 | 1. `vagrant up` 24 | 1. `vagrant ssh` 25 | 1. `POSTMARK_API_KEY=1234567889 MONGOHQ_URL=mongodb://127.0.0.1:27017/jsonresume node server.js` 26 | 27 | ## Testing 28 | 29 | To run the tests, simply run: 30 | 31 | npm test 32 | 33 | ## Documentation 34 | For additional documentation please see the [Wiki page](https://github.com/jsonresume/resume-docs/wiki/Registry-Server). 35 | 36 | ## License 37 | 38 | Available under [the MIT license](https://mths.be/mit). 39 | 40 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | VAGRANTFILE_API_VERSION = "2" 5 | 6 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 7 | 8 | config.vm.box = "ubuntu/trusty64" 9 | 10 | config.vm.provision :shell, path: "./provision/bootstrap.sh", privileged: false 11 | 12 | config.vm.network "forwarded_port", guest: 5000, host: 5000 13 | 14 | end 15 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # Resume Edit 2 | 3 | This is the live editor for https://jsonresume.org/ 4 | 5 | ### Preview 6 | 7 | It's currently under development, but feel free to try it out: 8 | https://erming.github.io/resume-edit 9 | -------------------------------------------------------------------------------- /assets/app/index.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/css/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | body { 6 | background: #eee; 7 | color: #222; 8 | margin: 0; 9 | } 10 | a { 11 | color: inherit; 12 | } 13 | a:hover { 14 | color: #aaa; 15 | text-decoration: none; 16 | } 17 | h3 { 18 | margin: 0; 19 | font-size: 18px; 20 | } 21 | button { 22 | background: none; 23 | border: 0; 24 | } 25 | .preload * { 26 | transition: none !important; 27 | } 28 | .row { 29 | margin: 0; 30 | } 31 | #sidebar { 32 | background: #f5f5f5; 33 | border-right: 1px solid #ccc; 34 | bottom: 0; 35 | left: -0; 36 | position: absolute; 37 | top: 0; 38 | width: 320px; 39 | z-index: 1; 40 | } 41 | #edit { 42 | height: 100%; 43 | overflow-x: hidden; 44 | overflow-y: auto; 45 | } 46 | #edit label { 47 | margin: 0; 48 | } 49 | #edit input.no-border, 50 | #edit label:last-child input { 51 | border: 0; 52 | } 53 | #edit input, 54 | #edit textarea { 55 | border: 0; 56 | border-bottom: 1px solid #ddd; 57 | font-size: 13px; 58 | font-weight: normal; 59 | margin: 2px 0 0; 60 | outline: 0; 61 | padding: 6px 0px; 62 | resize: none; 63 | transition: border-color .2s; 64 | width: 100%; 65 | } 66 | #edit textarea { 67 | resize: vertical; 68 | } 69 | #edit [class*='col-xs-'] { 70 | padding: 0 10px; 71 | } 72 | #edit .actions { 73 | background: #f5f5f5; 74 | border-bottom: 1px solid #ddd; 75 | height: 51px; 76 | padding: 12px 15px 0; 77 | } 78 | #edit .actions #export + .button { 79 | margin-left: 5px; 80 | } 81 | #edit .actions #export:before { 82 | content: "\f019"; 83 | font: 12px FontAwesome; 84 | } 85 | #edit .theme { 86 | background: #fff; 87 | padding: 20px 15px 25px; 88 | } 89 | #edit .theme .dropdown button { 90 | border: 1px solid #ddd; 91 | border-radius: 2px; 92 | margin-top: 5px; 93 | text-align: left; 94 | padding: 5px 10px; 95 | text-transform: capitalize; 96 | transition: border-color .2s; 97 | width: 100%; 98 | } 99 | #edit .theme .dropdown.open button, 100 | #edit .theme .dropdown button:hover { 101 | border: 1px solid #aaa; 102 | } 103 | #edit .theme .dropdown button:before { 104 | content: "\f0d7"; 105 | color: #ccc; 106 | font: 11px FontAwesome; 107 | float: right; 108 | position: absolute; 109 | margin: 5px 15px 0 0; 110 | right: 0; 111 | } 112 | #edit .theme .dropdown.open button:before, 113 | #edit .theme .dropdown button:hover:before { 114 | color: #888; 115 | } 116 | #edit .theme .dropdown-menu { 117 | border: 1px solid #aaa; 118 | border-radius: 2px; 119 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.075); 120 | width: 100%; 121 | text-transform: capitalize; 122 | } 123 | #edit .theme .dropdown-menu .version { 124 | float: right; 125 | font-family: monospace; 126 | font-size: 11px; 127 | line-height: 21px; 128 | color: #999; 129 | } 130 | #edit .header, 131 | #edit .content { 132 | background: #fff; 133 | border-bottom: 1px solid #eee; 134 | } 135 | #edit .header { 136 | background: #f5f5f5; 137 | border-bottom-color: #ccc; 138 | color: #444; 139 | font-size: 14px; 140 | font-weight: bold; 141 | cursor: pointer; 142 | padding: 12px 20px; 143 | position: relative; 144 | transition: border .3s linear; 145 | -webkit-user-select: none; 146 | -moz-user-select: none; 147 | -ms-user-select: none; 148 | user-select: none; 149 | } 150 | #edit .header:hover { 151 | background: #f5f5f5; 152 | } 153 | #edit .header:hover:before { 154 | color: #666; 155 | } 156 | #edit .header:before { 157 | content: "\f0d8"; 158 | color: #888; 159 | font: 11px FontAwesome; 160 | position: absolute; 161 | margin-top: 7px; 162 | right: 20px; 163 | } 164 | #edit .collapsed { 165 | background: #fff; 166 | border-bottom-color: #eee; 167 | } 168 | #edit .collapsed:before { 169 | content: "\f0d7"; 170 | color: #ccc; 171 | } 172 | #edit .content strong { 173 | font-weight: normal; 174 | font-size: 11px; 175 | color: #999; 176 | } 177 | #edit .content .row { 178 | padding: 12px 10px; 179 | } 180 | #edit .array, 181 | #edit .placeholder { 182 | background: #fff; 183 | border: 1px solid #d4d4d4; 184 | border-radius: 2px; 185 | margin: 5px 5px 10px; 186 | overflow: hidden; 187 | } 188 | #edit .handle { 189 | background-color: #ececec; 190 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f4f4f4), to(#ececec)); 191 | background-image: -moz-linear-gradient(#f4f4f4, #ececec); 192 | background-image: linear-gradient(#f4f4f4, #ececec); 193 | cursor: row-resize; 194 | font-size: 11px; 195 | padding: 10px 10px; 196 | position: relative; 197 | } 198 | #edit .handle:before { 199 | float: right; 200 | content: "\f0c9"; 201 | font: 14px FontAwesome; 202 | margin-right: 5px; 203 | color: #ddd; 204 | text-align: center; 205 | text-shadow: 0 1px #fff; 206 | position: absolute; 207 | transform: scale(2, 1); 208 | width: 93%; 209 | } 210 | #edit .handle .button { 211 | border: none; 212 | color: #ccc; 213 | float: right; 214 | margin-right: -6px; 215 | margin-top: -6px; 216 | } 217 | #edit .handle .button:hover { 218 | border: inherit; 219 | color: #fff; 220 | } 221 | #edit .array .collapse { 222 | border-top: 1px solid #d4d4d4; 223 | display: block; 224 | padding-bottom: 10px; 225 | overflow: hidden; 226 | } 227 | #edit .add.button { 228 | margin: 0 5px 5px; 229 | } 230 | #footer { 231 | border-top: 1px solid #ddd; 232 | color: #aaa; 233 | text-align: center; 234 | margin-top: -1px; 235 | padding: 30px 0; 236 | position: relative; 237 | } 238 | #footer a:hover { 239 | color: #222; 240 | } 241 | #sidebar.ui-resizable-resizing + #preview #overlay { 242 | width: 100%; 243 | } 244 | #preview { 245 | background: #fff; 246 | bottom: 0; 247 | left: 320px; 248 | overflow: hidden; 249 | position: absolute; 250 | right: 0; 251 | transition: opacity .3s; 252 | top: 0; 253 | } 254 | #preview.loading { 255 | opacity: .5; 256 | } 257 | #preview #overlay { 258 | bottom: 0; 259 | left: 0; 260 | position: absolute; 261 | top: 0; 262 | width: 5px; 263 | } 264 | #preview #iframe { 265 | border: 0; 266 | height: 100%; 267 | margin: 0; 268 | width: 100%; 269 | } 270 | 271 | .ui-resizable { 272 | position: relative; 273 | } 274 | .ui-resizable-handle { 275 | position: absolute; 276 | font-size: 0.1px; 277 | display: block; 278 | -ms-touch-action: none; 279 | touch-action: none; 280 | } 281 | .ui-resizable-disabled .ui-resizable-handle, 282 | .ui-resizable-autohide .ui-resizable-handle { 283 | display: none; 284 | } 285 | .ui-resizable-e { 286 | cursor: col-resize; 287 | width: 7px; 288 | right: -4px; 289 | top: 0; 290 | height: 100%; 291 | position: absolute; 292 | } 293 | .ui-resizable-e:hover { 294 | background: rgba(0, 0, 0, .05); 295 | } 296 | 297 | .button { 298 | position: relative; 299 | overflow: visible; 300 | display: inline-block; 301 | padding: 0.5em 1em; 302 | border: 1px solid #d4d4d4; 303 | margin: 0; 304 | text-decoration: none; 305 | text-align: center; 306 | text-shadow: 1px 1px 0 #fff; 307 | font:11px/normal sans-serif; 308 | color: #333; 309 | white-space: nowrap; 310 | cursor: pointer; 311 | outline: none; 312 | background-color: #ececec; 313 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f4f4f4), to(#ececec)); 314 | background-image: -moz-linear-gradient(#f4f4f4, #ececec); 315 | background-image: -ms-linear-gradient(#f4f4f4, #ececec); 316 | background-image: -o-linear-gradient(#f4f4f4, #ececec); 317 | background-image: linear-gradient(#f4f4f4, #ececec); 318 | -moz-background-clip: padding; /* for Firefox 3.6 */ 319 | background-clip: padding-box; 320 | border-radius: 0.2em; 321 | /* IE hacks */ 322 | zoom: 1; 323 | *display: inline; 324 | } 325 | .button:hover { 326 | border-color: #3072b3; 327 | border-bottom-color: #2a65a0; 328 | text-decoration: none; 329 | text-shadow: -1px -1px 0 rgba(0,0,0,0.3); 330 | color: #fff; 331 | background-color: #3c8dde; 332 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#599bdc), to(#3072b3)); 333 | background-image: -moz-linear-gradient(#599bdc, #3072b3); 334 | background-image: -o-linear-gradient(#599bdc, #3072b3); 335 | background-image: linear-gradient(#599bdc, #3072b3); 336 | } 337 | .button:active { 338 | border-color: #2a65a0; 339 | border-bottom-color: #3884cd; 340 | background-color: #3072b3; 341 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#3072b3), to(#599bdc)); 342 | background-image: -moz-linear-gradient(#3072b3, #599bdc); 343 | background-image: -ms-linear-gradient(#3072b3, #599bdc); 344 | background-image: -o-linear-gradient(#3072b3, #599bdc); 345 | background-image: linear-gradient(#3072b3, #599bdc); 346 | } 347 | .button.danger { 348 | color: #900; 349 | } 350 | .button.danger:hover, 351 | .button.danger:active { 352 | border-color: #b53f3a; 353 | border-bottom-color: #a0302a; 354 | color: #fff; 355 | background-color: #dc5f59; 356 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#dc5f59), to(#b33630)); 357 | background-image: -moz-linear-gradient(#dc5f59, #b33630); 358 | background-image: -ms-linear-gradient(#dc5f59, #b33630); 359 | background-image: -o-linear-gradient(#dc5f59, #b33630); 360 | background-image: linear-gradient(#dc5f59, #b33630); 361 | } 362 | .button::-moz-focus-inner { 363 | padding: 0; 364 | border: 0; 365 | } 366 | 367 | .user-modal label { 368 | font-weight: bold; 369 | display: block; 370 | margin-bottom: 5px; 371 | } 372 | -------------------------------------------------------------------------------- /assets/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "bio": { 3 | "firstName": "Richard", 4 | "lastName": "Hendriks", 5 | "email": { 6 | "work": "richard@piedpiper.com", 7 | "personal": "richard.hendriks@gmail.com" 8 | }, 9 | "phone": { 10 | "work": "(912) 555-1234", 11 | "personal": "(912) 555-4321" 12 | }, 13 | "summary": "Richard hails from Tulsa. He has earned degrees from the University of Oklahoma and Stanford. (Go Sooners and Cardinal!) Before starting Pied Piper, he worked for Hooli as a part time software developer. While his work focuses on applied information theory, mostly optimizing lossless compression schema of both the length-limited and adaptive variants, his non-work interests range widely, everything from quantum computing to chaos theory. He could tell you about it, but THAT would NOT be a “length-limited” conversation!", 14 | "location": { 15 | "city": "San Francisco", 16 | "countryCode": "US", 17 | "state": "California" 18 | }, 19 | "websites": { 20 | "blog": "https://richardhendricks.com" 21 | }, 22 | "profiles": { 23 | "github": "richardhendricks", 24 | "twitter": "richardhendricks" 25 | } 26 | }, 27 | "work": [{ 28 | "startDate": "2013-12-01", 29 | "position": "CEO/President", 30 | "company": "Pied Piper", 31 | "website": "https://piedpiper.com", 32 | "summary": "Pied Piper is a multi-platform technology based on a proprietary universal compression algorithm that has consistently fielded high Weisman Scores™ that are not merely competitive, but approach the theoretical limit of lossless compression.", 33 | "highlights": [ 34 | "Build an algorithm for artist to detect if their music was violating copy right infringement laws", 35 | "Successfully won Techcrunch Disrupt", 36 | "Optimized an algorithm that holds the current world record for Weisman Scores" 37 | ] 38 | }], 39 | "education": [{ 40 | "institution": "University of Oklahoma", 41 | "startDate": "2011-06-01", 42 | "endDate": "2014-01-01", 43 | "area": "Information Technology", 44 | "studyType": "Bachelor", 45 | "courses": ["DB1101 - Basic SQL", "CS2011 - Java Introduction"] 46 | }], 47 | "awards": [{ 48 | "title": "Digital Compression Pioneer Award", 49 | "date": "2014-11-01", 50 | "awarder": "Techcrunch" 51 | }], 52 | "publications": [{ 53 | "name": "Video compression for 3d media", 54 | "publisher": "Hooli", 55 | "releaseDate": "2014-10-01", 56 | "website": "https://en.wikipedia.org/wiki/Silicon_Valley_(TV_series)" 57 | }], 58 | "skills": [{ 59 | "name": "Web Development", 60 | "level": "Master", 61 | "keywords": ["HTML", "CSS", "Javascript"] 62 | },{ 63 | "name": "Compression", 64 | "level": "Master", 65 | "keywords": ["Mpeg", "MP4", "GIF"] 66 | }], 67 | "references": [{ 68 | "name": "Erlich Bachman", 69 | "reference": "It is my pleasure to recommend Richard, his performance working as a consultant for Main St. Company proved that he will be a valuable addition to any company." 70 | }] 71 | } 72 | -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonresume/registry-server/41a8d93e937cd4a38a7b64e24787857accd559a4/assets/favicon.png -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Edit 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 297 |
298 |
299 | 300 |
301 | 302 | 303 | 304 | 305 | 325 | 326 | 327 | 328 | 349 | 350 | 351 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | -------------------------------------------------------------------------------- /assets/js/download.js: -------------------------------------------------------------------------------- 1 | // download.js 2 | // https://danml.com/js/download.js 3 | 4 | function download(strData, strFileName, strMimeType) { 5 | var D = document, 6 | a = D.createElement("a"); 7 | strMimeType= strMimeType || "application/octet-stream"; 8 | 9 | if (navigator.msSaveBlob) { // IE10+ 10 | return navigator.msSaveBlob(new Blob([strData], {type: strMimeType}), strFileName); 11 | } /* end if(navigator.msSaveBlob) */ 12 | 13 | if ('download' in a) { //html5 A[download] 14 | if(window.URL){ 15 | a.href= window.URL.createObjectURL(new Blob([strData])); 16 | 17 | }else{ 18 | a.href = "data:" + strMimeType + "," + encodeURIComponent(strData); 19 | } 20 | a.setAttribute("download", strFileName); 21 | a.innerHTML = "downloading..."; 22 | D.body.appendChild(a); 23 | setTimeout(function() { 24 | a.click(); 25 | D.body.removeChild(a); 26 | if(window.URL){setTimeout(function(){ window.URL.revokeObjectURL(a.href);}, 250 );} 27 | }, 66); 28 | return true; 29 | } /* end if('download' in a) */ 30 | 31 | 32 | //do iframe dataURL download (old ch+FF): 33 | var f = D.createElement("iframe"); 34 | D.body.appendChild(f); 35 | f.src = "data:" + strMimeType + "," + encodeURIComponent(strData); 36 | 37 | setTimeout(function() { 38 | D.body.removeChild(f); 39 | }, 333); 40 | return true; 41 | } /* end download() */ 42 | -------------------------------------------------------------------------------- /assets/js/main.js: -------------------------------------------------------------------------------- 1 | jq = $; 2 | // some weird jquery bug, themes overriding or something something 3 | // screws up .modal calls 4 | 5 | $(function() { 6 | var edit = $("#edit"); 7 | var inputs = edit.find("input, textarea").val(""); 8 | 9 | var resume = {}; 10 | 11 | function update() { 12 | // Clone resume object. 13 | var json = $.extend({}, 14 | resume 15 | ); 16 | 17 | (function iterate(obj, key) { 18 | if ($.type(obj) == "object") { 19 | for (var i in obj) { 20 | var k = key ? [key, i].join(".") : i; 21 | iterate(obj[i], k); 22 | } 23 | return; 24 | } 25 | 26 | var value = ""; 27 | var self = edit.find("[data-name='" + key + "']"); 28 | if (!self.length) { 29 | return; 30 | } 31 | 32 | if (self.prop("tagName") == "INPUT" || self.prop("tagName") == "TEXTAREA") { 33 | value = self.val(); 34 | } else { 35 | value = []; 36 | self.each(function() { 37 | var self = $(this); 38 | var hash = {}; 39 | self.find("input, textarea").each(function() { 40 | var self = $(this); 41 | hash[self.attr("name")] = self.hasClass("list") ? self.val().split(",") : self.val(); 42 | }); 43 | value.push(hash); 44 | }); 45 | } 46 | 47 | var keys = key.split("."); 48 | (function(obj) { 49 | while (keys.length > 1) { 50 | obj = obj[keys.shift()]; 51 | } 52 | obj[keys[0]] = value; 53 | })(json); 54 | })(json); 55 | 56 | output.html(JSON.stringify(json, null, " ")); 57 | output.trigger("input"); 58 | } 59 | 60 | function resetBuilder(resumeObj) { 61 | (function iterate(obj, key) { 62 | if ($.type(obj) == "object") { 63 | for (var i in obj) { 64 | var k = key ? [key, i].join(".") : i; 65 | iterate(obj[i], k); 66 | } 67 | return; 68 | } else if ($.type(obj) == "array") { 69 | var item = edit.find("[data-name='" + key + "']").eq(0); 70 | if (!item.length) { 71 | return; 72 | } 73 | var clone = item[0].outerHTML; 74 | item.next(".add").data("clone", clone); 75 | for (var i in obj) { 76 | for (var ii in obj[i]) { 77 | var value = obj[i][ii]; 78 | item.find("[name='" + ii + "']").val( 79 | $.type(value) == "array" ? value.join(", ") : value 80 | ); 81 | } 82 | } 83 | return; 84 | } 85 | inputs.each(function() { 86 | var self = $(this); 87 | if (self.data("name") == key) { 88 | self.val(obj); 89 | } 90 | }); 91 | })(resumeObj); 92 | 93 | resume = resumeObj; 94 | update(); 95 | } 96 | 97 | function reset() { 98 | $.getJSON("default.json", function(json) { 99 | resetBuilder(json); 100 | }); 101 | } 102 | 103 | var output = $("#output"); 104 | var timer = null; 105 | 106 | output.on("input", function() { 107 | clearTimeout(timer); 108 | preview.addClass("loading"); 109 | timer = setTimeout(function() { 110 | edit.trigger("output"); 111 | }, 200); 112 | }); 113 | 114 | $("#export, #save").tooltip({ 115 | container: "body" 116 | }); 117 | 118 | edit.on("input", "input, textarea", function() { 119 | update(); 120 | }); 121 | 122 | edit.on("output", function(e) { 123 | e.preventDefault(); 124 | var json = ""; 125 | try { 126 | var json = JSON.parse(output.val()); 127 | } catch (e) { 128 | return; 129 | } 130 | 131 | var theme = edit.find(".dropdown").data("selected") || "flat"; 132 | var data = JSON.stringify({ 133 | resume: json 134 | }); 135 | 136 | $.ajax({ 137 | type: "POST", 138 | contentType: "application/json", 139 | data: data, 140 | url: "https://themes.jsonresume.org/theme/" + theme, 141 | success: function(html) { 142 | $("#iframe").contents().find("body").html(html); 143 | preview.removeClass("loading"); 144 | } 145 | }); 146 | }); 147 | 148 | var themes = $(".dropdown-menu"); 149 | (function populateThemeMenu() { 150 | themes.empty(); 151 | $.getJSON("https://themes.jsonresume.org/themes.json", function(json) { 152 | if (!json.themes) { 153 | return; 154 | } 155 | for (var t in json.themes) { 156 | var version = $("
" + json.themes[t].versions.pop() + "
"); 157 | var link = $("" + t + "").append(version); 158 | var item = $("
  • ").append(link); 159 | themes.append(item); 160 | } 161 | var hash = window.location.hash; 162 | if (hash != "") { 163 | var theme = edit.find(".dropdown a[href='" + hash + "']"); 164 | theme.trigger("click"); 165 | } 166 | }); 167 | })(); 168 | 169 | edit.on("click", "input", function() { 170 | $(this).select(); 171 | }); 172 | 173 | edit.on("click", ".dropdown a", function() { 174 | var theme = $(this).attr("href").substring(1); 175 | edit.find(".dropdown").data("selected", theme).find("button").html(theme); 176 | edit.trigger("output"); 177 | }); 178 | 179 | edit.on("click", ".add", function() { 180 | var self = $(this); 181 | var clone = self.data("clone"); 182 | if (clone) { 183 | self.before(clone); 184 | } 185 | }); 186 | 187 | edit.on("click", ".delete", function() { 188 | var self = $(this); 189 | self.closest(".array").remove(); 190 | update(); 191 | }); 192 | 193 | $("#reset").on("click", function() { 194 | if (confirm("Are you sure you want to start over? This action will clear the input fields and reload the theme.")) { 195 | reset(); 196 | } 197 | }); 198 | 199 | $("#export").on("click", function() { 200 | download(output.val(), "resume.json", "text/plain"); 201 | }); 202 | 203 | var rows = $(".row"); 204 | 205 | rows.sortable({ 206 | containment: "parent", 207 | items: ".array", 208 | handle: ".handle", 209 | placeholder: "placeholder", 210 | forcePlaceholderSize: true, 211 | scroll: false, 212 | update: function() { 213 | update(); 214 | } 215 | }); 216 | 217 | rows.on("click", ".handle", function() { 218 | var self = $(this); 219 | self.next(".collapse").toggle(); 220 | }); 221 | 222 | var preview = $("#preview"); 223 | $("#sidebar").resizable({ 224 | handles: "e", 225 | minWidth: 200, 226 | maxWidth: 800 227 | }).on("resize", function(e, ui) { 228 | preview.css({ 229 | left: ui.size.width 230 | }); 231 | }); 232 | 233 | setTimeout(function() { 234 | // Turn on transitions. 235 | $("body").removeClass("preload"); 236 | }, 500); 237 | 238 | 239 | var proxiedSync = Backbone.sync; 240 | 241 | Backbone.sync = function(method, model, options) { 242 | options || (options = {}); 243 | console.log(options, 'a'); 244 | if (!options.crossDomain) { 245 | options.crossDomain = true; 246 | } 247 | 248 | if (!options.xhrFields) { 249 | options.xhrFields = {withCredentials:true}; 250 | } 251 | options.url = 'https://registry.jsonresume.org' + model.url(); 252 | return proxiedSync(method, model, options); 253 | }; 254 | /* Session */ 255 | var SessionModel = Backbone.Model.extend({ 256 | 257 | urlRoot: '/session', 258 | initialize: function() { 259 | var that = this; 260 | // Hook into jquery 261 | 262 | // Use withCredentials to send the server cookies 263 | // The server must allow this through response headers 264 | /* 265 | $.ajaxPrefilter(function(options, originalOptions, jqXHR) { 266 | if (options.url.indexOf('session') !== -1 || options.url.indexOf('user') !== -1) { 267 | options.url = 'https://registry.jsonresume.org/' + options.url; 268 | //options.url = 'https://localhost:5000' + options.url; 269 | options.xhrFields = { 270 | withCredentials: true 271 | }; 272 | // If we have a csrf token send it through with the next request 273 | if (typeof that.get('_csrf') !== 'undefined') { 274 | jqXHR.setRequestHeader('X-CSRF-Token', that.get('_csrf')); 275 | } 276 | } 277 | });*/ 278 | }, 279 | login: function(creds, callback) { 280 | // Do a POST to /session and send the serialized form creds 281 | this.save(creds, { 282 | success: callback 283 | }); 284 | }, 285 | logout: function() { 286 | // Do a DELETE to /session and clear the clientside data 287 | var that = this; 288 | this.destroy({ 289 | success: function(model, resp) { 290 | model.clear() 291 | model.id = null; 292 | // Set auth to false to trigger a change:auth event 293 | // The server also returns a new csrf token so that 294 | // the user can relogin without refreshing the page 295 | that.set({ 296 | auth: false 297 | }); //, _csrf: resp._csrf}); 298 | 299 | } 300 | }); 301 | }, 302 | getAuth: function(callback) { 303 | // getAuth is wrapped around our router 304 | // before we start any routers let us see if the user is valid 305 | this.fetch({ 306 | success: callback 307 | }); 308 | } 309 | }); 310 | var Session = new SessionModel(); 311 | Session.getAuth(function(session) { 312 | 313 | if (session.get('auth')) { 314 | $('#logout-button').fadeIn(200); 315 | $.ajax('https://registry.jsonresume.org/' + session.get('username') + '.json', { 316 | 317 | xhrFields: { 318 | withCredentials: true 319 | }, 320 | success: function(res) { 321 | var resumeObj = res; 322 | 323 | resetBuilder(resumeObj); 324 | var theme = $(".dropdown a[href='#" + resumeObj.jsonresume.theme + "']"); 325 | console.log('a', theme); 326 | theme.trigger("click"); 327 | } 328 | }); 329 | } else { 330 | $('#register-button').fadeIn(200); 331 | $('#login-button').fadeIn(200); 332 | 333 | reset(); 334 | } 335 | //$.ajax('https://localhost:5000/thomasdavis.json', { 336 | /*$.ajax('https://registry.jsonresume.org/'+session.get('username')+'.json', { 337 | success: function (res) { 338 | var resumeObj = res; 339 | console.log(resumeObj); 340 | resetBuilder(resumeObj); 341 | update(); 342 | } 343 | })*/ 344 | //resetBuilder(json); 345 | 346 | }); 347 | Session.on('change:auth', function(session) {}) 348 | $('#login-button').on('click', function(ev) { 349 | jq('#login-modal').modal('show'); 350 | }); 351 | $('#register-button').on('click', function(ev) { 352 | jq('#register-modal').modal('show'); 353 | }); 354 | $('#save-button').on('click', function(ev) { 355 | if(Session.get('auth')) { 356 | var resume = JSON.parse(output.val()); 357 | $('#publish-modal .modal-body').html('

    Publishing...

    '); 358 | 359 | jq('#publish-modal').modal('show'); 360 | 361 | var resumeM = new ResumeModel(); 362 | alert($(".dropdown").data("selected")); 363 | resumeM.save({resume:resume, theme: $(".dropdown").data("selected")}, { 364 | success: function () { 365 | $('#publish-modal .modal-body').html('

    Beautiful! Access your published resume at
    https://registry.jsonresume.org/'+Session.get('username')+'

    '); 366 | 367 | console.log('whoa', arguments); 368 | } 369 | }) 370 | } else { 371 | jq('#register-modal').modal('show'); 372 | } 373 | }); 374 | 375 | $('#logout-button').on('click', function(ev) { 376 | Session.logout(); 377 | 378 | $('#logout-button').toggle(); 379 | $('#register-button').toggle(); 380 | $('#login-button').toggle(); 381 | reset(); 382 | //$('#login-modal').modal('show'); 383 | }); 384 | $('.login-form').on('submit', function(ev) { 385 | var form = $(ev.currentTarget); 386 | var email = $('.login-email', form).val(); 387 | var password = $('.login-password', form).val(); 388 | 389 | Session.login({ 390 | email: email, 391 | password: password 392 | }, function() { 393 | console.log('a'); 394 | jq('#login-modal').modal('hide'); 395 | $.ajax('https://registry.jsonresume.org/' + Session.get('username') + '.json', { 396 | success: function(res) { 397 | var resumeObj = res; 398 | resetBuilder(resumeObj); 399 | } 400 | }); 401 | 402 | $('#logout-button').toggle(); 403 | $('#register-button').toggle(); 404 | $('#login-button').toggle(); 405 | }); 406 | }); 407 | 408 | var UserModel = Backbone.Model.extend({ 409 | urlRoot: '/user' 410 | }); 411 | var ResumeModel = Backbone.Model.extend({ 412 | urlRoot: '/resume' 413 | }); 414 | $('.register-form').on('submit', function(ev) { 415 | var form = $(ev.currentTarget); 416 | var email = $('.register-email', form).val(); 417 | var username = $('.register-username', form).val(); 418 | var password = $('.register-password', form).val(); 419 | var user = new UserModel(); 420 | user.save({email: email, username:username, password:password}, { 421 | success: function () { 422 | $('.register-form .modal-body').html('

    Excellent! We are now saving your first resume....

    '); 423 | $('.register-form .modal-footer').remove(); 424 | var resume = JSON.parse(output.val()); 425 | console.log(resume); 426 | Session.getAuth(function() { 427 | 428 | $('#logout-button').toggle(); 429 | $('#register-button').toggle(); 430 | $('#login-button').toggle(); 431 | var resumeM = new ResumeModel(); 432 | resumeM.save({resume:resume, theme: $(".dropdown").data("selected")}, { 433 | success: function () { 434 | $('.register-form .modal-body').html('

    Beautiful! Access your published resume at
    https://registry.jsonresume.org/'+Session.get('username')+'

    '); 435 | 436 | console.log('whoa', arguments); 437 | } 438 | }) 439 | console.log('hey', arguments); 440 | }) 441 | } 442 | }) 443 | return false; 444 | }); 445 | }); 446 | -------------------------------------------------------------------------------- /cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonresume/registry-server/41a8d93e937cd4a38a7b64e24787857accd559a4/cache/.gitkeep -------------------------------------------------------------------------------- /controller/changePassword.js: -------------------------------------------------------------------------------- 1 | var bcrypt = require('bcrypt-nodejs'); 2 | var User = require('../models/user'); 3 | 4 | module.exports = function changePassword(req, res, next) { 5 | 6 | var email = req.body.email; 7 | var password = req.body.currentPassword; 8 | var hash = bcrypt.hashSync(req.body.newPassword); 9 | 10 | User.findOne({ 11 | 'email': email 12 | }, function(err, user) { 13 | if (err) { 14 | return next(err); 15 | } 16 | 17 | if (!user) { 18 | return res.status(401).json({ //HTTP Error 401 Unauthorized 19 | message: 'email not found' 20 | }); 21 | 22 | } 23 | 24 | if (!bcrypt.compareSync(password, user.hash)) { 25 | return res.status(401).json({ //HTTP Error 401 Unauthorized 26 | message: 'invalid password' 27 | }); 28 | } 29 | 30 | var conditions = { 31 | 'email': email 32 | }; 33 | 34 | var update = { 35 | 'hash': hash 36 | }; 37 | 38 | User.update(conditions, update, function(err, user) { 39 | if (err) { 40 | return next(err); 41 | } 42 | 43 | res.send({ 44 | message: 'password updated' 45 | }); 46 | 47 | }); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /controller/checkSession.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function checkSession(req, res) { 3 | 4 | console.log(req.body); 5 | 6 | // This checks the current users auth 7 | // It runs before Backbones router is started 8 | // we should return a csrf token for Backbone to use 9 | if (typeof req.session.username !== 'undefined') { 10 | res.send({ 11 | auth: true, 12 | id: req.session.id, 13 | username: req.session.username, 14 | _csrf: req.session._csrf 15 | }); 16 | } else { 17 | res.send({ 18 | auth: false, 19 | _csrf: req.session._csrf 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /controller/createSession.js: -------------------------------------------------------------------------------- 1 | var HttpStatus = require('http-status-codes'); 2 | var bcrypt = require('bcrypt-nodejs'); 3 | var User = require('../models/user'); 4 | 5 | module.exports = function createSession(req, res, next) { 6 | 7 | var redis = req.redis 8 | 9 | function uid(len) { 10 | return Math.random().toString(35).substr(2, len); 11 | } 12 | 13 | var password = req.body.password; 14 | var email = req.body.email; 15 | // console.log(req.body); 16 | User.findOne({ 17 | 'email': email 18 | }, function(err, user) { 19 | 20 | if (user && bcrypt.compareSync(password, user.hash)) { 21 | // console.log(email, bcrypt.hashSync(email)); 22 | // console.log(email, bcrypt.hashSync(email)); 23 | var sessionUID = uid(32); 24 | 25 | redis.set(sessionUID, true, redis.print); 26 | 27 | 28 | 29 | // var session = value.toString(); 30 | 31 | req.session.username = user.username; 32 | req.session.email = email; 33 | res.send({ 34 | message: 'loggedIn', 35 | username: user.username, 36 | email: email, 37 | session: sessionUID, 38 | auth: true 39 | }); 40 | 41 | // redis.quit(); 42 | } else { 43 | res.status(HttpStatus.UNAUTHORIZED).send({ 44 | message: 'authentication error' 45 | }); 46 | } 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /controller/createUser.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var bcrypt = require('bcrypt-nodejs'); 3 | var Mustache = require('mustache'); 4 | var postmark = require("postmark")(process.env.POSTMARK_API_KEY); 5 | var User = require('../models/user'); 6 | 7 | module.exports = function userController(req, res, next) { 8 | console.log('hit the user controller'); 9 | // console.log(req.body); 10 | User.findOne({ 11 | 'email': req.body.email 12 | }, function(err, user) { 13 | 14 | if (user) { 15 | res.status(409).json({ // HTTP Status 409 Conflict 16 | error: { 17 | field: 'email', 18 | message: 'Email is already in use, maybe you forgot your password?' 19 | } 20 | }); 21 | } else { 22 | 23 | User.findOne({ 24 | 'username': req.body.username 25 | }, function(err, user) { 26 | 27 | if (user) { 28 | res.status(409).json({ // HTTP Status 409 Conflict 29 | error: { 30 | field: 'username', 31 | message: 'This username is already taken, please try another one' 32 | } 33 | }); 34 | } else { 35 | var emailTemplate = fs.readFileSync('templates/email/welcome.html', 'utf8'); 36 | var emailCopy = Mustache.render(emailTemplate, { 37 | username: req.body.username 38 | }); 39 | var hash = bcrypt.hashSync(req.body.password); 40 | postmark.send({ 41 | "From": "admin@jsonresume.org", 42 | "To": req.body.email, 43 | "Subject": "Json Resume - Community driven HR", 44 | "TextBody": emailCopy 45 | }, function(error, success) { 46 | if (error) { 47 | console.error("Unable to send via postmark: " + error.message); 48 | return; 49 | } 50 | console.info("Sent to postmark for delivery") 51 | }); 52 | 53 | 54 | var newUser = { 55 | username: req.body.username, 56 | email: req.body.email, 57 | hash: hash 58 | }; 59 | 60 | 61 | 62 | User.create(newUser, function(err, user) { 63 | 64 | console.log(err, user, 'create error'); 65 | if (err) { 66 | return next(err); 67 | } 68 | 69 | req.session.username = user.username; 70 | req.session.email = user.email; 71 | // console.log('USER CREATED', req.session, req.session.username); 72 | res.status(201).json({ // HTTP status 201 created 73 | // username: user.username, 74 | email: user.email, 75 | username: user.username, 76 | message: "success" 77 | }); 78 | }); 79 | } 80 | }); 81 | } 82 | 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /controller/deleteSession.js: -------------------------------------------------------------------------------- 1 | var HttpStatus = require('http-status-codes'); 2 | var bcrypt = require('bcrypt-nodejs'); 3 | var User = require('../models/user'); 4 | 5 | module.exports = function deleteSession(req, res, next) { 6 | // Logout by clearing the session 7 | req.session.regenerate(function(err) { 8 | // Generate a new csrf token so the user can login again 9 | // This is pretty hacky, connect.csrf isn't built for rest 10 | // I will probably release a restful csrf module 11 | //csrf.generate(req, res, function () { 12 | res.send({ 13 | auth: false, 14 | _csrf: req.session._csrf 15 | }); 16 | //}); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /controller/deleteUser.js: -------------------------------------------------------------------------------- 1 | var bcrypt = require('bcrypt-nodejs'); 2 | var User = require('../models/user'); 3 | var Resume = require('../models/resume'); 4 | 5 | module.exports = function deleteUser(req, res, next) { 6 | 7 | var password = req.body.password; 8 | var email = req.body.email; 9 | 10 | User.findOne({ 11 | 'email': email 12 | }, function(err, user) { 13 | if (err) { 14 | return next(err); 15 | } 16 | 17 | if (!user) { 18 | res.send({ 19 | message: '\nemail not found' 20 | }); 21 | } else if (user && bcrypt.compareSync(password, user.hash)) { 22 | // console.log(req.body); 23 | 24 | //remove the users resume 25 | Resume.remove({ 26 | 'jsonresume.username': user.username 27 | }, function(err, numberRemoved) { 28 | if(err) { 29 | return next(err); 30 | } 31 | // then remove user 32 | User.remove({ 33 | 'email': email 34 | }, function(err, numberRemoved) { 35 | if (err) { 36 | res.send(err); 37 | } else { 38 | // something like this is probably a better response 39 | // res.sendStatus(204); // Status: 204 No Content 40 | res.send({ 41 | message: '\nYour account has been successfully deleted.' 42 | }); 43 | 44 | } 45 | }); 46 | }); 47 | } else { 48 | res.send({ 49 | message: '\ninvalid Password' 50 | }); 51 | } 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /controller/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('require-directory')(module); 2 | -------------------------------------------------------------------------------- /controller/renderMembersPage.js: -------------------------------------------------------------------------------- 1 | var Mustache = require('mustache'); 2 | var templateHelper = require('../template-helper'); 3 | var User = require('../models/user'); 4 | 5 | module.exports = function renderMembersPage(req, res, next) { 6 | 7 | User.find({}, function(err, docs) { 8 | if (err) { 9 | return next(err); 10 | } 11 | 12 | var usernameArray = []; 13 | 14 | docs.forEach(function(doc) { 15 | usernameArray.push({ 16 | username: doc.username 17 | 18 | }); 19 | }); 20 | 21 | var page = Mustache.render(templateHelper.get('members'), { 22 | usernames: usernameArray 23 | }); 24 | 25 | res.send(page); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /controller/renderResume.js: -------------------------------------------------------------------------------- 1 | var request = require('superagent'); 2 | var Mustache = require('mustache'); 3 | var templateHelper = require('../template-helper'); 4 | var HttpStatus = require('http-status-codes'); 5 | var resumeToText = require('resume-to-text'); 6 | var resumeToMarkdown = require('resume-to-markdown'); 7 | var Pusher = require('pusher'); 8 | var pdf = require('pdfcrowd'); 9 | var client = new pdf.Pdfcrowd('thomasdavis', '7d2352eade77858f102032829a2ac64e'); 10 | var pusher = null; 11 | if (process.env.PUSHER_KEY) { 12 | pusher = new Pusher({ 13 | appId: '83846', 14 | key: process.env.PUSHER_KEY, 15 | secret: process.env.PUSHER_SECRET 16 | }); 17 | }; 18 | var realTimeViews = 0; 19 | var DEFAULT_THEME = 'modern'; 20 | var Resume = require('../models/resume'); 21 | 22 | module.exports = function renderResume(req, res, err) { 23 | realTimeViews++; 24 | 25 | var redis = req.redis; 26 | 27 | redis.get('views', function(err, views) { 28 | if (err) { 29 | redis.set('views', 0); 30 | } else { 31 | redis.set('views', views * 1 + 1, redis.print); 32 | 33 | } 34 | 35 | if (pusher !== null) { 36 | pusher.trigger('test_channel', 'my_event', { 37 | views: views 38 | }); 39 | }; 40 | }); 41 | 42 | var themeName = req.query.theme || DEFAULT_THEME; 43 | var uid = req.params.uid; 44 | var format = req.params.format || req.headers.accept || 'html'; 45 | 46 | Resume.findOne({ 47 | 'jsonresume.username': uid 48 | }, function(err, resume) { 49 | if (err) { 50 | return next(err); 51 | } 52 | 53 | if (resume) resume = resume.toObject(); 54 | 55 | if (!resume) { 56 | var page = Mustache.render(templateHelper.get('noresume'), {}); 57 | res.status(HttpStatus.NOT_FOUND).send(page); 58 | return; 59 | } 60 | if (typeof resume.jsonresume.passphrase === 'string' && typeof req.body.passphrase === 'undefined') { 61 | 62 | var page = Mustache.render(templateHelper.get('password'), {}); 63 | res.send(page); 64 | return; 65 | } 66 | if (typeof req.body.passphrase !== 'undefined' && req.body.passphrase !== resume.jsonresume.passphrase) { 67 | res.send('Password was wrong, go back and try again'); 68 | return; 69 | } 70 | var content = ''; 71 | if (/json/.test(format)) { 72 | if (typeof req.session.username === 'undefined') { 73 | delete resume.jsonresume; // This removes our registry server config vars from the resume.json 74 | } 75 | delete resume._id; // This removes the document id of mongo 76 | content = JSON.stringify(resume, undefined, 4); 77 | res.set({ 78 | 'Content-Type': 'application/json', 79 | 'Content-Length': Buffer.byteLength(content, 'utf8') // TODO - This is a hack to try get the right content length 80 | // http://stackoverflow.com/questions/17922748/what-is-the-correct-method-for-calculating-the-content-length-header-in-node-js 81 | }); 82 | 83 | res.send(content); 84 | } else if (/txt/.test(format)) { 85 | content = resumeToText(resume, function(plainText) { 86 | res.set({ 87 | 'Content-Type': 'text/plain', 88 | 'Content-Length': plainText.length 89 | }); 90 | res.send(200, plainText); 91 | }); 92 | } else if (/md/.test(format)) { 93 | console.log(resume); 94 | resumeToMarkdown(resume, function(markdown, errs) { 95 | // TODO fix resumeToMarkdown validation errors 96 | console.log(markdown, errs); 97 | res.set({ 98 | 'Content-Type': 'text/plain', 99 | 'Content-Length': markdown.length 100 | }); 101 | res.send(markdown); 102 | }) 103 | } else if (/pdf/.test(format)) { 104 | // this code is used for web-based pdf export such as http://registry.jsonresume.org/thomasdavis.pdf - see line ~310 for resume-cli export 105 | console.log('Come on PDFCROWD'); 106 | var theme = req.query.theme || resume.jsonresume.theme || themeName; 107 | request 108 | .post('https://themes.jsonresume.org/theme/' + theme) 109 | .send({ 110 | resume: resume 111 | }) 112 | .set('Content-Type', 'application/json') 113 | .end(function(err, response) { 114 | client.convertHtml(response.text, pdf.sendHttpResponse(res, null, uid + ".pdf"), { 115 | use_print_media: "true" 116 | }); 117 | 118 | }); 119 | 120 | 121 | } else { 122 | var theme = req.query.theme || resume.jsonresume.theme || themeName; 123 | request 124 | .post('https://themes.jsonresume.org/theme/' + theme) 125 | .send({ 126 | resume: resume 127 | }) 128 | .set('Content-Type', 'application/json') 129 | .end(function(err, response) { 130 | if (err) res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(err); 131 | else res.send(response.text); 132 | }); 133 | /* 134 | resumeToHTML(resume, { 135 | 136 | }, function(content, errs) { 137 | console.log(content, errs); 138 | var page = Mustache.render(templateHelper.get('layout'), { 139 | output: content, 140 | resume: resume, 141 | username: uid 142 | }); 143 | res.send(content); 144 | }); 145 | */ 146 | } 147 | }); 148 | }; 149 | -------------------------------------------------------------------------------- /controller/showStats.js: -------------------------------------------------------------------------------- 1 | var User = require('../models/user'); 2 | var Resume = require('../models/resume'); 3 | 4 | module.exports = function stats(req, res, next) { 5 | 6 | var redis = req.redis 7 | 8 | redis.get('views', function(err, views) { 9 | User.count({}, function(err, usercount) { 10 | if (err) { 11 | return next(err); 12 | } 13 | 14 | Resume.count({}, function(err, resumecount) { 15 | if (err) { 16 | return next(err); 17 | } 18 | 19 | res.send({ 20 | userCount: usercount, 21 | resumeCount: resumecount, 22 | views: views * 1 23 | }); 24 | }); 25 | }); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /controller/updateTheme.js: -------------------------------------------------------------------------------- 1 | var bcrypt = require('bcrypt-nodejs'); 2 | var User = require('../models/user'); 3 | var Resume = require('../models/resume'); 4 | 5 | module.exports = function updateTheme(req, res, next) { 6 | 7 | var db = req.db; 8 | var redis = req.redis; 9 | 10 | var password = req.body.password; 11 | var email = req.body.email; 12 | var theme = req.body.theme; 13 | console.log(theme, "theme update!!!!!!!!!!!!1111"); 14 | // console.log(req.body); 15 | User.findOne({ 16 | 'email': email 17 | }, function(err, user) { 18 | 19 | redis.get(req.body.session, function(err, valueExists) { 20 | console.log(err, valueExists, 'theme redis'); 21 | 22 | if (!valueExists && user && bcrypt.compareSync(password, user.hash)) { 23 | 24 | Resume.update({ 25 | //query 26 | 'jsonresume.username': user.username 27 | }, { 28 | // update set new theme 29 | 'jsonresume.theme': theme 30 | }, function(err, resume) { 31 | res.send({ 32 | url: 'https://registry.jsonresume.org/' + user.username 33 | }); 34 | }); 35 | } else if (valueExists === null) { 36 | res.send({ 37 | sessionError: 'invalid session' 38 | }); 39 | } else if (valueExists === 'true') { 40 | console.log('redis session success'); 41 | Resume.update({ 42 | //query 43 | 'jsonresume.username': user.username 44 | }, { 45 | // update set new theme 46 | 'jsonresume.theme': theme 47 | }, function(err, resume) { 48 | res.send({ 49 | url: 'https://registry.jsonresume.org/' + user.username 50 | }); 51 | }); 52 | 53 | } else { 54 | console.log('deleted'); 55 | res.send({ 56 | message: 'authentication error' 57 | }); 58 | } 59 | }); 60 | }); 61 | 62 | } 63 | -------------------------------------------------------------------------------- /controller/upsertResume.js: -------------------------------------------------------------------------------- 1 | var bcrypt = require('bcrypt-nodejs'); 2 | var HttpStatus = require('http-status-codes'); 3 | var User = require('../models/user'); 4 | var Resume = require('../models/resume'); 5 | 6 | function S4() { 7 | return Math.floor((1 + Math.random()) * 0x10000) 8 | .toString(16) 9 | .substring(1); 10 | }; 11 | 12 | module.exports = function upsertResume(req, res, next) { 13 | 14 | var redis = req.redis; 15 | 16 | var password = req.body.password; 17 | var email = req.body.email || req.session.email; 18 | 19 | if (!req.body.guest) { 20 | User.findOne({ 21 | 'email': email 22 | }, function(err, user) { 23 | if (err) { 24 | return next(err); 25 | } 26 | 27 | redis.get(req.body.session, function(err, valueExists) { 28 | var respondWithResume = function() { 29 | 30 | }; 31 | 32 | if ((user && password && bcrypt.compareSync(password, user.hash)) || (typeof req.session.username !== 'undefined') || valueExists) { 33 | var resume = req.body && req.body.resume || {}; 34 | resume.jsonresume = { 35 | username: user.username, 36 | passphrase: req.body.passphrase || null, 37 | theme: req.body.theme || null 38 | }; 39 | 40 | var conditions = { 41 | 'jsonresume.username': user.username 42 | }; 43 | 44 | Resume.update(conditions, resume, { upsert: true, overwrite: true }, function(err, resume) { 45 | if (err) { 46 | return next(err); 47 | } 48 | 49 | res.send({ 50 | url: 'https://registry.jsonresume.org/' + user.username 51 | }); 52 | }); 53 | } else if (valueExists === null) { 54 | res.status(HttpStatus.UNAUTHORIZED).send({ 55 | sessionError: 'invalid session' 56 | }); 57 | } else { 58 | res.status(HttpStatus.INTERNAL_SERVER_ERROR).send({ 59 | message: 'ERRORRRSSSS' 60 | }); 61 | } 62 | }); 63 | }); 64 | } else { 65 | var guestUsername = S4() + S4(); 66 | var resume = req.body && req.body.resume || {}; 67 | resume.jsonresume = { 68 | username: guestUsername, 69 | passphrase: req.body.passphrase || null, 70 | theme: req.body.theme || null 71 | }; 72 | 73 | Resume.create(resume, function(err, resume) { 74 | if (err) { 75 | return next(err); 76 | } 77 | 78 | res.send({ 79 | url: 'https://registry.jsonresume.org/' + guestUsername 80 | }); 81 | }); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /csrf.js: -------------------------------------------------------------------------------- 1 | 2 | // TODO - Write a better csrf module lol 3 | // hacked this together for the tutorial 4 | 5 | var crypto = require('crypto'); 6 | 7 | var generateToken = function(len) { 8 | return crypto.randomBytes(Math.ceil(len * 3 / 4)) 9 | .toString('base64') 10 | .slice(0, len); 11 | }; 12 | function defaultValue(req) { 13 | return (req.body && req.body._csrf) 14 | || (req.query && req.query._csrf) 15 | || (req.headers['x-csrf-token']); 16 | } 17 | var checkToken = function(req, res, next){ 18 | var token = req.session._csrf || (req.session._csrf = generateToken(24)); 19 | if ('GET' == req.method || 'HEAD' == req.method || 'OPTIONS' == req.method) return next(); 20 | var val = defaultValue(req); 21 | if (val != token) return next(function(){ 22 | res.send({auth: false}); 23 | }); 24 | next(); 25 | } 26 | var newToken = function(req, res, next) { 27 | var token = req.session._csrf || (req.session._csrf = generateToken(24)); 28 | next(); 29 | } 30 | module.exports = { 31 | check: checkToken, 32 | generate: newToken 33 | }; 34 | 35 | -------------------------------------------------------------------------------- /editor/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /editor/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | var libs = "js/libs/**/*.js"; 3 | grunt.initConfig({ 4 | uglify: { 5 | options: { 6 | compress: false 7 | }, 8 | js: { 9 | files: { 10 | "js/libs.min.js": libs 11 | } 12 | } 13 | } 14 | }); 15 | grunt.loadNpmTasks("grunt-contrib-uglify"); 16 | grunt.registerTask( 17 | "default", 18 | ["uglify"] 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /editor/README.md: -------------------------------------------------------------------------------- 1 | # Resume Editor 2 | 3 | The live editor available at https://registry.jsonresume.org/ 4 | 5 | ## Development 6 | 7 | ### Using Grunt 8 | 9 | Install [Grunt](https://gruntjs.com/): 10 | 11 | ``` 12 | sudo npm -g install grunt-cli 13 | ``` 14 | 15 | When that is done, run `npm install` while standing in the repository folder. 16 | 17 | You can now concat the files: 18 | 19 | ``` 20 | grunt uglify 21 | ``` 22 | 23 | ## License 24 | 25 | Available under [the MIT license](https://mths.be/mit). 26 | -------------------------------------------------------------------------------- /editor/css/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | body { 6 | background: #eee; 7 | color: #222; 8 | margin: 0; 9 | } 10 | .preload * { 11 | transition: none !important; 12 | } 13 | a { 14 | color: inherit; 15 | } 16 | a:hover { 17 | color: #aaa; 18 | text-decoration: none; 19 | } 20 | a:focus { 21 | text-decoration: none; 22 | } 23 | label { 24 | display: block; 25 | font-weight: normal; 26 | margin: 0; 27 | } 28 | input { 29 | background: none; 30 | } 31 | #sidebar { 32 | background: #fff; 33 | border-right: 1px solid #ccc; 34 | bottom: 0; 35 | left: -0; 36 | position: absolute; 37 | top: 0; 38 | width: 480px; 39 | } 40 | #sidebar #panel { 41 | overflow: hidden; 42 | position: relative; 43 | z-index: 3; 44 | } 45 | #sidebar #panel .inner { 46 | float: right; 47 | height: 170px; 48 | padding: 16px 20px 0; 49 | position: relative; 50 | width: 280px; 51 | } 52 | #sidebar .actions { 53 | overflow: hidden; 54 | } 55 | #sidebar .actions #export { 56 | margin-right: 4px; 57 | } 58 | #sidebar .actions #export:before { 59 | content: "\f019"; 60 | font: 12px FontAwesome; 61 | margin-top: 5px; 62 | } 63 | #sidebar .theme { 64 | color: #999; 65 | padding-top: 20px; 66 | } 67 | #sidebar .theme strong { 68 | color: #000; 69 | display: block; 70 | font-size: 28px; 71 | overflow: hidden; 72 | text-overflow: ellipsis; 73 | text-transform: capitalize; 74 | white-space: nowrap; 75 | } 76 | #sidebar .tabs { 77 | overflow: hidden; 78 | position: absolute; 79 | bottom: 0; 80 | } 81 | #sidebar .tabs a { 82 | border: 1px solid transparent; 83 | border-bottom: 0; 84 | border-radius: 2px 2px 0 0; 85 | float: left; 86 | padding: 6px 10px 6px; 87 | } 88 | #sidebar .tabs a.active { 89 | border-color: #ddd; 90 | background: #fafafa; 91 | } 92 | #sidebar .view { 93 | font-size: 11px; 94 | position: absolute; 95 | bottom: 8px; 96 | right : 10px; 97 | } 98 | #sidebar .view a { 99 | margin-right: 10px; 100 | } 101 | #sidebar .view .active { 102 | color: #d14; 103 | } 104 | #sidebar .tab-pane { 105 | background: #fafafa; 106 | border-top: 1px solid #ddd; 107 | bottom: 0; 108 | left: 0; 109 | opacity: 0.0; 110 | position: absolute; 111 | right: 0; 112 | top: 169px; 113 | transition: opacity .5s; 114 | z-index: 1; 115 | } 116 | #sidebar .tab-pane.active { 117 | opacity: 1.0; 118 | z-index: 2; 119 | } 120 | #sidebar .tse-scrollable { 121 | height: 100%; 122 | width: 100%; 123 | } 124 | #sidebar .tse-scrollbar { 125 | bottom: 6px; 126 | } 127 | #sidebar .tse-content { 128 | float: right; 129 | padding: 20px 20px 40px; 130 | width: 280px; 131 | } 132 | #themes-list .item { 133 | display: block; 134 | padding: 10px 0; 135 | } 136 | #themes-list .item + .item { 137 | border-top: 1px solid #eee; 138 | } 139 | #themes-list .item:hover, 140 | #themes-list .item.active { 141 | color: #d14; 142 | } 143 | #themes-list .item:first-child { 144 | padding-top: 0px; 145 | } 146 | #themes-list .name { 147 | display: block; 148 | font-size: 18px; 149 | overflow: hidden; 150 | text-transform: capitalize; 151 | text-overflow: ellipsis; 152 | white-space: nowrap; 153 | } 154 | #themes-list .version { 155 | color: #ccc; 156 | font-size: 11px; 157 | float: right; 158 | margin-top: 7px; 159 | } 160 | #preview { 161 | background: #fff; 162 | bottom: 0; 163 | left: 480px; 164 | overflow: hidden; 165 | position: absolute; 166 | right: 0; 167 | top: 0; 168 | } 169 | #preview #overlay { 170 | display: none; 171 | height: 100%; 172 | position: absolute; 173 | width: 100%; 174 | } 175 | #preview.scroll #overlay { 176 | display: block; 177 | } 178 | #preview #json-editor { 179 | border: 0; 180 | bottom: 0; 181 | display: none; 182 | left: 0; 183 | font: 12px monospace; 184 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 185 | height: 100%; 186 | outline: 0; 187 | padding: 20px; 188 | position: absolute; 189 | resize: none; 190 | right: 0; 191 | top: 0; 192 | width: 100%; 193 | z-index: 2; 194 | } 195 | #preview #json-editor.show { 196 | display: block; 197 | } 198 | #preview #iframe-wrapper { 199 | height: 100%; 200 | -webkit-overflow-scrolling: touch; 201 | } 202 | #preview #iframe { 203 | border: 0; 204 | height: 100%; 205 | margin: 0; 206 | transition: opacity .3s; 207 | width: 100%; 208 | } 209 | #preview.loading #iframe { 210 | opacity: .5; 211 | } 212 | 213 | .button { 214 | position: relative; 215 | overflow: visible; 216 | display: inline-block; 217 | padding: 0.5em 1em; 218 | border: 1px solid #d4d4d4; 219 | margin: 0; 220 | text-decoration: none; 221 | text-align: center; 222 | text-shadow: 1px 1px 0 #fff; 223 | font:11px/normal sans-serif; 224 | color: #333; 225 | white-space: nowrap; 226 | cursor: pointer; 227 | outline: none; 228 | background-color: #ececec; 229 | background-image: linear-gradient(#f4f4f4, #ececec); 230 | -moz-background-clip: padding; 231 | background-clip: padding-box; 232 | border-radius: 0.2em; 233 | } 234 | .button:hover { 235 | border-color: #3072b3; 236 | border-bottom-color: #2a65a0; 237 | text-decoration: none; 238 | text-shadow: -1px -1px 0 rgba(0,0,0,0.3); 239 | color: #fff; 240 | background-color: #3c8dde; 241 | background-image: linear-gradient(#599bdc, #3072b3); 242 | } 243 | .button:active { 244 | border-color: #2a65a0; 245 | border-bottom-color: #3884cd; 246 | background-color: #3072b3; 247 | background-image: linear-gradient(#3072b3, #599bdc); 248 | } 249 | .button.danger { 250 | color: #900; 251 | } 252 | .button.danger:hover, 253 | .button.danger:active { 254 | border-color: #b53f3a; 255 | border-bottom-color: #a0302a; 256 | color: #fff; 257 | background-color: #dc5f59; 258 | background-image: linear-gradient(#dc5f59, #b33630); 259 | } 260 | .button::-moz-focus-inner { 261 | padding: 0; 262 | border: 0; 263 | } 264 | 265 | .user-modal label { 266 | font-weight: bold; 267 | display: block; 268 | margin-bottom: 5px; 269 | } 270 | 271 | /** 272 | * TrackpadScrollEmulator 273 | * Version: 1.0.6 274 | * Author: Jonathan Nicol @f6design 275 | * https://github.com/jnicol/trackpad-scroll-emulator 276 | */ 277 | .tse-scrollable { 278 | position: relative; 279 | width: 200px; /* Default value. Overwite this if you want. */ 280 | height: 300px; /* Default value. Overwite this if you want. */ 281 | overflow: hidden; 282 | } 283 | .tse-scrollable .tse-scroll-content { 284 | overflow: hidden; 285 | overflow-y: scroll; 286 | -webkit-overflow-scrolling: touch; 287 | } 288 | .tse-scrollable .tse-scroll-content::-webkit-scrollbar { 289 | width: 0; 290 | height: 0; 291 | } 292 | .tse-scrollbar { 293 | z-index: 99; 294 | position: absolute; 295 | top: 0; 296 | right: 0; 297 | bottom: 0; 298 | width: 11px; 299 | } 300 | .tse-scrollbar .drag-handle { 301 | position: absolute; 302 | right: 2px; 303 | -webkit-border-radius: 7px; 304 | -moz-border-radius: 7px; 305 | border-radius: 7px; 306 | min-height: 10px; 307 | width: 7px; 308 | opacity: 0.1; 309 | -webkit-transition: opacity 0.2s linear; 310 | -moz-transition: opacity 0.2s linear; 311 | -o-transition: opacity 0.2s linear; 312 | -ms-transition: opacity 0.2s linear; 313 | transition: opacity 0.2s linear; 314 | background: #6c6e71; 315 | -webkit-background-clip: padding-box; 316 | -moz-background-clip: padding; 317 | } 318 | .tse-scrollbar:hover .drag-handle { 319 | opacity: 0.7; 320 | -webkit-transition: opacity 0 linear; 321 | -moz-transition: opacity 0 linear; 322 | -o-transition: opacity 0 linear; 323 | -ms-transition: opacity 0 linear; 324 | transition: opacity 0 linear; 325 | } 326 | .tse-scrollbar .drag-handle.visible { 327 | opacity: 0.7; 328 | } 329 | .scrollbar-width-tester::-webkit-scrollbar { 330 | width: 0; 331 | height: 0; 332 | } 333 | 334 | @media (max-width: 1260px){ 335 | #sidebar { 336 | width: 320px; 337 | } 338 | #preview { 339 | left: 320px; 340 | } 341 | } 342 | @media (max-device-width: 1024px) { 343 | #iframe-wrapper { 344 | overflow: auto; 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /editor/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonresume/registry-server/41a8d93e937cd4a38a7b64e24787857accd559a4/editor/favicon.png -------------------------------------------------------------------------------- /editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Edit 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 57 |
    58 |
    59 | 60 |
    61 | 62 |
    63 |
    64 | 65 | 84 | 102 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /editor/js/libs/download.js: -------------------------------------------------------------------------------- 1 | // download.js 2 | // https://danml.com/js/download.js 3 | 4 | function download(strData, strFileName, strMimeType) { 5 | var D = document, 6 | a = D.createElement("a"); 7 | strMimeType= strMimeType || "application/octet-stream"; 8 | 9 | if (navigator.msSaveBlob) { // IE10+ 10 | return navigator.msSaveBlob(new Blob([strData], {type: strMimeType}), strFileName); 11 | } /* end if(navigator.msSaveBlob) */ 12 | 13 | if ('download' in a) { //html5 A[download] 14 | if(window.URL){ 15 | a.href= window.URL.createObjectURL(new Blob([strData])); 16 | 17 | }else{ 18 | a.href = "data:" + strMimeType + "," + encodeURIComponent(strData); 19 | } 20 | a.setAttribute("download", strFileName); 21 | a.innerHTML = "downloading..."; 22 | D.body.appendChild(a); 23 | setTimeout(function() { 24 | a.click(); 25 | D.body.removeChild(a); 26 | if(window.URL){setTimeout(function(){ window.URL.revokeObjectURL(a.href);}, 250 );} 27 | }, 66); 28 | return true; 29 | } /* end if('download' in a) */ 30 | 31 | 32 | //do iframe dataURL download (old ch+FF): 33 | var f = D.createElement("iframe"); 34 | D.body.appendChild(f); 35 | f.src = "data:" + strMimeType + "," + encodeURIComponent(strData); 36 | 37 | setTimeout(function() { 38 | D.body.removeChild(f); 39 | }, 333); 40 | return true; 41 | } 42 | -------------------------------------------------------------------------------- /editor/js/libs/handlebars.runtime.js: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | handlebars v2.0.0-alpha.4 4 | 5 | Copyright (C) 2011-2014 by Yehuda Katz 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | @license 26 | */ 27 | /* exported Handlebars */ 28 | this.Handlebars = (function() { 29 | // handlebars/safe-string.js 30 | var __module3__ = (function() { 31 | "use strict"; 32 | var __exports__; 33 | // Build out our basic SafeString type 34 | function SafeString(string) { 35 | this.string = string; 36 | } 37 | 38 | SafeString.prototype.toString = function() { 39 | return "" + this.string; 40 | }; 41 | 42 | __exports__ = SafeString; 43 | return __exports__; 44 | })(); 45 | 46 | // handlebars/utils.js 47 | var __module2__ = (function(__dependency1__) { 48 | "use strict"; 49 | var __exports__ = {}; 50 | /*jshint -W004 */ 51 | var SafeString = __dependency1__; 52 | 53 | var escape = { 54 | "&": "&", 55 | "<": "<", 56 | ">": ">", 57 | '"': """, 58 | "'": "'", 59 | "`": "`" 60 | }; 61 | 62 | var badChars = /[&<>"'`]/g; 63 | var possible = /[&<>"'`]/; 64 | 65 | function escapeChar(chr) { 66 | return escape[chr] || "&"; 67 | } 68 | 69 | function extend(obj /* , ...source */) { 70 | for (var i = 1; i < arguments.length; i++) { 71 | for (var key in arguments[i]) { 72 | if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { 73 | obj[key] = arguments[i][key]; 74 | } 75 | } 76 | } 77 | 78 | return obj; 79 | } 80 | 81 | __exports__.extend = extend;var toString = Object.prototype.toString; 82 | __exports__.toString = toString; 83 | // Sourced from lodash 84 | // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt 85 | var isFunction = function(value) { 86 | return typeof value === 'function'; 87 | }; 88 | // fallback for older versions of Chrome and Safari 89 | if (isFunction(/x/)) { 90 | isFunction = function(value) { 91 | return typeof value === 'function' && toString.call(value) === '[object Function]'; 92 | }; 93 | } 94 | var isFunction; 95 | __exports__.isFunction = isFunction; 96 | var isArray = Array.isArray || function(value) { 97 | return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; 98 | }; 99 | __exports__.isArray = isArray; 100 | 101 | function escapeExpression(string) { 102 | // don't escape SafeStrings, since they're already safe 103 | if (string instanceof SafeString) { 104 | return string.toString(); 105 | } else if (!string && string !== 0) { 106 | return ""; 107 | } 108 | 109 | // Force a string conversion as this will be done by the append regardless and 110 | // the regex test will do this transparently behind the scenes, causing issues if 111 | // an object's to string has escaped characters in it. 112 | string = "" + string; 113 | 114 | if(!possible.test(string)) { return string; } 115 | return string.replace(badChars, escapeChar); 116 | } 117 | 118 | __exports__.escapeExpression = escapeExpression;function isEmpty(value) { 119 | if (!value && value !== 0) { 120 | return true; 121 | } else if (isArray(value) && value.length === 0) { 122 | return true; 123 | } else { 124 | return false; 125 | } 126 | } 127 | 128 | __exports__.isEmpty = isEmpty;function appendContextPath(contextPath, id) { 129 | return (contextPath ? contextPath + '.' : '') + id; 130 | } 131 | 132 | __exports__.appendContextPath = appendContextPath; 133 | return __exports__; 134 | })(__module3__); 135 | 136 | // handlebars/exception.js 137 | var __module4__ = (function() { 138 | "use strict"; 139 | var __exports__; 140 | 141 | var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; 142 | 143 | function Exception(message, node) { 144 | var line; 145 | if (node && node.firstLine) { 146 | line = node.firstLine; 147 | 148 | message += ' - ' + line + ':' + node.firstColumn; 149 | } 150 | 151 | var tmp = Error.prototype.constructor.call(this, message); 152 | 153 | // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. 154 | for (var idx = 0; idx < errorProps.length; idx++) { 155 | this[errorProps[idx]] = tmp[errorProps[idx]]; 156 | } 157 | 158 | if (line) { 159 | this.lineNumber = line; 160 | this.column = node.firstColumn; 161 | } 162 | } 163 | 164 | Exception.prototype = new Error(); 165 | 166 | __exports__ = Exception; 167 | return __exports__; 168 | })(); 169 | 170 | // handlebars/base.js 171 | var __module1__ = (function(__dependency1__, __dependency2__) { 172 | "use strict"; 173 | var __exports__ = {}; 174 | var Utils = __dependency1__; 175 | var Exception = __dependency2__; 176 | 177 | var VERSION = "2.0.0-alpha.4"; 178 | __exports__.VERSION = VERSION;var COMPILER_REVISION = 5; 179 | __exports__.COMPILER_REVISION = COMPILER_REVISION; 180 | var REVISION_CHANGES = { 181 | 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it 182 | 2: '== 1.0.0-rc.3', 183 | 3: '== 1.0.0-rc.4', 184 | 4: '== 1.x.x', 185 | 5: '>= 2.0.0' 186 | }; 187 | __exports__.REVISION_CHANGES = REVISION_CHANGES; 188 | var isArray = Utils.isArray, 189 | isFunction = Utils.isFunction, 190 | toString = Utils.toString, 191 | objectType = '[object Object]'; 192 | 193 | function HandlebarsEnvironment(helpers, partials) { 194 | this.helpers = helpers || {}; 195 | this.partials = partials || {}; 196 | 197 | registerDefaultHelpers(this); 198 | } 199 | 200 | __exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { 201 | constructor: HandlebarsEnvironment, 202 | 203 | logger: logger, 204 | log: log, 205 | 206 | registerHelper: function(name, fn, inverse) { 207 | if (toString.call(name) === objectType) { 208 | if (inverse || fn) { throw new Exception('Arg not supported with multiple helpers'); } 209 | Utils.extend(this.helpers, name); 210 | } else { 211 | if (inverse) { fn.not = inverse; } 212 | this.helpers[name] = fn; 213 | } 214 | }, 215 | unregisterHelper: function(name) { 216 | delete this.helpers[name]; 217 | }, 218 | 219 | registerPartial: function(name, str) { 220 | if (toString.call(name) === objectType) { 221 | Utils.extend(this.partials, name); 222 | } else { 223 | this.partials[name] = str; 224 | } 225 | }, 226 | unregisterPartial: function(name) { 227 | delete this.partials[name]; 228 | } 229 | }; 230 | 231 | function registerDefaultHelpers(instance) { 232 | instance.registerHelper('helperMissing', function(/* [args, ]options */) { 233 | if(arguments.length === 1) { 234 | // A missing field in a {{foo}} constuct. 235 | return undefined; 236 | } else { 237 | // Someone is actually trying to call something, blow up. 238 | throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'"); 239 | } 240 | }); 241 | 242 | instance.registerHelper('blockHelperMissing', function(context, options) { 243 | var inverse = options.inverse || function() {}, fn = options.fn; 244 | 245 | if (isFunction(context)) { context = context.call(this); } 246 | 247 | if(context === true) { 248 | return fn(this); 249 | } else if(context === false || context == null) { 250 | return inverse(this); 251 | } else if (isArray(context)) { 252 | if(context.length > 0) { 253 | if (options.ids) { 254 | options.ids = [options.name]; 255 | } 256 | 257 | return instance.helpers.each(context, options); 258 | } else { 259 | return inverse(this); 260 | } 261 | } else { 262 | if (options.data && options.ids) { 263 | var data = createFrame(options.data); 264 | data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name); 265 | options = {data: data}; 266 | } 267 | 268 | return fn(context, options); 269 | } 270 | }); 271 | 272 | instance.registerHelper('each', function(context, options) { 273 | // Allow for {{#each}} 274 | if (!options) { 275 | options = context; 276 | context = this; 277 | } 278 | 279 | var fn = options.fn, inverse = options.inverse; 280 | var i = 0, ret = "", data; 281 | 282 | var contextPath; 283 | if (options.data && options.ids) { 284 | contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; 285 | } 286 | 287 | if (isFunction(context)) { context = context.call(this); } 288 | 289 | if (options.data) { 290 | data = createFrame(options.data); 291 | } 292 | 293 | if(context && typeof context === 'object') { 294 | if (isArray(context)) { 295 | for(var j = context.length; i
    '); 61 | $scrollbarEl = $el.find('.tse-scrollbar'); 62 | $dragHandleEl = $el.find('.drag-handle'); 63 | 64 | if (options.wrapContent) { 65 | $contentEl.wrap('
    '); 66 | } 67 | $scrollContentEl = $el.find('.tse-scroll-content'); 68 | 69 | resizeScrollContent(); 70 | 71 | if (options.autoHide) { 72 | $el.on('mouseenter', flashScrollbar); 73 | } 74 | 75 | $dragHandleEl.on('mousedown', startDrag); 76 | $scrollbarEl.on('mousedown', jumpScroll); 77 | $scrollContentEl.on('scroll', onScrolled); 78 | 79 | resizeScrollbar(); 80 | 81 | $(window).on('resize', function() { 82 | recalculate(); 83 | }); 84 | 85 | if (!options.autoHide) { 86 | showScrollbar(); 87 | } 88 | } 89 | 90 | /** 91 | * Start scrollbar handle drag 92 | */ 93 | function startDrag(e) { 94 | // Preventing the event's default action stops text being 95 | // selectable during the drag. 96 | e.preventDefault(); 97 | 98 | var self = $(this); 99 | self.trigger('startDrag'); 100 | 101 | // Measure how far the user's mouse is from the top of the scrollbar drag handle. 102 | var eventOffset = e.pageY; 103 | if (scrollDirection === 'horiz') { 104 | eventOffset = e.pageX; 105 | } 106 | dragOffset = eventOffset - $dragHandleEl.offset()[offsetAttr]; 107 | 108 | $(document).on('mousemove', drag); 109 | $(document).on('mouseup', function() { 110 | endDrag.call(self); 111 | }); 112 | } 113 | 114 | /** 115 | * Drag scrollbar handle 116 | */ 117 | function drag(e) { 118 | e.preventDefault(); 119 | 120 | // Calculate how far the user's mouse is from the top/left of the scrollbar (minus the dragOffset). 121 | var eventOffset = e.pageY; 122 | if (scrollDirection === 'horiz') { 123 | eventOffset = e.pageX; 124 | } 125 | var dragPos = eventOffset - $scrollbarEl.offset()[offsetAttr] - dragOffset; 126 | // Convert the mouse position into a percentage of the scrollbar height/width. 127 | var dragPerc = dragPos / $scrollbarEl[sizeAttr](); 128 | // Scroll the content by the same percentage. 129 | var scrollPos = dragPerc * $contentEl[sizeAttr](); 130 | 131 | $scrollContentEl[scrollOffsetAttr](scrollPos); 132 | } 133 | 134 | /** 135 | * End scroll handle drag 136 | */ 137 | function endDrag() { 138 | $(this).trigger('endDrag'); 139 | $(document).off('mousemove', drag); 140 | $(document).off('mouseup', endDrag); 141 | } 142 | 143 | /** 144 | * Scroll in the same manner as the PAGE UP/DOWN keys 145 | */ 146 | function jumpScroll(e) { 147 | // If the drag handle element was pressed, don't do anything here. 148 | if (e.target === $dragHandleEl[0]) { 149 | return; 150 | } 151 | 152 | // The content will scroll by 7/8 of a page. 153 | var jumpAmt = pageJumpMultp * $scrollContentEl[sizeAttr](); 154 | 155 | // Calculate where along the scrollbar the user clicked. 156 | var eventOffset = (scrollDirection === 'vert') ? e.originalEvent.layerY : e.originalEvent.layerX; 157 | 158 | // Get the position of the top (or left) of the drag handle. 159 | var dragHandleOffset = $dragHandleEl.position()[offsetAttr]; 160 | 161 | // Determine which direction to scroll. 162 | var scrollPos = (eventOffset < dragHandleOffset) ? $scrollContentEl[scrollOffsetAttr]() - jumpAmt : $scrollContentEl[scrollOffsetAttr]() + jumpAmt; 163 | 164 | $scrollContentEl[scrollOffsetAttr](scrollPos); 165 | } 166 | 167 | /** 168 | * Scroll callback 169 | */ 170 | function onScrolled(e) { 171 | flashScrollbar(); 172 | } 173 | 174 | /** 175 | * Resize scrollbar 176 | */ 177 | function resizeScrollbar() { 178 | var contentSize = $contentEl[sizeAttr](); 179 | var scrollOffset = $scrollContentEl[scrollOffsetAttr](); // Either scrollTop() or scrollLeft(). 180 | var scrollbarSize = $scrollbarEl[sizeAttr](); 181 | var scrollbarRatio = scrollbarSize / contentSize; 182 | 183 | // Calculate new height/position of drag handle. 184 | // Offset of 2px allows for a small top/bottom or left/right margin around handle. 185 | var handleOffset = Math.round(scrollbarRatio * scrollOffset) + 2; 186 | var handleSize = Math.floor(scrollbarRatio * (scrollbarSize - 2)) - 2; 187 | 188 | if (scrollbarSize < contentSize) { 189 | if (scrollDirection === 'vert'){ 190 | $dragHandleEl.css({'top': handleOffset, 'height': handleSize}); 191 | } else { 192 | $dragHandleEl.css({'left': handleOffset, 'width': handleSize}); 193 | } 194 | $scrollbarEl.show(); 195 | } else { 196 | $scrollbarEl.hide(); 197 | } 198 | } 199 | 200 | /** 201 | * Flash scrollbar visibility 202 | */ 203 | function flashScrollbar() { 204 | resizeScrollbar(); 205 | showScrollbar(); 206 | } 207 | 208 | /** 209 | * Show scrollbar 210 | */ 211 | function showScrollbar() { 212 | $dragHandleEl.addClass('visible'); 213 | 214 | if (!options.autoHide) { 215 | return; 216 | } 217 | if(typeof flashTimeout === 'number') { 218 | window.clearTimeout(flashTimeout); 219 | } 220 | flashTimeout = window.setTimeout(function() { 221 | hideScrollbar(); 222 | }, 1000); 223 | } 224 | 225 | /** 226 | * Hide Scrollbar 227 | */ 228 | function hideScrollbar() { 229 | $dragHandleEl.removeClass('visible'); 230 | if(typeof flashTimeout === 'number') { 231 | window.clearTimeout(flashTimeout); 232 | } 233 | } 234 | 235 | /** 236 | * Resize content element 237 | */ 238 | function resizeScrollContent() { 239 | if (scrollDirection === 'vert'){ 240 | $scrollContentEl.width($el.width()+scrollbarWidth()); 241 | $scrollContentEl.height($el.height()); 242 | } else { 243 | $scrollContentEl.width($el.width()); 244 | $scrollContentEl.height($el.height()+scrollbarWidth()); 245 | $contentEl.height($el.height()); 246 | } 247 | } 248 | 249 | /** 250 | * Calculate scrollbar width 251 | * 252 | * Original function by Jonathan Sharp: 253 | * https://jdsharp.us/jQuery/minute/calculate-scrollbar-width.php 254 | * Updated to work in Chrome v25. 255 | */ 256 | function scrollbarWidth() { 257 | // Append a temporary scrolling element to the DOM, then measure 258 | // the difference between between its outer and inner elements. 259 | var tempEl = $('
    '); 260 | $('body').append(tempEl); 261 | var width = $(tempEl).innerWidth(); 262 | var widthMinusScrollbars = $('div', tempEl).innerWidth(); 263 | tempEl.remove(); 264 | // On OS X if the scrollbar is set to auto hide it will have zero width. On webkit we can still 265 | // hide it using ::-webkit-scrollbar { width:0; height:0; } but there is no moz equivalent. So we're 266 | // forced to sniff Firefox and return a hard-coded scrollbar width. I know, I know... 267 | if (width === widthMinusScrollbars && navigator.userAgent.toLowerCase().indexOf('firefox') > -1) { 268 | return 17; 269 | } 270 | return (width - widthMinusScrollbars); 271 | } 272 | 273 | /** 274 | * Recalculate scrollbar 275 | */ 276 | function recalculate() { 277 | resizeScrollContent(); 278 | resizeScrollbar(); 279 | } 280 | 281 | /** 282 | * Get/Set plugin option. 283 | */ 284 | function option (key, val) { 285 | if (val) { 286 | options[key] = val; 287 | } else { 288 | return options[key]; 289 | } 290 | } 291 | 292 | /** 293 | * Destroy plugin. 294 | */ 295 | function destroy() { 296 | // Restore the element to its original state. 297 | $contentEl.insertBefore($scrollbarEl); 298 | $scrollbarEl.remove(); 299 | $scrollContentEl.remove(); 300 | $contentEl.css({'height': $el.height()+'px', 'overflow-y': 'scroll'}); 301 | 302 | hook('onDestroy'); 303 | $el.removeData('plugin_' + pluginName); 304 | } 305 | 306 | /** 307 | * Plugin callback hook. 308 | */ 309 | function hook(hookName) { 310 | if (options[hookName] !== undefined) { 311 | options[hookName].call(el); 312 | } 313 | } 314 | 315 | init(); 316 | 317 | return { 318 | option: option, 319 | destroy: destroy, 320 | recalculate: recalculate 321 | }; 322 | } 323 | 324 | $.fn[pluginName] = function(options) { 325 | if (typeof arguments[0] === 'string') { 326 | var methodName = arguments[0]; 327 | var args = Array.prototype.slice.call(arguments, 1); 328 | var returnVal; 329 | this.each(function() { 330 | if ($.data(this, 'plugin_' + pluginName) && typeof $.data(this, 'plugin_' + pluginName)[methodName] === 'function') { 331 | returnVal = $.data(this, 'plugin_' + pluginName)[methodName].apply(this, args); 332 | } else { 333 | throw new Error('Method ' + methodName + ' does not exist on jQuery.' + pluginName); 334 | } 335 | }); 336 | if (returnVal !== undefined){ 337 | return returnVal; 338 | } else { 339 | return this; 340 | } 341 | } else if (typeof options === "object" || !options) { 342 | return this.each(function() { 343 | if (!$.data(this, 'plugin_' + pluginName)) { 344 | $.data(this, 'plugin_' + pluginName, new Plugin(this, options)); 345 | } 346 | }); 347 | } 348 | }; 349 | 350 | $.fn[pluginName].defaults = { 351 | onInit: function() {}, 352 | onDestroy: function() {}, 353 | wrapContent: true, 354 | autoHide: true 355 | }; 356 | 357 | })(jQuery); 358 | -------------------------------------------------------------------------------- /editor/js/main.js: -------------------------------------------------------------------------------- 1 | // Global by intention. 2 | var builder; 3 | 4 | jQuery(document).ready(function($) { 5 | var form = $("#form"); 6 | builder = new Builder(form); 7 | 8 | $.getJSON("json/schema.json", function(data) { 9 | builder.init(data); 10 | reset(); 11 | }); 12 | 13 | var preview = $("#preview"); 14 | var iframe = $("#iframe"); 15 | 16 | (function() { 17 | var timer = null; 18 | form.on("change", function() { 19 | clearTimeout(timer); 20 | preview.addClass("loading"); 21 | timer = setTimeout(function() { 22 | var data = builder.getFormValues(); 23 | form.data("resume", data); 24 | postResume(data); 25 | }, 200); 26 | }); 27 | })(); 28 | 29 | function postResume(data) { 30 | var theme = "flat"; 31 | var hash = window.location.hash; 32 | if (hash != "") { 33 | theme = hash.replace("#", ""); 34 | } 35 | $.ajax({ 36 | type: "POST", 37 | contentType: "application/json", 38 | data: JSON.stringify({resume: data}, null, " "), 39 | url: "https://themes.jsonresume.org/" + theme, 40 | success: function(html) { 41 | iframe.contents().find("body").html(html); 42 | preview.removeClass("loading"); 43 | } 44 | }); 45 | (function toggleActive() { 46 | $("#theme-current").html(theme); 47 | var active = $("#themes-list .item[href='#" + theme + "']").addClass("active"); 48 | active.siblings().removeClass("active"); 49 | })(); 50 | } 51 | 52 | enableTSEplugin(); 53 | enableCSStransitions(); 54 | 55 | $("#export").on("click", function() { 56 | var data = form.data("resume"); 57 | download(JSON.stringify(data, null, " "), "resume.json", "text/plain"); 58 | }); 59 | $("#export").tooltip({ 60 | container: "body" 61 | }); 62 | 63 | 64 | $("#reset").on("click", function() { 65 | if (confirm("Are you sure?")) { 66 | reset(); 67 | } 68 | }); 69 | 70 | $("#clear").on("click", function() { 71 | if (confirm("Are you sure?")) { 72 | clear(); 73 | } 74 | }); 75 | 76 | var tabs = $("#sidebar .tabs a"); 77 | tabs.on("click", function() { 78 | var self = $(this); 79 | self.addClass("active").siblings().removeClass("active"); 80 | }); 81 | 82 | (function getThemes() { 83 | var list = $("#themes-list"); 84 | var item = list.find(".item").remove(); 85 | $.getJSON("https://themes.jsonresume.org/themes.json", function(json) { 86 | var themes = json.themes; 87 | if (!themes) { 88 | return; 89 | } 90 | for (var t in themes) { 91 | var theme = item 92 | .clone() 93 | .attr("href", "#" + t) 94 | .find(".name") 95 | .html(t) 96 | .end() 97 | .find(".version") 98 | .html(themes[t].versions.shift()) 99 | .end() 100 | .appendTo(list); 101 | } 102 | }); 103 | list.on("click", ".item", function() { 104 | form.trigger("change"); 105 | }); 106 | })(); 107 | 108 | var jsonEditor = $("#json-editor"); 109 | 110 | (function() { 111 | var timer = null; 112 | jsonEditor.on("keyup", function() { 113 | clearTimeout(timer); 114 | timer = setTimeout(function() { 115 | try { 116 | var text = jsonEditor.val(); 117 | builder.setFormValues(JSON.parse(text)) 118 | } catch(e) { 119 | // .. 120 | } 121 | }, 200); 122 | }); 123 | })(); 124 | 125 | form.on("change", function() { 126 | var json = builder.getFormValuesAsJSON(); 127 | if (jsonEditor.val() !== json) { 128 | jsonEditor.val(json); 129 | } 130 | }); 131 | 132 | $("#sidebar .view").on("click", "a", function(e) { 133 | e.preventDefault(); 134 | var self = $(this); 135 | var type = self.data("type"); 136 | self.addClass("active").siblings().removeClass("active"); 137 | jsonEditor.toggleClass("show", type == "json"); 138 | }); 139 | }); 140 | 141 | function reset() { 142 | $.getJSON("json/resume.json", function(data) { 143 | builder.setFormValues(data); 144 | }); 145 | } 146 | 147 | function clear() { 148 | builder.setFormValues({}); 149 | } 150 | 151 | function enableTSEplugin() { 152 | var preview = $("#preview"); 153 | var scrollable = $(".tse-scrollable"); 154 | scrollable.TrackpadScrollEmulator(); 155 | scrollable.on("startDrag", function() { 156 | preview.addClass("scroll"); 157 | }); 158 | scrollable.on("endDrag", function() { 159 | preview.removeClass("scroll"); 160 | }); 161 | } 162 | 163 | function enableCSStransitions() { 164 | setTimeout(function() { 165 | $("body").removeClass("preload"); 166 | }, 200); 167 | } 168 | -------------------------------------------------------------------------------- /editor/json-builder/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /editor/json-builder/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | watch: { 4 | files: "templates/*", 5 | tasks: ["build"] 6 | } 7 | }); 8 | grunt.loadNpmTasks("grunt-contrib-watch"); 9 | grunt.registerTask( 10 | "build", 11 | function() { 12 | grunt.util.spawn({ 13 | cmd: "bash", 14 | args: ["build.sh"] 15 | }, function() { 16 | // .. 17 | }); 18 | } 19 | ); 20 | grunt.registerTask( 21 | "default", 22 | ["build"] 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /editor/json-builder/README.md: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | Available under [the MIT license](https://mths.be/mit). 4 | -------------------------------------------------------------------------------- /editor/json-builder/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./node_modules/.bin/handlebars -e tpl -f src/builder.templates.js templates/ 3 | -------------------------------------------------------------------------------- /editor/json-builder/css/json-builder.css: -------------------------------------------------------------------------------- 1 | .json-builder input { 2 | border: none; 3 | border-bottom: 1px solid #ddd; 4 | font-size: 13px; 5 | margin: 0; 6 | outline: none; 7 | padding: 3px 0 6px; 8 | resize: none; 9 | width: 100%; 10 | } 11 | .json-builder button { 12 | font-size: 11px; 13 | margin: 0; 14 | outline: 0; 15 | padding: 4px 8px; 16 | } 17 | .json-builder button:hover { 18 | opacity: .8; 19 | } 20 | .json-builder .title { 21 | font-size: 18px; 22 | font-weight: bold; 23 | margin: 25px 0 15px; 24 | text-transform: capitalize; 25 | } 26 | .json-builder .content { 27 | padding: 10px; 28 | } 29 | .json-builder .array .array .title { 30 | font-size: 14px; 31 | } 32 | .json-builder .item { 33 | background: #fff; 34 | border: 1px solid #ddd; 35 | border-radius: 2px; 36 | margin-bottom: 10px; 37 | overflow: hidden; 38 | padding: 20px 10px 10px; 39 | } 40 | .json-builder .item .handle { 41 | background: #ececec; 42 | background-image: linear-gradient(#f4f4f4, #ececec); 43 | border-bottom: 1px solid #ddd; 44 | cursor: row-resize; 45 | overflow: hidden; 46 | margin: -20px -10px 10px; 47 | min-height: 10px; 48 | } 49 | .json-builder .item > .string input { 50 | border-bottom: none; 51 | padding: 0; 52 | } 53 | .json-builder .item > .string .title { 54 | display: none; 55 | } 56 | .json-builder .placeholder { 57 | background: #f9f9f9; 58 | border: 1px dashed #ddd; 59 | border-radius: 2px; 60 | margin-bottom: 10px; 61 | padding: 20px 10px 10px; 62 | } 63 | .json-builder .string .title { 64 | color: #999; 65 | font-size: 11px; 66 | font-family: monospace; 67 | font-weight: normal; 68 | margin: 0; 69 | } 70 | .json-builder .string + .string, 71 | .json-builder .string + .object { 72 | margin-top: 8px; 73 | } 74 | .json-builder .add { 75 | margin-left: 10px; 76 | } 77 | .json-builder .item .add { 78 | margin-left: 0px; 79 | } 80 | -------------------------------------------------------------------------------- /editor/json-builder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-builder", 3 | "version": "0.0.0", 4 | "description": "", 5 | "author": "Mattias Erming", 6 | "scripts": { 7 | "build": "./build.sh" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/erming/json-builder" 12 | }, 13 | "license": "MIT", 14 | "devDependencies": { 15 | "grunt": "^0.4.5", 16 | "grunt-contrib-watch": "^0.6.1", 17 | "handlebars": "2.0.0-alpha.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /editor/json-builder/src/builder.js: -------------------------------------------------------------------------------- 1 | function Builder(form) { 2 | this.form = form.addClass("json-builder"); 3 | this.json = null; 4 | this.items = []; 5 | } 6 | 7 | Builder.prototype.init = function(json, cb) { 8 | this.json = json; 9 | this.html = this.buildForm(json); 10 | 11 | this.resetForm(); 12 | 13 | var self = this; 14 | var form = this.form; 15 | 16 | form.on("click", ".add", function(e) { 17 | e.preventDefault(); 18 | var add = $(this); 19 | var name = add.closest(".array").data("name"); 20 | var item = self.items[name].clone(); 21 | add.before(item); 22 | form.trigger("change"); 23 | }); 24 | 25 | form.on("click", ".remove", function(e) { 26 | e.preventDefault(); 27 | var self = $(this); 28 | self.closest(".array").children(".item:last").remove(); 29 | form.trigger("change"); 30 | }); 31 | 32 | form.on("input", "input, textarea", function() { 33 | form.trigger("change"); 34 | }); 35 | 36 | if (typeof cb === "function") { 37 | cb(); 38 | } 39 | }; 40 | 41 | Builder.prototype.resetForm = function() { 42 | this.form.html(this.html); 43 | var self = this; 44 | var arrays = this.form 45 | .find(".array") 46 | .get() 47 | .reverse(); 48 | $(arrays).each(function() { 49 | var array = $(this); 50 | var name = array.data("name"); 51 | var item = array.children(".item"); 52 | if (!self.items[name]) { 53 | self.items[name] = item.clone(); 54 | } 55 | item.remove(); 56 | }); 57 | }; 58 | 59 | Builder.prototype.buildForm = function(json, name, html) { 60 | if (!json.type) { 61 | return; 62 | } 63 | 64 | name = name || ""; 65 | html = html || ""; 66 | 67 | var title = name.split(".").pop(); 68 | 69 | switch (json.type) { 70 | case "array": 71 | var items = json.items; 72 | html = Handlebars.templates["array"]({ 73 | name: name, 74 | title: title, 75 | html: this.buildForm(items, name) 76 | }); 77 | break; 78 | 79 | case "object": 80 | if (name) { 81 | name += "."; 82 | } 83 | var props = json.properties; 84 | for (var i in props) { 85 | html += this.buildForm(props[i], name + i); 86 | } 87 | html = Handlebars.templates["object"]({ 88 | name: name, 89 | title: title, 90 | html: html 91 | }); 92 | break; 93 | 94 | case "string": 95 | html = Handlebars.templates["string"]({ 96 | name: name, 97 | title: title 98 | }); 99 | break; 100 | } 101 | 102 | return html; 103 | }; 104 | 105 | Builder.prototype.setFormValues = function(json, scope, name) { 106 | scope = scope || this.form; 107 | name = name || ""; 108 | if (name == "") { 109 | this.resetForm(); 110 | } 111 | 112 | var type = $.type(json); 113 | switch (type) { 114 | case "array": 115 | var array = scope.find(".array[data-name='" + name + "']"); 116 | var add = array.find(".add"); 117 | for (var i in json) { 118 | var item = this.items[name].clone(); 119 | this.setFormValues(json[i], item, name); 120 | add.before(item) 121 | } 122 | break; 123 | 124 | case "object": 125 | if (name) { 126 | name += "."; 127 | } 128 | for (var i in json) { 129 | this.setFormValues(json[i], scope, name + i); 130 | } 131 | break; 132 | 133 | case "string": 134 | var input = scope.find("input[name='" + name + "']"); 135 | input.val(json); 136 | break; 137 | } 138 | 139 | if (name == "") { 140 | var form = this.form; 141 | form.trigger("change"); 142 | if ($.fn.sortable) { 143 | form.find(".array").sortable({ 144 | containment: "parent", 145 | cursor: "row-resize", 146 | items: ".item", 147 | handle: ".handle", 148 | placeholder: "placeholder", 149 | forcePlaceholderSize: true, 150 | scroll: false, 151 | update: function() { 152 | form.trigger("change"); 153 | } 154 | }); 155 | } 156 | } 157 | }; 158 | 159 | Builder.prototype.getFormValuesAsJSON = function() { 160 | var values = this.getFormValues(); 161 | var json = JSON.stringify(values, null, " "); 162 | return json; 163 | }; 164 | 165 | Builder.prototype.getFormValues = function() { 166 | var form = this.form; 167 | var values = {}; 168 | 169 | (function iterate(obj, form, scope, name) { 170 | if (!obj.type) { 171 | return; 172 | } 173 | 174 | scope = scope || values; 175 | name = name || ""; 176 | 177 | var key = name.split(".").pop(); 178 | 179 | switch (obj.type) { 180 | case "array": 181 | scope[key] = []; 182 | var array = form.find(".item[data-name='" + name + "']"); 183 | var i = 0; 184 | array.each(function() { 185 | iterate( 186 | obj.items, 187 | $(this), 188 | scope[key], 189 | name 190 | ); 191 | i++; 192 | }); 193 | break; 194 | 195 | case "object": 196 | var props = obj.properties; 197 | var type = $.type(scope); 198 | if (type == "array") { 199 | scope.push({}); 200 | } else if (key !== "") { 201 | scope[key] = {}; 202 | } 203 | if (name) { 204 | name += "."; 205 | } 206 | for (var i in props) { 207 | iterate( 208 | props[i], 209 | form, 210 | type == "array" ? scope[scope.length - 1] : scope[key], 211 | name + i 212 | ); 213 | } 214 | break; 215 | 216 | case "string": 217 | var inputs = form.find("input[name='" + name + "']"); 218 | if ($.type(scope) != "array") { 219 | scope[key] = inputs.eq(0).val(); 220 | } else { 221 | inputs.each(function() { 222 | scope.push($(this).val()); 223 | }); 224 | } 225 | break; 226 | } 227 | })(this.json, form); 228 | 229 | return values; 230 | }; 231 | -------------------------------------------------------------------------------- /editor/json-builder/src/builder.templates.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; 3 | templates['array'] = template({"compiler":[5,">= 2.0.0"],"main":function(depth0,helpers,partials,data) { 4 | var stack1, helper, functionType="function", escapeExpression=this.escapeExpression, buffer = "
    \n
    \n " 7 | + escapeExpression(((helper = helpers.title || (depth0 && depth0.title)),(typeof helper === functionType ? helper.call(depth0, {"name":"title","hash":{},"data":data}) : helper))) 8 | + "\n
    \n
    \n
    \n "; 11 | stack1 = ((helper = helpers.html || (depth0 && depth0.html)),(typeof helper === functionType ? helper.call(depth0, {"name":"html","hash":{},"data":data}) : helper)); 12 | if(stack1 || stack1 === 0) { buffer += stack1; } 13 | return buffer + "\n
    \n \n \n
    \n"; 14 | },"useData":true}); 15 | templates['object'] = template({"compiler":[5,">= 2.0.0"],"main":function(depth0,helpers,partials,data) { 16 | var stack1, helper, functionType="function", buffer = "
    \n "; 17 | stack1 = ((helper = helpers.html || (depth0 && depth0.html)),(typeof helper === functionType ? helper.call(depth0, {"name":"html","hash":{},"data":data}) : helper)); 18 | if(stack1 || stack1 === 0) { buffer += stack1; } 19 | return buffer + "\n
    \n"; 20 | },"useData":true}); 21 | templates['string'] = template({"compiler":[5,">= 2.0.0"],"main":function(depth0,helpers,partials,data) { 22 | var helper, functionType="function", escapeExpression=this.escapeExpression; 23 | return "
    \n \n
    \n"; 28 | },"useData":true}); 29 | })(); -------------------------------------------------------------------------------- /editor/json-builder/templates/array.tpl: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | {{title}} 4 |
    5 |
    6 |
    7 | {{{html}}} 8 |
    9 | 12 | 15 |
    16 | -------------------------------------------------------------------------------- /editor/json-builder/templates/object.tpl: -------------------------------------------------------------------------------- 1 |
    2 | {{{html}}} 3 |
    4 | -------------------------------------------------------------------------------- /editor/json-builder/templates/string.tpl: -------------------------------------------------------------------------------- 1 |
    2 | 6 |
    7 | -------------------------------------------------------------------------------- /editor/json/resume.json: -------------------------------------------------------------------------------- 1 | { 2 | "basics": { 3 | "name": "Richard Hendriks", 4 | "label": "Programmer", 5 | "picture": "", 6 | "email": "richard.hendriks@gmail.com", 7 | "phone": "(912) 555-4321", 8 | "website": "https://richardhendricks.com", 9 | "summary": "Richard hails from Tulsa. He has earned degrees from the University of Oklahoma and Stanford. (Go Sooners and Cardinal!) Before starting Pied Piper, he worked for Hooli as a part time software developer. While his work focuses on applied information theory, mostly optimizing lossless compression schema of both the length-limited and adaptive variants, his non-work interests range widely, everything from quantum computing to chaos theory. He could tell you about it, but THAT would NOT be a “length-limited” conversation!", 10 | "location": { 11 | "address": "2712 Broadway St", 12 | "postalCode": "CA 94115", 13 | "city": "San Francisco", 14 | "countryCode": "US", 15 | "region": "California" 16 | }, 17 | "profiles": [ 18 | { 19 | "network": "Twitter", 20 | "username": "neutralthoughts", 21 | "url": "" 22 | }, 23 | { 24 | "network": "SoundCloud", 25 | "username": "dandymusicnl", 26 | "url": "https://soundcloud.com/dandymusicnl" 27 | } 28 | ] 29 | }, 30 | "work": [ 31 | { 32 | "company": "Pied Piper", 33 | "position": "CEO/President", 34 | "website": "https://piedpiper.com", 35 | "startDate": "2013-12-01", 36 | "endDate": "2014-12-01", 37 | "summary": "Pied Piper is a multi-platform technology based on a proprietary universal compression algorithm that has consistently fielded high Weisman Scores™ that are not merely competitive, but approach the theoretical limit of lossless compression.", 38 | "highlights": [ 39 | "Build an algorithm for artist to detect if their music was violating copy right infringement laws", 40 | "Successfully won Techcrunch Disrupt", 41 | "Optimized an algorithm that holds the current world record for Weisman Scores" 42 | ] 43 | } 44 | ], 45 | "volunteer": [ 46 | { 47 | "organization": "CoderDojo", 48 | "position": "Teacher", 49 | "website": "https://coderdojo.com/", 50 | "startDate": "2012-01-01", 51 | "endDate": "2013-01-01", 52 | "summary": "Global movement of free coding clubs for young people.", 53 | "highlights": [ 54 | "Awarded 'Teacher of the Month'" 55 | ] 56 | } 57 | ], 58 | "education": [ 59 | { 60 | "institution": "University of Oklahoma", 61 | "area": "Information Technology", 62 | "studyType": "Bachelor", 63 | "startDate": "2011-06-01", 64 | "endDate": "2014-01-01", 65 | "gpa": "4.0", 66 | "courses": [ 67 | "DB1101 - Basic SQL", 68 | "CS2011 - Java Introduction" 69 | ] 70 | } 71 | ], 72 | "awards": [ 73 | { 74 | "title": "Digital Compression Pioneer Award", 75 | "date": "2014-11-01", 76 | "awarder": "Techcrunch", 77 | "summary": "There is no spoon." 78 | } 79 | ], 80 | "publications": [ 81 | { 82 | "name": "Video compression for 3d media", 83 | "publisher": "Hooli", 84 | "releaseDate": "2014-10-01", 85 | "website": "https://en.wikipedia.org/wiki/Silicon_Valley_(TV_series)", 86 | "summary": "Innovative middle-out compression algorithm that changes the way we store data." 87 | } 88 | ], 89 | "skills": [ 90 | { 91 | "name": "Web Development", 92 | "level": "Master", 93 | "keywords": [ 94 | "HTML", 95 | "CSS", 96 | "Javascript" 97 | ] 98 | }, 99 | { 100 | "name": "Compression", 101 | "level": "Master", 102 | "keywords": [ 103 | "Mpeg", 104 | "MP4", 105 | "GIF" 106 | ] 107 | } 108 | ], 109 | "languages": [ 110 | { 111 | "language": "English", 112 | "fluency": "Native speaker" 113 | } 114 | ], 115 | "interests": [ 116 | { 117 | "name": "Wildlife", 118 | "keywords": [ 119 | "Ferrets", 120 | "Unicorns" 121 | ] 122 | } 123 | ], 124 | "references": [ 125 | { 126 | "name": "Erlich Bachman", 127 | "reference": "It is my pleasure to recommend Richard, his performance working as a consultant for Main St. Company proved that he will be a valuable addition to any company." 128 | } 129 | ] 130 | } 131 | -------------------------------------------------------------------------------- /editor/json/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft-04/schema#", 3 | "title": "Resume Schema", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "properties": { 7 | "basics": { 8 | "type": "object", 9 | "additionalProperties": false, 10 | "properties": { 11 | "name": { 12 | "type": "string" 13 | }, 14 | "label": { 15 | "type": "string", 16 | "description": "e.g. Web Developer" 17 | }, 18 | "picture": { 19 | "type": "string", 20 | "description": "URL (as per RFC 3986) to a picture in JPEG or PNG format" 21 | }, 22 | "email": { 23 | "type": "string", 24 | "description": "e.g. thomas@gmail.com", 25 | "format": "email" 26 | }, 27 | "phone": { 28 | "type": "string", 29 | "description": "Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923" 30 | }, 31 | "website": { 32 | "type": "string", 33 | "description": "URL (as per RFC 3986) to your website, e.g. personal homepage", 34 | "format": "uri" 35 | }, 36 | "summary": { 37 | "type": "string", 38 | "description": "Write a short 2-3 sentence biography about yourself" 39 | }, 40 | "location": { 41 | "type": "object", 42 | "additionalProperties": false, 43 | "properties": { 44 | "address": { 45 | "type": "string", 46 | "description": "To add multiple address lines, use \n. For example, 1234 Glücklichkeit Straße\nHinterhaus 5. Etage li." 47 | }, 48 | "postalCode": { 49 | "type": "string" 50 | }, 51 | "city": { 52 | "type": "string" 53 | }, 54 | "countryCode": { 55 | "type": "string", 56 | "description": "code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN" 57 | }, 58 | "region": { 59 | "type": "string", 60 | "description": "The general region where you live. Can be a US state, or a province, for instance." 61 | } 62 | } 63 | }, 64 | "profiles": { 65 | "type": "array", 66 | "description": "Specify any number of social networks that you participate in", 67 | "additionalItems": false, 68 | "items": { 69 | "type": "object", 70 | "additionalProperties": false, 71 | "properties": { 72 | "network": { 73 | "type": "string", 74 | "description": "e.g. Facebook or Twitter" 75 | }, 76 | "username": { 77 | "type": "string", 78 | "description": "e.g. neutralthoughts" 79 | }, 80 | "url": { 81 | "type": "string", 82 | "description": "e.g. https://twitter.com/neutralthoughts" 83 | } 84 | } 85 | } 86 | } 87 | } 88 | }, 89 | "work": { 90 | "type": "array", 91 | "additionalItems": false, 92 | "items": { 93 | "type": "object", 94 | "additionalProperties": false, 95 | "properties": { 96 | "company": { 97 | "type": "string", 98 | "description": "e.g. Facebook" 99 | }, 100 | "position": { 101 | "type": "string", 102 | "description": "e.g. Software Engineer" 103 | }, 104 | "website": { 105 | "type": "string", 106 | "description": "e.g. https://facebook.com", 107 | "format": "uri" 108 | }, 109 | "startDate": { 110 | "type": "string", 111 | "description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29", 112 | "format": "date" 113 | }, 114 | "endDate": { 115 | "type": "string", 116 | "description": "e.g. 2012-06-29", 117 | "format": "date" 118 | }, 119 | "summary": { 120 | "type": "string", 121 | "description": "Give an overview of your responsibilities at the company" 122 | }, 123 | "highlights": { 124 | "type": "array", 125 | "description": "Specify multiple accomplishments", 126 | "additionalItems": false, 127 | "items": { 128 | "type": "string", 129 | "description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising" 130 | } 131 | } 132 | } 133 | 134 | } 135 | }, 136 | "volunteer": { 137 | "type": "array", 138 | "additionalItems": false, 139 | "items": { 140 | "type": "object", 141 | "additionalProperties": false, 142 | "properties": { 143 | "organization": { 144 | "type": "string", 145 | "description": "e.g. Facebook" 146 | }, 147 | "position": { 148 | "type": "string", 149 | "description": "e.g. Software Engineer" 150 | }, 151 | "website": { 152 | "type": "string", 153 | "description": "e.g. https://facebook.com", 154 | "format": "uri" 155 | }, 156 | "startDate": { 157 | "type": "string", 158 | "description": "resume.json uses the ISO 8601 date standard e.g. 2014-06-29", 159 | "format": "date" 160 | }, 161 | "endDate": { 162 | "type": "string", 163 | "description": "e.g. 2012-06-29", 164 | "format": "date" 165 | }, 166 | "summary": { 167 | "type": "string", 168 | "description": "Give an overview of your responsibilities at the company" 169 | }, 170 | "highlights": { 171 | "type": "array", 172 | "description": "Specify multiple accomplishments", 173 | "additionalItems": false, 174 | "items": { 175 | "type": "string", 176 | "description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising" 177 | } 178 | } 179 | } 180 | 181 | } 182 | }, 183 | "education": { 184 | "type": "array", 185 | "additionalItems": false, 186 | "items": { 187 | "type": "object", 188 | "additionalProperties": false, 189 | "properties": { 190 | "institution": { 191 | "type": "string", 192 | "description": "e.g. Massachusetts Institute of Technology" 193 | }, 194 | "area": { 195 | "type": "string", 196 | "description": "e.g. Arts" 197 | }, 198 | "studyType": { 199 | "type": "string", 200 | "description": "e.g. Bachelor" 201 | }, 202 | "startDate": { 203 | "type": "string", 204 | "description": "e.g. 2014-06-29", 205 | "format": "date" 206 | }, 207 | "endDate": { 208 | "type": "string", 209 | "description": "e.g. 2012-06-29", 210 | "format": "date" 211 | }, 212 | "gpa": { 213 | "type": "string", 214 | "description": "grade point average, e.g. 3.67/4.0" 215 | }, 216 | "courses": { 217 | "type": "array", 218 | "description": "List notable courses/subjects", 219 | "additionalItems": false, 220 | "items": { 221 | "type": "string", 222 | "description": "e.g. H1302 - Introduction to American history" 223 | } 224 | } 225 | } 226 | 227 | 228 | } 229 | }, 230 | "awards": { 231 | "type": "array", 232 | "description": "Specify any awards you have received throughout your professional career", 233 | "additionalItems": false, 234 | "items": { 235 | "type": "object", 236 | "additionalProperties": false, 237 | "properties": { 238 | "title": { 239 | "type": "string", 240 | "description": "e.g. One of the 100 greatest minds of the century" 241 | }, 242 | "date": { 243 | "type": "string", 244 | "description": "e.g. 1989-06-12", 245 | "format": "date" 246 | }, 247 | "awarder": { 248 | "type": "string", 249 | "description": "e.g. Time Magazine" 250 | }, 251 | "summary": { 252 | "type": "string", 253 | "description": "e.g. Received for my work with Quantum Physics" 254 | } 255 | } 256 | } 257 | }, 258 | "publications": { 259 | "type": "array", 260 | "description": "Specify your publications through your career", 261 | "additionalItems": false, 262 | "items": { 263 | "type": "object", 264 | "additionalProperties": false, 265 | "properties": { 266 | "name": { 267 | "type": "string", 268 | "description": "e.g. The World Wide Web" 269 | }, 270 | "publisher": { 271 | "type": "string", 272 | "description": "e.g. IEEE, Computer Magazine" 273 | }, 274 | "releaseDate": { 275 | "type": "string", 276 | "description": "e.g. 1990-08-01" 277 | }, 278 | "website": { 279 | "type": "string", 280 | "description": "e.g. https://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html" 281 | }, 282 | "summary": { 283 | "type": "string", 284 | "description": "Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML." 285 | } 286 | } 287 | } 288 | }, 289 | "skills": { 290 | "type": "array", 291 | "description": "List out your professional skill-set", 292 | "additionalItems": false, 293 | "items": { 294 | "type": "object", 295 | "additionalProperties": false, 296 | "properties": { 297 | "name": { 298 | "type": "string", 299 | "description": "e.g. Web Development" 300 | }, 301 | "level": { 302 | "type": "string", 303 | "description": "e.g. Master" 304 | }, 305 | "keywords": { 306 | "type": "array", 307 | "description": "List some keywords pertaining to this skill", 308 | "additionalItems": false, 309 | "items": { 310 | "type": "string", 311 | "description": "e.g. HTML" 312 | } 313 | } 314 | } 315 | } 316 | }, 317 | "languages": { 318 | "type": "array", 319 | "description": "List any other languages you speak", 320 | "additionalItems": false, 321 | "items": { 322 | "type": "object", 323 | "additionalProperties": false, 324 | "properties": { 325 | "language": { 326 | "type": "string", 327 | "description": "e.g. English, Spanish" 328 | }, 329 | "fluency": { 330 | "type": "string", 331 | "description": "e.g. Fluent, Beginner" 332 | } 333 | } 334 | } 335 | }, 336 | "interests": { 337 | "type": "array", 338 | "additionalItems": false, 339 | "items": { 340 | "type": "object", 341 | "additionalProperties": false, 342 | "properties": { 343 | "name": { 344 | "type": "string", 345 | "description": "e.g. Philosophy" 346 | }, 347 | "keywords": { 348 | "type": "array", 349 | "additionalItems": false, 350 | "items": { 351 | "type": "string", 352 | "description": "e.g. Friedrich Nietzsche" 353 | } 354 | } 355 | } 356 | 357 | } 358 | }, 359 | "references": { 360 | "type": "array", 361 | "description": "List references you have received", 362 | "additionalItems": false, 363 | "items": { 364 | "type": "object", 365 | "additionalProperties": false, 366 | "properties": { 367 | "name": { 368 | "type": "string", 369 | "description": "e.g. Timothy Cook" 370 | }, 371 | "reference": { 372 | "type": "string", 373 | "description": "e.g. Joe blogs was a great employee, who turned up to work at least once a week. He exceeded my expectations when it came to doing nothing." 374 | } 375 | } 376 | 377 | } 378 | } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resume-editor", 3 | "version": "0.0.0", 4 | "description": "The live editor for https://jsonresume.org/", 5 | "author": "Mattias Erming", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/erming/resume-editor" 9 | }, 10 | "license": "MIT", 11 | "devDependencies": { 12 | "grunt": "^0.4.5", 13 | "grunt-contrib-uglify": "^0.5.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/mongoose-connection/index.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | mongoose.connection.on('error', function(err) { 4 | console.log('Mongoose connection error', { err: err }); 5 | throw new Error('database connection error'); 6 | }); 7 | 8 | mongoose.connection.on('open', function() { 9 | console.log('Mongoose connection open'); 10 | }); 11 | 12 | var mongoUrl = process.env.MONGOHQ_URL || 'mongodb://localhost:27017/jsonresume'; 13 | 14 | console.log('Using mongoUrl: ', mongoUrl); 15 | 16 | mongoose.connect(mongoUrl); 17 | 18 | module.exports = mongoose; 19 | -------------------------------------------------------------------------------- /lib/redis-connection/index.js: -------------------------------------------------------------------------------- 1 | if (process.env.REDISTOGO_URL) { 2 | var rtg = require("url").parse(process.env.REDISTOGO_URL); 3 | var redis = require("redis").createClient(rtg.port, rtg.hostname); 4 | redis.auth(rtg.auth.split(":")[1]); 5 | } else { 6 | var redis = require("redis").createClient(); 7 | } 8 | 9 | redis.on("error", function(err) { 10 | console.log("error event - " + redis.host + ":" + redis.port + " - " + err); 11 | res.send({ 12 | sessionError: err 13 | }); 14 | }); 15 | 16 | module.exports = redis; 17 | -------------------------------------------------------------------------------- /middleware/allow-cross-domain.js: -------------------------------------------------------------------------------- 1 | module.exports = function allowCrossDomain(req, res, next) { 2 | // Added other domains you want the server to give access to 3 | // WARNING - Be careful with what origins you give access to 4 | var allowedHost = [ 5 | 'https://backbonetutorials.com', 6 | 'https://localhost' 7 | ]; 8 | 9 | res.header('Access-Control-Allow-Credentials', true); 10 | res.header('Access-Control-Allow-Origin', req.headers.origin) 11 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); 12 | res.header('Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'); 13 | next(); 14 | 15 | }; 16 | -------------------------------------------------------------------------------- /models/resume.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | var resumeSchema = new Schema({}, { strict: false }); 5 | 6 | module.exports = mongoose.model('Resume', resumeSchema); 7 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | var userSchema = new Schema({ 5 | username: { 6 | type: String, 7 | required: true 8 | }, 9 | email: { 10 | type: String 11 | }, 12 | password: { 13 | type: String 14 | }, 15 | hash: { // TODO make virtual 16 | type: String 17 | } 18 | }); 19 | 20 | 21 | module.exports = mongoose.model('User', userSchema); 22 | -------------------------------------------------------------------------------- /newrelic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * New Relic agent configuration. 3 | * 4 | * See lib/config.defaults.js in the agent distribution for a more complete 5 | * description of configuration variables and their potential values. 6 | */ 7 | exports.config = { 8 | /** 9 | * Array of application names. 10 | */ 11 | app_name: ['JSON Resume'], 12 | /** 13 | * Your New Relic license key. 14 | */ 15 | license_key: 'c339a25ad5250214af1a2c58714b006a7a1567ce', 16 | logging: { 17 | /** 18 | * Level at which to log. 'trace' is most useful to New Relic when diagnosing 19 | * issues with the agent, 'info' and higher will impose the least overhead on 20 | * production applications. 21 | */ 22 | level: 'info' 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "registry-server", 3 | "version": "0.0.0", 4 | "engines": { 5 | "node": "^8" 6 | }, 7 | "description": "", 8 | "main": "server.js", 9 | "scripts": { 10 | "test": "NODE_ENV=test node node_modules/mocha/bin/mocha test/**/*.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/jsonresume/registry-server" 15 | }, 16 | "author": "", 17 | "license": "MIT", 18 | "dependencies": { 19 | "bcrypt-nodejs": "0.0.3", 20 | "body-parser": "~1.17.1", 21 | "compression": "^1.6.0", 22 | "connect-redis": "^3.0.0", 23 | "cookie-parser": "^1.4.0", 24 | "crypto": "0.0.3", 25 | "dotenv": "^4.0.0", 26 | "express": "~4.15", 27 | "express-minify": "^0.2.0", 28 | "express-session": "^1.11.3", 29 | "gravatar": "^1.3.1", 30 | "http-status-codes": "^1.0.5", 31 | "lodash": "^4.17.10", 32 | "mongodb": "~2.2.26", 33 | "mongoose": "^4.13", 34 | "mustache": "^2.3.0", 35 | "newrelic": "^1.26.2", 36 | "pdfcrowd": "^1.1.1", 37 | "postmark": "~1.3.1", 38 | "pusher": "^1.0.6", 39 | "redis": "^2.1", 40 | "require-directory": "^2.1.1", 41 | "resume-to-html": "0.0.21", 42 | "resume-to-markdown": "0.0.14", 43 | "resume-to-text": "0.0.15", 44 | "sha256": "^0.2.0", 45 | "superagent": "^3.8" 46 | }, 47 | "devDependencies": { 48 | "chai": "^3.3.0", 49 | "chai-properties": "^1.2.1", 50 | "mocha": "^3.3.0", 51 | "nock": "^9.0.13", 52 | "q": "^1.4.1", 53 | "should": "^11.2.1", 54 | "supertest": "^3.0", 55 | "supertest-as-promised": "^4.0.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /provision/.profile_additions: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | source /vagrant/provision/constants.sh 4 | 5 | echo "Running additions to .profile" 6 | 7 | # mongo environment variable 8 | export MONGOHQ_URL=mongodb://$MONGO_URL 9 | 10 | # ensure we `vagrant ssh` into the project directory 11 | cd $PROJECT_DIR 12 | 13 | # help new devs get started 14 | tput smso 15 | echo -e "\nRun 'node server.js' to start the server" 16 | echo "Run 'npm test' to run the tests" 17 | tput rmso 18 | -------------------------------------------------------------------------------- /provision/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source /vagrant/provision/constants.sh 4 | source /vagrant/provision/utils.sh 5 | 6 | echo_heading "Update and install packages" 7 | sudo apt-get update -qq 8 | sudo apt-get install -y curl git mongodb redis-server 9 | 10 | echo_heading "Installing latest stable NodeJS using NVM" 11 | curl --compressed -s0- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash 12 | source "$VAGRANT_HOME/.nvm/nvm.sh" 13 | nvm install 4 14 | 15 | echo_heading "Setting up NodeJS and project" 16 | cd "$PROJECT_DIR" 17 | npm install 18 | 19 | git submodule update --init --recursive --depth 1 20 | 21 | # mongo config 22 | mongo "$MONGO_URL" --eval "db.resumes.insert({})" 23 | 24 | echo_heading "Idempotently add stuff to .profile" 25 | cd "$VAGRANT_HOME" 26 | # If not already there, then append command to execute .profile_additions to .profile 27 | if ! grep -q ".profile_additions" "$VAGRANT_HOME/.profile"; then 28 | echo "source $PROVISION_DIR/.profile_additions" >> "$VAGRANT_HOME/.profile" 29 | fi 30 | 31 | echo -e "\nFinished provisioning" 32 | -------------------------------------------------------------------------------- /provision/constants.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PROJECT_DIR=/vagrant 4 | export PROVISION_DIR=$PROJECT_DIR/provision 5 | export VAGRANT_HOME=/home/vagrant 6 | 7 | export MONGO_URL=localhost:27017/jsonresume 8 | -------------------------------------------------------------------------------- /provision/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function echo_heading { 4 | local string=$1 5 | local string_length=${#string} 6 | 7 | local underline="" 8 | for ((i=1;i<=string_length;i++)) ; do 9 | underline=${underline}- 10 | done 11 | 12 | echo -e "\n$string" 13 | echo $underline 14 | } 15 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // require('dotenv').load(); 2 | require('./lib/mongoose-connection'); 3 | var redis = require('./lib/redis-connection'); 4 | var express = require("express"); 5 | var path = require('path'); 6 | var bodyParser = require('body-parser'); 7 | var pdf = require('pdfcrowd'); 8 | var client = new pdf.Pdfcrowd('thomasdavis', '7d2352eade77858f102032829a2ac64e'); 9 | var app = express(); 10 | var request = require('superagent'); 11 | var expressSession = require('express-session'); 12 | var cookieParser = require('cookie-parser'); 13 | var compress = require('compression'); 14 | var minify = require('express-minify'); 15 | var controller = require('./controller'); 16 | 17 | var points = []; 18 | var DEFAULT_THEME = 'modern'; 19 | 20 | var RedisStore = require('connect-redis')(expressSession); 21 | 22 | app.use(compress()); 23 | app.use(minify({ 24 | cache: __dirname + '/cache' 25 | })); 26 | app.use(require('./middleware/allow-cross-domain')); 27 | app.use(cookieParser()); 28 | app.use(expressSession({ 29 | store: new RedisStore({ 30 | client: redis 31 | }), 32 | secret: 'keyboard cat' 33 | })); 34 | //app.use(expressSession({secret:'somesecrettokenhere'})); 35 | 36 | app.use(express.static(__dirname + '/editor', { 37 | maxAge: 21600 * 1000 38 | })); 39 | 40 | app.use(bodyParser()); 41 | var fs = require('fs'); 42 | var guid = (function() { 43 | function s4() { 44 | return Math.floor((1 + Math.random()) * 0x10000) 45 | .toString(16) 46 | .substring(1); 47 | } 48 | return function() { 49 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 50 | s4() + '-' + s4() + s4() + s4(); 51 | }; 52 | })(); 53 | 54 | function S4() { 55 | return Math.floor((1 + Math.random()) * 0x10000) 56 | .toString(16) 57 | .substring(1); 58 | }; 59 | 60 | app.all('/*', function(req, res, next) { 61 | //res.header("Access-Control-Allow-Origin", "*"); 62 | //res.header("Access-Control-Allow-Headers", "X-Requested-With"); 63 | 64 | // Make the db accessible to the router 65 | // probably not the most performant way to pass the db's around 66 | // TODO find a better way 67 | req.redis = redis; 68 | next(); 69 | }); 70 | 71 | app.get('/session', controller.checkSession); 72 | app.delete('/session/:id', controller.deleteSession); 73 | app.get('/members', controller.renderMembersPage); 74 | app.get('/stats', controller.showStats); 75 | // export pdf route 76 | // this code is used by resume-cli for pdf export, see line ~188 for web-based export 77 | app.get('/pdf', function(req, res) { 78 | console.log(req.body.resume, req.body.theme); 79 | request 80 | .post('https://themes.jsonresume.org/theme/' + req.body.theme) 81 | .send({ 82 | resume: req.body.resume 83 | }) 84 | .set('Content-Type', 'application/json') 85 | .end(function(err, response) { 86 | client.convertHtml(response.text, pdf.sendHttpResponse(res), { 87 | use_print_media: "true" 88 | }); 89 | }); 90 | }); 91 | 92 | app.get('/:uid.:format', controller.renderResume); 93 | app.get('/:uid', controller.renderResume); 94 | app.post('/resume', controller.upsertResume); 95 | app.put('/resume', controller.updateTheme); 96 | app.post('/user', controller.createUser); 97 | app.post('/session', controller.createSession); 98 | app.put('/account', controller.changePassword); 99 | app.delete('/account', controller.deleteUser); 100 | app.post('/:uid', controller.renderResume); 101 | 102 | process.addListener('uncaughtException', function(err) { 103 | console.error('Uncaught error in server.js', { 104 | err: err 105 | // hide stack in production 106 | //, stack: err.stack 107 | }); 108 | // TODO some sort of notification 109 | // process.exit(1); 110 | }); 111 | 112 | var port = Number(process.env.PORT || 5000); 113 | 114 | app.listen(port, function() { 115 | console.log("Listening on " + port); 116 | }); 117 | 118 | module.exports = app; 119 | module.exports.DEFAULT_THEME = DEFAULT_THEME; 120 | -------------------------------------------------------------------------------- /template-helper.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var _ = require('lodash'); 4 | var TEMPLATE_PATH = "./templates/"; 5 | var templatesKeeper = {}; 6 | 7 | /** 8 | * read all templates and cache them 9 | */ 10 | var templates = _.map( fs.readdirSync(TEMPLATE_PATH), function(filename, i) { 11 | if(filename.split('.template').length === 1) { 12 | return; 13 | } 14 | var page = filename.split('.template')[0]; 15 | if(!templatesKeeper[page]) { 16 | templatesKeeper[page] = fs.readFileSync(path.resolve(__dirname, TEMPLATE_PATH + page + '.template'), 'utf8'); 17 | } 18 | return filename.split('.template')[0]; 19 | }); 20 | 21 | exports.get = function(page){ 22 | if(templates.indexOf(page) > -1) { 23 | return templatesKeeper[page]; 24 | } else { 25 | return ''; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /templates/email/welcome.html: -------------------------------------------------------------------------------- 1 | Hey {{username}}, 2 | 3 | Thanks for joining the early stages of the JSON resume movement. We are trying to move fast with polishing off the ecosystem and encourage everyone with a keyboard to hack on top of the project. 4 | 5 | Your newly registered account can be viewed at https://registry.jsonresume.org/{{username}} 6 | 7 | If you haven't published a resume yet, some instructions will be at the link above. 8 | 9 | Let us know if you have any feedback or suggestions, 10 | Thomas and Roland, 11 | https://jsonresume.org -------------------------------------------------------------------------------- /templates/home.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Json Resume 8 | 9 | 10 | 11 | 12 | 54 | 55 | 56 | 57 | 58 | 62 | 63 | 64 | 65 |
    66 | 67 | 68 | 69 | 70 | 71 | 72 | 396 | 397 | 398 | 399 | -------------------------------------------------------------------------------- /templates/layout.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{resume.name}} - Json Resume 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 |
    22 |
    23 |
    24 | {{{output}}} 25 |
    26 |
    27 |
    28 | 29 |

    Social Networks

    30 | Follow @{{resume.profiles.twitter}} 31 |

    32 | Github @{{resume.profiles.github}} 33 |

    34 | Youtube @{{resume.profiles.github}} 35 |

    Export

    36 |
    37 | Export 38 | 39 | 46 |
    47 |
    48 |
    49 |
    50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /templates/members.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Json Resume 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 21 | 22 | 23 | 24 | 25 | 40 |
    41 |
    42 |

    Coming soon!

    43 |
    44 | 45 |
    46 | 47 | 48 | {{#usernames}} 49 | 50 | {{username}} 51 |
    52 | {{/usernames}} 53 |
    54 | 55 | 56 | 57 |
    58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /templates/noresume.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Json Resume 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 |
    23 | 24 |
    25 | 26 |
    27 |

    28 | You've made it this far, great!
    29 | But you still need to publish your resume

    30 | Create a new resume.json by typing `resume init`

    31 | Fill out a few of the fields until you are happy, then simply type `resume publish` 32 |

    33 |
    34 | 35 | 36 | 37 |
    38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /templates/password.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Json Resume 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 |
    23 | 24 |
    25 | 26 |
    27 |

    This resume is password protected

    28 |
    29 |
    30 | 31 | 32 | 33 | 34 |
    35 |
    36 | 37 |
    38 | 39 | 40 | 41 |
    42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /test/account-test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonresume/registry-server/41a8d93e937cd4a38a7b64e24787857accd559a4/test/account-test.js -------------------------------------------------------------------------------- /test/fixtures.js: -------------------------------------------------------------------------------- 1 | var user = { 2 | default: { 3 | username: 'someTestUsername', 4 | email: 'someTestEmail', 5 | password: 'someTestPassword' 6 | }, 7 | test1: { 8 | username: 'test1Username', 9 | email: 'test1@email.com', 10 | password: 'test1Password' 11 | }, 12 | test2: { 13 | username: 'test2Username', 14 | email: 'test2@email.com', 15 | password: 'test2Password' 16 | }, 17 | test3: { 18 | username: 'test3Username', 19 | email: 'test3@email.com', 20 | password: 'test3Password' 21 | }, 22 | sessionUser: { 23 | username: 'sessionUser', 24 | email: 'sessionUser@email.com', 25 | password: 'sessionUserPassword' 26 | } 27 | }; 28 | 29 | module.exports = { 30 | user: user 31 | }; 32 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | global.expect = chai.expect; 3 | 4 | chai.use(require('chai-properties')); 5 | 6 | 7 | var mongoose = require('mongoose'); 8 | process.env.MONGOHQ_URL = 'mongodb://localhost:27017/jsonresume-tests'; 9 | // register model schemas 10 | require('../models/user'); 11 | require('../models/resume'); 12 | 13 | 14 | require('../lib/mongoose-connection'); 15 | 16 | function dropMongoDatabase(callback) { 17 | // Drop the database once connected (or immediately if connected). 18 | var CONNECTION_STATES = mongoose.Connection.STATES; 19 | var readyState = mongoose.connection.readyState; 20 | var connected = false; 21 | 22 | var drop = function() { 23 | mongoose.connection.db.dropDatabase(function(err) { 24 | if (err) { 25 | throw err; 26 | } 27 | callback(err); 28 | }); 29 | }; 30 | 31 | if (CONNECTION_STATES[readyState] === 'connected') { 32 | drop(); 33 | } else { 34 | mongoose.connection.once('connected', drop); 35 | } 36 | } 37 | 38 | // This bit of code runs before ANY tests start. 39 | before(function beforeAllTests(done) { 40 | 41 | dropMongoDatabase(done); 42 | }); 43 | -------------------------------------------------------------------------------- /test/integration/changePassword.js: -------------------------------------------------------------------------------- 1 | process.env.MONGOHQ_URL = 'mongodb://localhost:27017/jsonresume-tests'; 2 | process.env.POSTMARK_API_KEY = 'POSTMARK_API_TEST'; // https://blog.postmarkapp.com/post/913165552/handling-email-in-your-test-environment 3 | var server = require('../../server'); 4 | var request = require('supertest')(server); 5 | var should = require('should'); 6 | var bcrypt = require('bcrypt-nodejs'); 7 | var User = require('../../models/user'); 8 | 9 | describe('changePassword: PUT /account ', function() { 10 | 11 | var url = '/account'; 12 | 13 | var user = { 14 | username: 'changePasswordUsername', 15 | email: 'changePasswordEmail', 16 | // FIXME should throw an error if we try to dave a raw Password 17 | // TODO use a mongoose virtual 18 | password: 'changePasswordPassword', 19 | }; 20 | user.hash = bcrypt.hashSync(user.password); 21 | 22 | before(function(done) { 23 | User.remove({}, function(err) { // TODO: move this into a beforeAll function 24 | User.create(user, done); 25 | }); 26 | }); 27 | 28 | it('should return a 401 Unauthorized Error when attempting to change password with incorrect credentials', function(done) { 29 | 30 | request.put(url) 31 | .send({ 32 | email: 'someRandomEmail', 33 | currentPassword: user.password, 34 | newPassword: 'newPassword' 35 | }) 36 | .expect(401, function(err, res) { 37 | should.not.exist(err); 38 | res.body.should.have.properties({ 39 | message: 'email not found' 40 | }) 41 | 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should return a 401 Unauthorized Error when attempting to change password with invalid password', function(done) { 47 | 48 | request.put(url) 49 | .send({ 50 | email: user.email, 51 | currentPassword: 'someWrongPassword', 52 | newPassword: 'newPassword' 53 | }) 54 | .expect(401, function(err, res) { 55 | should.not.exist(err); 56 | res.body.should.have.properties({ 57 | message: 'invalid password' 58 | }) 59 | 60 | done(); 61 | }); 62 | }); 63 | 64 | it('change user password', function(done) { 65 | 66 | request.put(url) 67 | .send({ 68 | email: user.email, 69 | currentPassword: user.password, 70 | newPassword: 'newPassword' 71 | }) 72 | .expect(200, function(err, res) { 73 | should.not.exist(err); 74 | 75 | done(); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/integration/checkSession.js: -------------------------------------------------------------------------------- 1 | process.env.MONGOHQ_URL = 'mongodb://localhost:27017/jsonresume-tests'; 2 | process.env.POSTMARK_API_KEY = 'POSTMARK_API_TEST'; // https://blog.postmarkapp.com/post/913165552/handling-email-in-your-test-environment 3 | 4 | var Q = require('q'); 5 | var should = require('should'); 6 | var supertest = require("supertest-as-promised")(Q.Promise); 7 | var utils = require('../utils'); 8 | var server = require('../../server'); 9 | var api = supertest(server), 10 | apiUtils = utils(api); 11 | var User = require('../../models/user'); 12 | 13 | describe('checkSession GET /session', function() { 14 | 15 | before(function(done){ 16 | User.remove({}, done); 17 | }); 18 | 19 | var agent = supertest.agent(server), // use cookies 20 | agentUtils = utils(agent), 21 | user = utils.getUserForTest(this); 22 | 23 | it('should return {auth: false} if there is no session', function() { 24 | 25 | return agent.get('/session') 26 | .send() 27 | .expect(utils.property({ 28 | auth: false 29 | })); 30 | }); 31 | 32 | it('should return {auth: true} if there is a valid session', function() { 33 | 34 | return agentUtils.createUser(user) 35 | .then(function() { 36 | return agent.get('/session') 37 | .send() 38 | .expect(utils.property({ 39 | auth: true 40 | })); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/integration/createSession.js: -------------------------------------------------------------------------------- 1 | process.env.MONGOHQ_URL = 'mongodb://localhost:27017/jsonresume-tests'; 2 | process.env.POSTMARK_API_KEY = 'POSTMARK_API_TEST'; // https://blog.postmarkapp.com/post/913165552/handling-email-in-your-test-environment 3 | var server = require('../../server'); 4 | var request = require('supertest')(server); 5 | var should = require('should'); 6 | var bcrypt = require('bcrypt-nodejs'); 7 | var User = require('../../models/user'); 8 | 9 | describe('createSession: POST /session ', function() { 10 | 11 | var url = '/session'; 12 | 13 | var user = { 14 | username: 'sessionLoginUsername', 15 | email: 'sessionLoginEmail', 16 | // FIXME should throw an error if we try to dave a raw Password 17 | // TODO use a mongoose virtual 18 | password: 'sessionLoginPassword', 19 | }; 20 | user.hash = bcrypt.hashSync(user.password); 21 | 22 | before(function(done) { 23 | User.remove({}, function(err) { // TODO: move this into a beforeAll function 24 | User.create(user, done); 25 | }); 26 | }); 27 | 28 | it('should return 200 OK and a session for a valid user', function(done) { 29 | 30 | request.post(url) 31 | .send(user) 32 | .expect(200, function(err, res) { 33 | 34 | should.not.exist(err); 35 | res.body.should.have.property('session'); 36 | 37 | res.body.should.have.properties({ 38 | auth: true, 39 | email: user.email, 40 | message: 'loggedIn', 41 | username: user.username 42 | }); 43 | 44 | done(); 45 | }); 46 | }); 47 | 48 | it('should return 401 Unauthorized for an incorrect password', function(done) { 49 | 50 | request.post(url) 51 | .send({ 52 | email: user.email, 53 | password: "different" 54 | }) 55 | // HTTP status 401 UNAUTHORIZED 56 | .expect(401, function(err, res) { 57 | 58 | done() 59 | }); 60 | }); 61 | 62 | it('should return 401 Unauthorized for an unregistered email', function(done) { 63 | request.post(url) 64 | .send({ 65 | email: "different", 66 | password: user.password 67 | }) 68 | // HTTP status 401 UNAUTHORIZED 69 | .expect(401, function(err, res) { 70 | 71 | done() 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/integration/createUser.js: -------------------------------------------------------------------------------- 1 | process.env.MONGOHQ_URL = 'mongodb://localhost:27017/jsonresume-tests'; 2 | process.env.POSTMARK_API_KEY = 'POSTMARK_API_TEST'; // https://blog.postmarkapp.com/post/913165552/handling-email-in-your-test-environment 3 | var server = require('../../server'); 4 | var request = require('supertest')(server); 5 | var should = require('should'); 6 | var bcrypt = require('bcrypt-nodejs'); 7 | var User = require('../../models/user'); 8 | 9 | describe('createUser: POST /user', function() { 10 | 11 | var user = { 12 | username: 'createUserUsername', 13 | email: 'createUserEmail', 14 | password: 'createUserPassword', 15 | }; 16 | 17 | var url = '/user'; 18 | 19 | before(function(done) { 20 | // TODO: move this into a beforeAll function 21 | User.remove({}, done); 22 | }); 23 | 24 | it('should return 201 Created', function(done) { 25 | request.post(url) 26 | .send(user) 27 | .expect(201, function(err, res) { 28 | should.not.exist(err); 29 | 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should return 409 CONFLICT when the email already exists', function(done) { 35 | request.post(url) 36 | .send({ 37 | username: "different", 38 | email: user.email, 39 | password: user.password 40 | }) 41 | // HTTP status 409 Conflict 42 | .expect(409, function(err, res) { 43 | should.not.exist(err); 44 | res.body.error.should.have.properties({ 45 | field: 'email', 46 | message: 'Email is already in use, maybe you forgot your password?' 47 | }); 48 | 49 | done(); 50 | }); 51 | }); 52 | 53 | it('should return 409 Conflict when the username already exists', function(done) { 54 | request.post(url) 55 | .send({ 56 | username: user.username, 57 | email: "different", 58 | password: user.password 59 | }) 60 | // HTTP status 409 Conflict 61 | .expect(409, function(err, res) { 62 | should.not.exist(err); 63 | res.body.error.should.have.properties({ 64 | field: 'username', 65 | message: 'This username is already taken, please try another one' 66 | }); 67 | 68 | done(); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/integration/deleteSession.js: -------------------------------------------------------------------------------- 1 | process.env.MONGOHQ_URL = 'mongodb://localhost:27017/jsonresume-tests'; 2 | process.env.POSTMARK_API_KEY = 'POSTMARK_API_TEST'; // https://blog.postmarkapp.com/post/913165552/handling-email-in-your-test-environment 3 | 4 | var Q = require('q'); 5 | var should = require('should'); 6 | var supertest = require("supertest-as-promised")(Q.Promise); 7 | var utils = require('../utils'); 8 | var server = require('../../server'); 9 | var HttpStatus = require('http-status-codes'); 10 | var api = supertest(server), 11 | apiUtils = utils(api); 12 | var User = require('../../models/user'); 13 | 14 | describe('DELETE session ID', function() { 15 | 16 | var agent = supertest.agent(server), // use cookies 17 | agentUtils = utils(agent), 18 | user = utils.getUserForTest(this); 19 | 20 | before(function(done){ 21 | User.remove({}, function(){ 22 | agentUtils.createUser(user).then(function(err){ 23 | done(); 24 | }); 25 | }); 26 | }); 27 | 28 | it('should end the session', function() { 29 | return agent.post('/session') 30 | .send(user) 31 | .then(function(res) { 32 | expect(res.body.session).to.exist; 33 | return agent.delete('/session/' + res.body.session) 34 | .send() 35 | .expect(HttpStatus.OK) 36 | .expect(utils.property({ 37 | auth: false 38 | })); 39 | }) 40 | .then(function() { 41 | return agent.get('/session') 42 | .send() 43 | .expect(utils.property({ 44 | auth: false 45 | })); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/integration/deleteUser.js: -------------------------------------------------------------------------------- 1 | process.env.MONGOHQ_URL = 'mongodb://localhost:27017/jsonresume-tests'; 2 | process.env.POSTMARK_API_KEY = 'POSTMARK_API_TEST'; // https://blog.postmarkapp.com/post/913165552/handling-email-in-your-test-environment 3 | var server = require('../../server'); 4 | var request = require('supertest')(server); 5 | var should = require('should'); 6 | var bcrypt = require('bcrypt-nodejs'); 7 | var User = require('../../models/user'); 8 | 9 | describe('deleteUser: DELETE /account ', function() { 10 | 11 | var url = '/account'; 12 | 13 | var user = { 14 | username: 'deleteUserUsername', 15 | email: 'deleteUserEmail', 16 | // FIXME should throw an error if we try to dave a raw Password 17 | // TODO use a mongoose virtual 18 | password: 'deleteUserPassword', 19 | }; 20 | user.hash = bcrypt.hashSync(user.password); 21 | 22 | before(function(done) { 23 | User.remove({}, function(err) { // TODO: move this into a beforeAll function 24 | User.create(user, done); 25 | }); 26 | }); 27 | 28 | it('should delete user account', function(done) { 29 | 30 | request.delete(url) 31 | .send({ 32 | email: user.email, 33 | password: user.password, 34 | }) 35 | .expect(200, function(err, res) { 36 | should.not.exist(err); 37 | res.body.should.have.properties({ 38 | message: '\nYour account has been successfully deleted.' 39 | }); 40 | 41 | done(); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/integration/index-test.js: -------------------------------------------------------------------------------- 1 | process.env.MONGOHQ_URL = 'mongodb://localhost:27017/jsonresume-tests'; 2 | process.env.POSTMARK_API_KEY = 'POSTMARK_API_TEST'; // https://blog.postmarkapp.com/post/913165552/handling-email-in-your-test-environment 3 | 4 | var should = require('should'); 5 | var server = require('../../server'); 6 | var request = require('supertest')(server); 7 | 8 | describe('/', function() { 9 | it('should return 200 OK', function(done) { 10 | request.get('/') 11 | .expect(200, function(err, res) { 12 | should.not.exist(err); 13 | 14 | done(); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/integration/renderResume.js: -------------------------------------------------------------------------------- 1 | process.env.MONGOHQ_URL = 'mongodb://localhost:27017/jsonresume-tests'; 2 | process.env.POSTMARK_API_KEY = 'POSTMARK_API_TEST'; // https://blog.postmarkapp.com/post/913165552/handling-email-in-your-test-environment 3 | var Q = require('q'); 4 | var bcrypt = require('bcrypt-nodejs'); 5 | var should = require('should'); 6 | var fixtures = require('../fixtures'); 7 | var supertest = require("supertest-as-promised")(Q.Promise); 8 | var xrequest = require('supertest'); 9 | var HttpStatus = require('http-status-codes'); 10 | var nock = require('nock'); 11 | var utils = require('../utils'); 12 | var server = require('../../server'); 13 | var api = supertest(server), 14 | apiUtils = utils(api); 15 | 16 | var User = require('../../models/user'); 17 | var Resume = require('../../models/resume'); 18 | 19 | 20 | 21 | describe('renderResume: GET /:username ', function() { 22 | 23 | var user = fixtures.user.test3; 24 | user.hash = bcrypt.hashSync(user.password); 25 | 26 | before(function(done) { 27 | // create a test user 28 | User.remove({}, function(err) { 29 | Resume.remove({}, function(err) { 30 | User.create(user, done); 31 | }); 32 | }); 33 | }); 34 | 35 | it('should return 404 Not Found for an invalid user', function(done) { 36 | api.get('/not_a_real_user') 37 | .send() 38 | .expect(HttpStatus.NOT_FOUND, function(err, res) { 39 | should.not.exist(err); 40 | 41 | done(); 42 | }); 43 | }); 44 | 45 | it('should return 404 Not Found for a valid user with no resume', function(done) { 46 | 47 | api.get('/' + user.username) 48 | .send() 49 | .expect(HttpStatus.NOT_FOUND, function(err, res) { 50 | should.not.exist(err); 51 | 52 | done(); 53 | }); 54 | }); 55 | 56 | it('should return 200 OK for a valid user with a resume', function() { 57 | var themeReq = nock('https://themes.jsonresume.org') 58 | .post('/theme/' + server.DEFAULT_THEME) 59 | .reply(200, 'An example resume'); 60 | return api.post('/resume') 61 | .send({ 62 | email: user.email, 63 | password: user.password, 64 | resume: { 65 | test: "Put a real resume here?" 66 | } 67 | }) 68 | .then(function() { 69 | return api.get('/' + user.username) 70 | .send() 71 | .expect(HttpStatus.OK) 72 | .expect('An example resume'); 73 | }) 74 | .then(function() { 75 | expect(themeReq.isDone()).to.be.true; 76 | }); 77 | }); 78 | 79 | it('should return 500 Internal Server Error if the theme manager service returns an error', function() { 80 | var themeReq = nock('https://themes.jsonresume.org') 81 | .post('/theme/' + server.DEFAULT_THEME) 82 | .replyWithError({ 83 | message: 'server is down' 84 | }); 85 | return api.post('/resume') 86 | .send({ 87 | email: user.email, 88 | password: user.password, 89 | resume: { 90 | test: "Put a real resume here?" 91 | } 92 | }) 93 | .then(function() { 94 | return api.get('/' + user.username) 95 | .send() 96 | .expect(HttpStatus.INTERNAL_SERVER_ERROR) 97 | .expect({ 98 | message: 'server is down' 99 | }); 100 | }) 101 | .then(function() { 102 | expect(themeReq.isDone()).to.be.true; 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/integration/showStats.js: -------------------------------------------------------------------------------- 1 | process.env.MONGOHQ_URL = 'mongodb://localhost:27017/jsonresume-tests'; 2 | process.env.POSTMARK_API_KEY = 'POSTMARK_API_TEST'; // https://blog.postmarkapp.com/post/913165552/handling-email-in-your-test-environment 3 | var server = require('../../server'); 4 | var request = require('supertest')(server); 5 | var should = require('should'); 6 | 7 | describe('/stats', function() { 8 | it('should return stats', function(done) { 9 | request.get('/stats') 10 | .expect(200, function(err, res) { 11 | console.log(res.body); 12 | should.not.exist(err); 13 | // TODO acturlly test stat numbers 14 | res.body.should.have.property('userCount'); 15 | res.body.should.have.property('resumeCount'); 16 | res.body.should.have.property('views'); 17 | 18 | done(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/integration/updateTheme.js: -------------------------------------------------------------------------------- 1 | process.env.MONGOHQ_URL = 'mongodb://localhost:27017/jsonresume-tests'; 2 | var request = require('supertest')(require('../../server')); 3 | var bcrypt = require('bcrypt-nodejs'); 4 | var should = require('should'); 5 | var fixtures = require('../fixtures'); 6 | var User = require('../../models/user'); 7 | 8 | describe('Resumes: ', function() { 9 | 10 | var user = fixtures.user.default; 11 | user.hash = bcrypt.hashSync(user.password); 12 | 13 | before(function(done) { 14 | // create a test user 15 | User.create(user, done); 16 | // create New resume 17 | 18 | }); 19 | 20 | var resumeJson = require('../resume.json'); 21 | 22 | // it('should create a new guest resume', function(done) { 23 | // 24 | // request.post('/resume') 25 | // .send({ 26 | // guest: true, 27 | // resume: resumeJson 28 | // }) 29 | // .expect(200, function(err, res) { 30 | // 31 | // should.not.exist(err); 32 | // res.body.should.have.property('url'); 33 | // res.body.url.should.startWith('https://registry.jsonresume.org/'); 34 | // // url should end with the randomly generated guestUsername 35 | // // TODO test that test resume was actually created. 36 | // 37 | // done(); 38 | // }); 39 | // }); 40 | // 41 | // it('should create a resume for an existing user', function(done) { 42 | // 43 | // request.post('/resume') 44 | // .send({ 45 | // password: user.password, 46 | // email: user.email, 47 | // resume: resumeJson 48 | // }) 49 | // .expect(200, function(err, res) { 50 | // should.not.exist(err); 51 | // res.body.should.have.property('url', 'https://registry.jsonresume.org/' + user.username); 52 | // 53 | // done(); 54 | // }); 55 | // }); 56 | 57 | it('should update resume theme', function(done) { 58 | 59 | request.put('/resume') 60 | .send({ 61 | password: user.password, 62 | email: user.email, 63 | theme: 'flat' 64 | }) 65 | .expect(200, function(err, res) { 66 | should.not.exist(err); 67 | res.body.should.have.property('url', 'https://registry.jsonresume.org/' + user.username); 68 | // TODO find resume and check theme field 69 | 70 | done(); 71 | }); 72 | }); 73 | 74 | it('should not update resume theme with wrongPassword', function(done) { 75 | 76 | request.put('/resume') 77 | .send({ 78 | password: 'wrongPassword', 79 | email: user.email, 80 | theme: 'flat' 81 | }) 82 | .expect(200, function(err, res) { // TODO return some HTTP error code 83 | should.not.exist(err); 84 | 85 | // should probably be invalid password not session 86 | res.body.should.have.property('sessionError', 'invalid session'); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('should not update resume theme with non-existant email', function(done) { 92 | 93 | request.put('/resume') 94 | .send({ 95 | password: user.password, 96 | email: 'nonExistatnEmail', 97 | theme: 'flat' 98 | }) 99 | .expect(200, function(err, res) { 100 | should.not.exist(err); 101 | 102 | res.body.should.have.property('sessionError', 'invalid session'); // TODO change returned msg to invalid email 103 | done(); 104 | }); 105 | }); 106 | 107 | xit('TODO: should return some error if theme does not exist??'); 108 | 109 | }); 110 | -------------------------------------------------------------------------------- /test/integration/upsertResume.js: -------------------------------------------------------------------------------- 1 | process.env.MONGOHQ_URL = 'mongodb://localhost:27017/jsonresume-tests'; 2 | var request = require('supertest')(require('../../server')); 3 | var bcrypt = require('bcrypt-nodejs'); 4 | var should = require('should'); 5 | var fixtures = require('../fixtures'); 6 | var User = require('../../models/user'); 7 | 8 | describe('Resumes: ', function() { 9 | 10 | var user = fixtures.user.default; 11 | user.hash = bcrypt.hashSync(user.password); 12 | 13 | before(function(done) { 14 | // create a test user 15 | User.create(user, done); 16 | }); 17 | 18 | var resumeJson = require('../resume.json'); 19 | 20 | it('should create a new guest resume', function(done) { 21 | 22 | request.post('/resume') 23 | .send({ 24 | guest: true, 25 | resume: resumeJson 26 | }) 27 | .expect(200, function(err, res) { 28 | 29 | should.not.exist(err); 30 | res.body.should.have.property('url'); 31 | res.body.url.should.startWith('https://registry.jsonresume.org/'); 32 | // url should end with the randomly generated guestUsername 33 | // TODO test that test resume was actually created. 34 | 35 | done(); 36 | }); 37 | }); 38 | 39 | it('should create a resume for an existing user', function(done) { 40 | 41 | request.post('/resume') 42 | .send({ 43 | password: user.password, 44 | email: user.email, 45 | resume: resumeJson 46 | }) 47 | .expect(200, function(err, res) { 48 | should.not.exist(err); 49 | res.body.should.have.property('url', 'https://registry.jsonresume.org/' + user.username); 50 | 51 | done(); 52 | }); 53 | }); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /test/resume.json: -------------------------------------------------------------------------------- 1 | { 2 | "basics": { 3 | "name": "test", 4 | "label": "Programmer", 5 | "picture": "", 6 | "email": "test4@test.com", 7 | "phone": "(912) 555-4321", 8 | "website": "https://richardhendricks.com", 9 | "summary": "Richard hails from Tulsa. He has earned degrees from the University of Oklahoma and Stanford. (Go Sooners and Cardinal!) Before starting Pied Piper, he worked for Hooli as a part time software developer. While his work focuses on applied information theory, mostly optimizing lossless compression schema of both the length-limited and adaptive variants, his non-work interests range widely, everything from quantum computing to chaos theory. He could tell you about it, but THAT would NOT be a “length-limited” conversation!", 10 | "location": { 11 | "address": "2712 Broadway St", 12 | "postalCode": "CA 94115", 13 | "city": "San Francisco", 14 | "countryCode": "US", 15 | "region": "California" 16 | }, 17 | "profiles": [ 18 | { 19 | "network": "Twitter", 20 | "username": "neutralthoughts", 21 | "url": "" 22 | }, 23 | { 24 | "network": "SoundCloud", 25 | "username": "dandymusicnl", 26 | "url": "https://soundcloud.com/dandymusicnl" 27 | } 28 | ] 29 | }, 30 | "work": [ 31 | { 32 | "company": "Pied Piper", 33 | "position": "CEO/President", 34 | "website": "https://piedpiper.com", 35 | "startDate": "2013-12-01", 36 | "endDate": "2014-12-01", 37 | "summary": "Pied Piper is a multi-platform technology based on a proprietary universal compression algorithm that has consistently fielded high Weisman Scores™ that are not merely competitive, but approach the theoretical limit of lossless compression.", 38 | "highlights": [ 39 | "Build an algorithm for artist to detect if their music was violating copy right infringement laws", 40 | "Successfully won Techcrunch Disrupt", 41 | "Optimized an algorithm that holds the current world record for Weisman Scores" 42 | ] 43 | } 44 | ], 45 | "volunteer": [ 46 | { 47 | "organization": "CoderDojo", 48 | "position": "Teacher", 49 | "website": "https://coderdojo.com/", 50 | "startDate": "2012-01-01", 51 | "endDate": "2013-01-01", 52 | "summary": "Global movement of free coding clubs for young people.", 53 | "highlights": [ 54 | "Awarded 'Teacher of the Month'" 55 | ] 56 | } 57 | ], 58 | "education": [ 59 | { 60 | "institution": "University of Oklahoma", 61 | "area": "Information Technology", 62 | "studyType": "Bachelor", 63 | "startDate": "2011-06-01", 64 | "endDate": "2014-01-01", 65 | "gpa": "4.0", 66 | "courses": [ 67 | "DB1101 - Basic SQL", 68 | "CS2011 - Java Introduction" 69 | ] 70 | } 71 | ], 72 | "awards": [ 73 | { 74 | "title": "Digital Compression Pioneer Award", 75 | "date": "2014-11-01", 76 | "awarder": "Techcrunch", 77 | "summary": "There is no spoon." 78 | } 79 | ], 80 | "publications": [ 81 | { 82 | "name": "Video compression for 3d media", 83 | "publisher": "Hooli", 84 | "releaseDate": "2014-10-01", 85 | "website": "https://en.wikipedia.org/wiki/Silicon_Valley_(TV_series)", 86 | "summary": "Innovative middle-out compression algorithm that changes the way we store data." 87 | } 88 | ], 89 | "skills": [ 90 | { 91 | "name": "Web Development", 92 | "level": "Master", 93 | "keywords": [ 94 | "HTML", 95 | "CSS", 96 | "Javascript" 97 | ] 98 | }, 99 | { 100 | "name": "Compression", 101 | "level": "Master", 102 | "keywords": [ 103 | "Mpeg", 104 | "MP4", 105 | "GIF" 106 | ] 107 | } 108 | ], 109 | "languages": [ 110 | { 111 | "language": "English", 112 | "fluency": "Native speaker" 113 | } 114 | ], 115 | "interests": [ 116 | { 117 | "name": "Wildlife", 118 | "keywords": [ 119 | "Ferrets", 120 | "Unicorns" 121 | ] 122 | } 123 | ], 124 | "references": [ 125 | { 126 | "name": "Erlich Bachman", 127 | "reference": "It is my pleasure to recommend Richard, his performance working as a consultant for Main St. Company proved that he will be a valuable addition to any company." 128 | } 129 | ] 130 | } 131 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | global.expect = chai.expect; 3 | 4 | chai.use(require('chai-properties')); 5 | 6 | var cleanUsername = function(s) { 7 | // remove spaces and slashes to make nice URLs 8 | return s.replace(/ /g, "_").replace("/", ""); 9 | }; 10 | 11 | var getTestName = function(test) { 12 | return cleanUsername(test.fullTitle()); 13 | }; 14 | 15 | var utils = function(api) { 16 | return { 17 | createUser: function(user) { 18 | return api.post('/user') 19 | .send(user) 20 | .then(function(res) { 21 | return res.body; 22 | }); 23 | }, 24 | getSessionFor: function(user) { 25 | return api.post('/session') 26 | .send(user) 27 | .then(function(res) { 28 | return res.body.session; 29 | }); 30 | } 31 | }; 32 | }; 33 | 34 | // should only be used inside callbacks to `describe` 35 | utils.getUserForTest = function(test) { 36 | var testName = getTestName(test); 37 | return { 38 | username: testName, 39 | email: testName+"@example.com", 40 | password: "password" 41 | }; 42 | }; 43 | 44 | utils.property = function(obj) { 45 | if (typeof obj === 'string') { 46 | return function(res) { 47 | expect(res.body).to.have.property(obj); 48 | }; 49 | } else { 50 | return function(res) { 51 | expect(res.body).to.have.properties(obj); 52 | }; 53 | } 54 | }; 55 | 56 | module.exports = utils; 57 | --------------------------------------------------------------------------------