├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── assets ├── css │ ├── base │ │ ├── all.less │ │ ├── reset.less │ │ ├── typography.less │ │ └── variables.less │ ├── com │ │ ├── activity-feed.less │ │ ├── alerts.less │ │ ├── all.less │ │ ├── archive-list.less │ │ ├── beaker-prompt.less │ │ ├── breadcrumbs.less │ │ ├── buttons.less │ │ ├── card.less │ │ ├── columns-grid.less │ │ ├── dashboard.less │ │ ├── dat-link.less │ │ ├── dropdown-menu.less │ │ ├── featured-content.less │ │ ├── features.less │ │ ├── forms.less │ │ ├── hero.less │ │ ├── invoice.less │ │ ├── jumbotron.less │ │ ├── link.less │ │ ├── logo.less │ │ ├── pro-tag.less │ │ ├── progress-bar.less │ │ ├── prompt.less │ │ ├── stripe.less │ │ ├── tools-cta.less │ │ └── tooltip.less │ ├── font-awesome │ │ ├── animated.less │ │ ├── bordered-pulled.less │ │ ├── core.less │ │ ├── fixed-width.less │ │ ├── font-awesome.less │ │ ├── icons.less │ │ ├── larger.less │ │ ├── list.less │ │ ├── mixins.less │ │ ├── path.less │ │ ├── rotated-flipped.less │ │ ├── screen-reader.less │ │ ├── stacked.less │ │ └── variables.less │ ├── forms │ │ ├── account-change-password.less │ │ ├── account-upgrade.less │ │ ├── archive-settings.less │ │ ├── forgot-password.less │ │ ├── login.less │ │ ├── new-archive.less │ │ ├── register.less │ │ └── reset-password.less │ ├── jquery.dataTables.min.css │ ├── layout │ │ ├── all.less │ │ ├── containers.less │ │ ├── footer.less │ │ └── nav.less │ ├── main.less │ ├── pages │ │ ├── about.less │ │ ├── account.less │ │ ├── admin-dashboard.less │ │ ├── archive.less │ │ ├── base.less │ │ ├── error.less │ │ ├── home.less │ │ ├── pricing.less │ │ ├── profile.less │ │ └── support.less │ └── utils │ │ ├── all.less │ │ └── decorators.less ├── fonts │ ├── Catamaran-Bold.ttf │ ├── FontAwesome.otf │ ├── OpenSans-Regular.ttf │ ├── RobotoMono-Bold.ttf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── html │ ├── 404.html │ ├── about.html │ ├── acceptable-use.html │ ├── account-cancel-plan.html │ ├── account-canceled-plan.html │ ├── account-change-password.html │ ├── account-update-email.html │ ├── account-upgrade.html │ ├── account-upgraded.html │ ├── account.html │ ├── admin-dashboard-archives.html │ ├── admin-dashboard-report.html │ ├── admin-dashboard-reports.html │ ├── admin-dashboard-stats.html │ ├── admin-dashboard-user.html │ ├── admin-dashboard-users.html │ ├── admin-visits-list.html │ ├── archive.html │ ├── com │ │ ├── activity.html │ │ ├── archive-list-item.ejs │ │ ├── dashboard.html │ │ ├── featured-content.html │ │ ├── features.html │ │ ├── footer-light.html │ │ ├── footer.html │ │ ├── full-nav.html │ │ ├── mobile-nav.html │ │ ├── nav.html │ │ ├── session-alerts.html │ │ ├── stdhead.html │ │ ├── stdjs.html │ │ ├── tools-cta.html │ │ └── your-archives.html │ ├── error.html │ ├── explore-activity.html │ ├── explore.html │ ├── forgot-password.html │ ├── frontpage.html │ ├── hero.html │ ├── login.html │ ├── new-archive.html │ ├── pricing.html │ ├── privacy.html │ ├── register-pro.html │ ├── register.html │ ├── registered.html │ ├── reset-password.html │ ├── support.html │ ├── terms.html │ └── user.html ├── images │ ├── apps │ │ ├── editor.png │ │ ├── nexus.svg │ │ ├── paste-dat.svg │ │ ├── photo-album.svg │ │ └── rss.png │ ├── cc-americanexpress.png │ ├── cc-dinersclub.png │ ├── cc-discover.png │ ├── cc-jcb.png │ ├── cc-mastercard.png │ ├── cc-visa.png │ ├── demo-screencap.jpg │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── logo-blue.png │ ├── logo-white.png │ ├── logo.png │ ├── paul.jpg │ ├── sort_asc.png │ ├── sort_asc_disabled.png │ ├── sort_both.png │ ├── sort_desc.png │ ├── sort_desc_disabled.png │ └── tara.jpg └── js │ ├── account-cancel-plan.js │ ├── account-change-password.js │ ├── account-update-email.js │ ├── account-upgrade.js │ ├── account.js │ ├── admin-dashboard-archives.js │ ├── admin-dashboard-report.js │ ├── admin-dashboard-reports.js │ ├── admin-dashboard-stats.js │ ├── admin-dashboard-user.js │ ├── admin-dashboard-users.js │ ├── admin-visits-list.js │ ├── archive-admin.js │ ├── archive.js │ ├── clipboard.js │ ├── d3.min.js │ ├── forgot-password.js │ ├── frontpage.js │ ├── jquery-3.1.1.min.js │ ├── jquery.dataTables.file-size-sorting-plugin.js │ ├── jquery.dataTables.min.js │ ├── login.js │ ├── moment.min.js │ ├── nav.js │ ├── new-archive.js │ ├── register-pro.js │ ├── register.js │ ├── report.js │ ├── reset-password.js │ ├── ua.js │ ├── upload-progress.js │ ├── user-admin.js │ ├── util.js │ ├── zepto-patches.js │ └── zepto.min.js ├── bin.js ├── config.defaults.yml ├── contributors.yml ├── docs ├── README.md ├── components │ ├── jobs.md │ ├── locks.md │ └── triggers.md ├── flows │ ├── dat-ownership-proof.md │ ├── forgot-password.md │ └── registration.md ├── schemas │ ├── access-scopes.md │ ├── events.md │ └── leveldb.md ├── webapis-v1.md └── webapis.md ├── index.js ├── lib ├── analytics.js ├── apis │ ├── admin.js │ ├── archive-files.js │ ├── archives.js │ ├── pages.js │ ├── reports.js │ ├── service.js │ ├── user-content.js │ └── users.js ├── archiver.js ├── config.js ├── const.js ├── crypto.js ├── dbs │ ├── activity.js │ ├── archives.js │ ├── base.js │ ├── legacy-leveldb │ │ ├── activity.js │ │ ├── archives.js │ │ ├── featured-archives.js │ │ ├── index.js │ │ ├── reports.js │ │ └── users.js │ ├── migrations │ │ └── 001-initial-schema.sql │ ├── reports.js │ ├── schemas.js │ └── users.js ├── helpers.js ├── index.js ├── less-express.js ├── lock.js ├── mailer.js ├── monitoring.js ├── proofs.js ├── sanitizers.js ├── sessions.js ├── templates │ ├── directory-listing-page.js │ └── mail │ │ ├── forgot-password.js │ │ ├── support.js │ │ ├── verification.js │ │ └── verify-update-email.js └── validators.js ├── package-lock.json ├── package.json ├── pm2.config.js ├── readme.md └── test ├── activity.js ├── admin.js ├── archives.js ├── dat-pinning-client.js ├── featured-archives.js ├── lib ├── dat.js ├── server.js └── util.js ├── quotas.js ├── scaffold └── testdat1 │ ├── dat.json │ └── hello.txt └── users.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .hypercloud 4 | .hashbase 5 | config.*.yml 6 | !config.defaults.yml 7 | test/scaffold/testdat1/.dat 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "8" 5 | - "10" -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We <3 PRs. 4 | 5 | Follow this guide for making changes, and then adding yourself to the in-app contributors page. 6 | 7 | ## Making Changes 8 | 9 | * Create a topic branch from where you want to base your work. 10 | * This is usually the master branch. 11 | * Make commits of logical units. 12 | * Make sure your commit messages are in the proper format. If appropriate, [use an extended commit to describe the changes involved.](https://git-scm.com/book/ch5-2.html) 13 | 14 | ```` 15 | The short-line description is capitalized at front, and <50 chars. 16 | 17 | If you feel you need to write more about your commit, do so here. This can 18 | help future developers understand the logic of the changes you made, and 19 | sometimes that future developer is you! 20 | ```` 21 | 22 | * Make sure you have added the necessary tests for your changes. 23 | * Run _all_ the tests to assure nothing else was accidentally broken. 24 | * Update the documentation. Add new documentation files as needed. 25 | 26 | ## Common reasons a pull-request will not be accepted 27 | 28 | * The changes need to have tests added. 29 | * The changes need to be documented. 30 | * The changes don't pass the `standard` formatting test. 31 | 32 | Make sure you update tests and docs! 33 | 34 | ## Adding yourself to the Contributors page 35 | 36 | After your first successful PR, you should create a second PR to add yourself to the contributors.yml doc. 37 | 38 | Open `./contributors.yml` and add to the bottom a line that follows this format: 39 | 40 | ```yaml 41 | --- 42 | name: Bob Robertson 43 | catchphrase: That's-a spicey meatball! 44 | website: https://bobs-homepage.com 45 | ``` 46 | 47 | Of course, you'll want to put your own name, catchphrase, and website. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Blue Link Labs 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 | -------------------------------------------------------------------------------- /assets/css/base/all.less: -------------------------------------------------------------------------------- 1 | @import "reset"; 2 | @import "variables"; 3 | @import "typography"; 4 | -------------------------------------------------------------------------------- /assets/css/base/reset.less: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | header, 4 | nav, 5 | div, 6 | h1, 7 | h2, 8 | h3, 9 | h4, 10 | h5, 11 | h6, 12 | label, 13 | input, 14 | p { 15 | display: block; 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | *, 21 | *:before, 22 | *:after { 23 | box-sizing: border-box; 24 | } 25 | 26 | main { 27 | min-height: 50vh; 28 | } 29 | 30 | @media (min-width: @desktop) { 31 | main { 32 | min-height: 70vh; 33 | } 34 | 35 | body { 36 | position: relative; 37 | height: 100vh; 38 | } 39 | } 40 | 41 | ul { 42 | padding: 0; 43 | margin: 0; 44 | list-style: none; 45 | } 46 | 47 | a, 48 | a:hover { 49 | color: inherit; 50 | text-decoration: none; 51 | } 52 | -------------------------------------------------------------------------------- /assets/css/base/typography.less: -------------------------------------------------------------------------------- 1 | // only use with light text on a dark background 2 | .antialiased { 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | } 6 | 7 | body { 8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 9 | font-size: 14.5px; 10 | font-weight: 400; 11 | line-height: 1.4; 12 | text-rendering: optimizeLegibility; 13 | color: @color-text; 14 | background: #fff; 15 | } 16 | 17 | @font-face { 18 | font-family: 'Catamaran'; 19 | src: url(/assets/fonts/Catamaran-Bold.ttf); 20 | font-weight: 700; 21 | } 22 | 23 | h1 { 24 | font-family: Catamaran; 25 | } 26 | 27 | strong { 28 | font-weight: 700; 29 | } 30 | 31 | em { 32 | font-style: italic; 33 | } 34 | 35 | code { 36 | font-family: Consolas, 'Lucida Console', Monaco, monospace; 37 | } 38 | 39 | .monospace-font { 40 | font-family: Consolas, 'Lucida Console', Monaco, monospace; 41 | font-size: .85rem; 42 | } -------------------------------------------------------------------------------- /assets/css/base/variables.less: -------------------------------------------------------------------------------- 1 | @red: #F44336; 2 | @red--600: #E53935; 3 | 4 | // pink 5 | @pink: #E91E63; 6 | @pink--700: #C2185B; 7 | @pink--accent-200: #FF4081; 8 | @pink--accent-400: #F50057; 9 | 10 | @purple: #9C27B0; 11 | @deep-purple: #673AB7; 12 | @indigo: #3F51B5; 13 | 14 | // blue 15 | @blue: #2196F3; 16 | @blue--600: #1E88E5; 17 | @blue--700: #1976D2; 18 | @blue--800: #1565c0; 19 | @blue--900: #0d47a1; 20 | 21 | @blue--accent-400: #2979FF; 22 | @blue--accent-700: #2962FF; 23 | 24 | // light blue 25 | @light-blue: #03A9F4; 26 | @light-blue--700: #0288d1; 27 | @light-blue--900: #01579b; 28 | 29 | @cyan: #00BCD4; 30 | @teal: #009688; 31 | 32 | @green: #4CAF50; 33 | @green--accent-400: #00E676; 34 | @green--accent-700: #00C853; 35 | 36 | @light-green: #8BC34A; 37 | @lime: #CDDC39; 38 | @yellow: #FFEB3B; 39 | @amber: #FFC107; 40 | @orange: #FF9800; 41 | @deep-orange: #FF5722; 42 | @blue-grey: #607D8B; 43 | 44 | @color-link: mix(@color-text, @blue--accent-400, 10%); 45 | 46 | // background colors 47 | @color-bg: mix(@blue--accent-700, fadeout(#000, 98%), 1%); 48 | @color-bg--white-transparent: fadeout(#fff, 75%); 49 | @color-bg--black-transparent: fadeout(#000, 95%); 50 | @color-bg--light-gray: tint(#000, 95%); 51 | @color-bg--lighter-gray: tint(#000, 97%); 52 | @color-bg--lightest-gray: tint(#000, 98%); 53 | @color-bg--card-form: tint(#000, 99%); 54 | @color-bg--hover: tint(#000, 95%); 55 | @color-bg--light-blue: tint(@blue, 95%); 56 | 57 | // border colors 58 | @color-border--transparent-btn: fadeout(#000, 90%); 59 | @color-border--input: fadeout(#000, 75%); 60 | @color-border--light-gray: tint(#000, 85%); 61 | @color-border--lighter-gray: tint(#000, 90%); 62 | @color-border--lightest-gray: fadeout(#000, 94%); 63 | @color-border--dark-gray: tint(#000, 50%); 64 | @color-border--gray: tint(#000, 75%); 65 | @color-border--light-blue: fadeout(@blue, 70%); 66 | 67 | // text colors 68 | @color-text: #111; 69 | @color-text--muted: tint(@color-text, 40%); 70 | @color-text--light: tint(@color-text, 65%); 71 | @color-text--white-transparent: fadeout(#fff, 1%); 72 | @color-text--form-warning: mix(@color-text, @pink, 25%); 73 | 74 | // media queries 75 | @tablet-vertical: 600px; 76 | @tablet-horizontal: 800px; 77 | @desktop: 950px; 78 | @desktop-wide: 1300px; 79 | 80 | // z-indexes 81 | @on-top: 3; 82 | @underneath: -3; -------------------------------------------------------------------------------- /assets/css/com/activity-feed.less: -------------------------------------------------------------------------------- 1 | .activity-feed { 2 | padding: 0; 3 | 4 | .event { 5 | display: block; 6 | color: @color-text--muted; 7 | margin-bottom: .25rem; 8 | 9 | * { 10 | display: inline-block; 11 | } 12 | } 13 | 14 | a { 15 | color: @color-link; 16 | 17 | &:hover { 18 | &:extend(.link); 19 | } 20 | } 21 | 22 | .more { 23 | display: block; 24 | margin-top: 1rem; 25 | 26 | &:after { 27 | display: inline-block; 28 | content: '\00bb'; 29 | margin-left: 5px; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /assets/css/com/alerts.less: -------------------------------------------------------------------------------- 1 | .alert { 2 | &:extend(.rounded); 3 | padding: 4px 12px; 4 | margin: .5rem 0; 5 | background: @color-bg--light-gray; 6 | color: mix(@color-text, @color-bg--light-gray, 40%); 7 | font-size: .9em; 8 | min-width: 300px; 9 | max-width: 500px; 10 | margin: auto; 11 | margin-bottom: 5px; 12 | text-align: center; 13 | 14 | &:empty { 15 | display: none; 16 | } 17 | 18 | &.success { 19 | background: tint(@green, 65%); 20 | color: mix(@color-text, @green, 35%); 21 | } 22 | 23 | &.primary { 24 | background: tint(@blue, 85%); 25 | color: mix(@color-text, @blue, 30%); 26 | } 27 | 28 | &.warning { 29 | background: tint(@pink, 85%); 30 | color: mix(@color-text, @pink, 25%); 31 | } 32 | } 33 | 34 | .session-alerts { 35 | .alert { 36 | display: block; 37 | margin: 0.5rem auto; 38 | text-align: center; 39 | } 40 | } -------------------------------------------------------------------------------- /assets/css/com/all.less: -------------------------------------------------------------------------------- 1 | @import "logo"; 2 | @import "link"; 3 | @import "hero"; 4 | @import "featured-content"; 5 | @import "pro-tag"; 6 | @import "card"; 7 | @import "alerts"; 8 | @import "buttons"; 9 | @import "forms"; 10 | @import "dropdown-menu"; 11 | @import "progress-bar"; 12 | @import "stripe"; 13 | @import "activity-feed"; 14 | @import "archive-list"; 15 | @import "breadcrumbs"; 16 | @import "dat-link"; 17 | @import "tools-cta"; 18 | @import "columns-grid"; 19 | @import "features"; 20 | @import "tooltip"; 21 | @import "invoice"; 22 | @import "prompt"; 23 | @import "beaker-prompt"; 24 | @import "dashboard"; -------------------------------------------------------------------------------- /assets/css/com/beaker-prompt.less: -------------------------------------------------------------------------------- 1 | .beaker-prompt { 2 | 3 | &.hidden { 4 | display: none !important; 5 | } 6 | } -------------------------------------------------------------------------------- /assets/css/com/breadcrumbs.less: -------------------------------------------------------------------------------- 1 | .breadcrumbs { 2 | font-size: 1.2rem; 3 | font-weight: 500; 4 | color: @color-link; 5 | 6 | > *:not(:last-child) { 7 | 8 | &:after { 9 | display: inline-block; 10 | content: '\203A'; 11 | margin-left: 9px; 12 | margin-right: 3px; 13 | } 14 | 15 | &:hover { 16 | &:extend(.link); 17 | } 18 | } 19 | 20 | > *:last-child { 21 | color: @color-text; 22 | } 23 | } -------------------------------------------------------------------------------- /assets/css/com/card.less: -------------------------------------------------------------------------------- 1 | .card { 2 | background: #fff; 3 | border: 1px solid @color-border--light-gray; 4 | border-radius: 4px; 5 | padding: 1.25rem; 6 | overflow: hidden; 7 | 8 | h1 { 9 | margin-bottom: 1rem; 10 | } 11 | 12 | &.modal { 13 | max-width: 400px; 14 | margin: auto; 15 | margin-bottom: 1.5rem; 16 | 17 | p:not(:last-child) { 18 | margin-bottom: 1rem; 19 | } 20 | 21 | a { 22 | &:extend(.link); 23 | color: @color-link; 24 | } 25 | } 26 | 27 | *:first-child { 28 | margin-top: 0; 29 | padding-top: 0; 30 | } 31 | 32 | *:last-child { 33 | margin-bottom: 0; 34 | padding-bottom: 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /assets/css/com/columns-grid.less: -------------------------------------------------------------------------------- 1 | .columns-grid { 2 | display: grid; 3 | grid-template-columns: 1fr; 4 | grid-gap: 1.25rem; 5 | 6 | @media (min-width: @tablet-vertical) { 7 | grid-template-columns: 1fr 1fr; 8 | } 9 | 10 | @media (min-width: @desktop) { 11 | grid-template-columns: 1fr 1fr 1fr 1fr; 12 | } 13 | } -------------------------------------------------------------------------------- /assets/css/com/dashboard.less: -------------------------------------------------------------------------------- 1 | #dashboard { 2 | padding: 2rem; 3 | background: #fff; 4 | background: @color-bg; 5 | border-bottom: 1px solid @color-border--light-gray; 6 | font-size: .85rem !important; 7 | 8 | .container { 9 | &:extend(.flex); 10 | justify-content: space-between; 11 | flex-wrap: wrap !important; 12 | } 13 | 14 | .stats { 15 | flex-basis: 100%; 16 | margin-bottom: 2rem; 17 | 18 | @media(min-width: @tablet-horizontal) { 19 | flex-basis: 45%; 20 | margin-bottom: 0; 21 | } 22 | 23 | a:hover .label { 24 | &:extend(.link); 25 | } 26 | 27 | .stat { 28 | &:extend(.flex); 29 | justify-content: space-between; 30 | align-items: baseline; 31 | border-bottom: 1px solid rgba(0,0,0,.05); 32 | padding: 3px 0; 33 | 34 | .value { 35 | font-weight: 700; 36 | font-size: 1.25rem; 37 | color: rgba(0,0,0,.65); 38 | } 39 | 40 | .label { 41 | padding-left: 10px; 42 | color: rgba(0,0,0,.85); 43 | } 44 | 45 | @media (min-width: @tablet-horizontal) { 46 | justify-content: flex-start; 47 | 48 | .value { 49 | font-size: 2.5rem; 50 | } 51 | } 52 | } 53 | } 54 | 55 | .tools { 56 | &:extend(.flex); 57 | align-items: flex-start; 58 | flex: 1; 59 | color: rgba(0,0,0,.7); 60 | 61 | .archives-container { 62 | &:extend(.card); 63 | padding: 0; 64 | width: 100%; 65 | } 66 | 67 | .header { 68 | &:extend(.flex); 69 | width: 100%; 70 | justify-content: space-between; 71 | padding: 5px 10px; 72 | align-items: center; 73 | background: @color-bg--lightest-gray; 74 | 75 | h1 { 76 | display: inline-block; 77 | margin-bottom: 0; 78 | font-size: .85rem; 79 | font-family: -apple-system, BlinkMacSystemFont; 80 | color: rgba(0,0,0,.75); 81 | font-weight: 500; 82 | } 83 | 84 | i { 85 | font-size: .75rem; 86 | } 87 | } 88 | 89 | .your-archives { 90 | width: 100%; 91 | } 92 | 93 | a.more { 94 | font-size: .8rem; 95 | color: rgba(0,0,0,.7); 96 | margin-top: 5px; 97 | 98 | &:hover { 99 | &:extend(.link); 100 | } 101 | } 102 | 103 | @media(min-width: @tablet-horizontal) { 104 | margin-left: 3rem; 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /assets/css/com/dat-link.less: -------------------------------------------------------------------------------- 1 | .dat-link { 2 | &:extend(.monospace-font); 3 | font-size: .85rem; 4 | } -------------------------------------------------------------------------------- /assets/css/com/dropdown-menu.less: -------------------------------------------------------------------------------- 1 | .dropdown-menu-link { 2 | position: relative; 3 | cursor: pointer; 4 | } 5 | 6 | .dropdown-menu { 7 | &:extend(.rounded); 8 | &:extend(.box-shadow); 9 | display: none; 10 | position: absolute; 11 | top: 30px; 12 | right: 0; 13 | background: #fff; 14 | color: @color-text; 15 | text-shadow: none; 16 | z-index: @on-top; 17 | padding: .25rem 0; 18 | min-width: 175px; 19 | max-width: 300px; 20 | border: 1px solid @color-border--light-gray; 21 | 22 | &.open { 23 | display: block; 24 | } 25 | 26 | .dropdown-item { 27 | padding: .25rem 1rem; 28 | width: 100%; 29 | 30 | &:hover { 31 | background: @color-bg--hover; 32 | } 33 | } 34 | 35 | hr { 36 | width: 100%; 37 | height: 1px; 38 | border: none; 39 | background: @color-border--light-gray; 40 | margin: 0; 41 | } 42 | 43 | hr + .dropdown-item { 44 | margin-top: -1rem; 45 | } 46 | 47 | .dropdown-item + hr { 48 | margin-top: -1rem; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /assets/css/com/featured-content.less: -------------------------------------------------------------------------------- 1 | .featured-content { 2 | 3 | .item { 4 | display: inline-block; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /assets/css/com/features.less: -------------------------------------------------------------------------------- 1 | .features.columns-grid { 2 | 3 | .column h3 { 4 | margin-top: 1rem; 5 | } 6 | .column:nth-child(1) { 7 | border-top: 10px solid @purple; 8 | } 9 | .column:nth-child(2) { 10 | border-top: 10px solid @green; 11 | } 12 | .column:nth-child(3) { 13 | border-top: 10px solid @red; 14 | } 15 | .column:nth-child(4) { 16 | border-top: 10px solid @orange; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /assets/css/com/forms.less: -------------------------------------------------------------------------------- 1 | .form { 2 | font-size: .9rem; 3 | } 4 | 5 | .form.card-form, 6 | .form + .form-followup { 7 | &:extend(.card); 8 | max-width: 400px; 9 | margin: 1rem auto; 10 | padding: 1rem; 11 | border: 1px solid @color-border--light-gray; 12 | } 13 | 14 | .modal-form-container { 15 | display: none; 16 | position: fixed; 17 | top: 0; 18 | left: 0; 19 | width: 100%; 20 | height: 100%; 21 | background: fadeout(#000, 60%); 22 | z-index: 10; 23 | 24 | &.visible { 25 | display: block; 26 | } 27 | } 28 | 29 | .form.modal-form { 30 | &:extend(.card); 31 | margin: auto; 32 | margin-top: 50px; 33 | max-width: 400px; 34 | } 35 | 36 | input, 37 | textarea { 38 | &:extend(.rounded); 39 | outline: none; 40 | border: 1px solid @color-border--input; 41 | width: 100%; 42 | padding: .5rem; 43 | font-size: .85rem; 44 | 45 | &:focus { 46 | outline: none; 47 | border: 1px solid fadeout(@blue--accent-400, 5%); 48 | } 49 | } 50 | 51 | input[type="radio"] { 52 | display: inline-block; 53 | width: 20px; 54 | } 55 | 56 | .form-heading { 57 | margin-bottom: 1rem; 58 | } 59 | 60 | .form-desc { 61 | 62 | a { 63 | &:extend(.link); 64 | } 65 | } 66 | 67 | .form-desc, 68 | .form-help { 69 | margin-bottom: 1.5rem; 70 | } 71 | 72 | .form-footer { 73 | font-size: .85rem; 74 | color: @color-text--muted; 75 | padding: 1rem; 76 | margin: 1rem -1rem -1rem -1rem; 77 | background: shade(@color-bg--card-form, 2%); 78 | border-top: 1px solid @color-border--light-gray; 79 | 80 | a { 81 | color: @color-link; 82 | &:extend(.link); 83 | } 84 | } 85 | 86 | .form-desc.bordered { 87 | padding-bottom: 1.5rem; 88 | border-bottom: 1px solid @color-border--light-gray; 89 | } 90 | 91 | .form label { 92 | font-weight: 500; 93 | margin-bottom: .5rem; 94 | } 95 | 96 | .form-group { 97 | margin: 1rem 0; 98 | 99 | &.warning { 100 | 101 | input, 102 | textarea { 103 | border: 1px solid @color-text--form-warning; 104 | } 105 | 106 | .form-control-feedback { 107 | color: @color-text--form-warning; 108 | } 109 | } 110 | } 111 | 112 | .form-control-feedback { 113 | font-size: .8rem; 114 | } 115 | 116 | .form .actions { 117 | &:extend(.flex); 118 | align-items: baseline; 119 | justify-content: space-between; 120 | margin-top: .5rem; 121 | 122 | div { 123 | margin-left: auto; 124 | } 125 | 126 | button[type="submit"], 127 | .submit { 128 | margin-left: auto; 129 | } 130 | 131 | .alternate-link { 132 | color: @color-link; 133 | } 134 | } 135 | 136 | .form + .form-followup a { 137 | color: @color-link; 138 | } -------------------------------------------------------------------------------- /assets/css/com/invoice.less: -------------------------------------------------------------------------------- 1 | .invoice { 2 | & > div { 3 | display: flex; 4 | 5 | span:first-child { 6 | flex: 1; 7 | font-weight: bold; 8 | } 9 | } 10 | 11 | .total { 12 | border-top: 1px solid #ddd; 13 | margin: 5px 0 1rem; 14 | padding-top: 5px; 15 | } 16 | } -------------------------------------------------------------------------------- /assets/css/com/jumbotron.less: -------------------------------------------------------------------------------- 1 | .jumbotron { 2 | padding-top: 6rem; 3 | padding-bottom: 6rem; 4 | margin-bottom: 0; 5 | background-color: #fff; 6 | } 7 | 8 | .jumbotron p:last-child { 9 | margin-bottom: 0; 10 | } 11 | 12 | .jumbotron-heading { 13 | font-weight: 300; 14 | } 15 | 16 | .jumbotron .container { 17 | max-width: 40rem; 18 | } -------------------------------------------------------------------------------- /assets/css/com/link.less: -------------------------------------------------------------------------------- 1 | .link { 2 | // TODO use smart underline; 3 | text-decoration: underline; 4 | cursor: pointer; 5 | } 6 | 7 | button.link { 8 | outline: none; 9 | font-size: inherit; 10 | font-family: inherit; 11 | border: none; 12 | background: none; 13 | padding: 0; 14 | } -------------------------------------------------------------------------------- /assets/css/com/logo.less: -------------------------------------------------------------------------------- 1 | .hashbase-logo { 2 | 3 | img { 4 | width: 160px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /assets/css/com/pro-tag.less: -------------------------------------------------------------------------------- 1 | .pro-tag { 2 | &:extend(.antialiased); 3 | &:extend(.rounded); 4 | display: inline-block; 5 | text-transform: uppercase; 6 | background: @green--accent-700; 7 | color: @color-text--white-transparent; 8 | font-weight: 500; 9 | padding: 0 3px; 10 | font-size: .7rem; 11 | letter-spacing: .01rem; 12 | margin: 0 .25rem; 13 | text-align: center; 14 | } 15 | -------------------------------------------------------------------------------- /assets/css/com/progress-bar.less: -------------------------------------------------------------------------------- 1 | .progress-bar-container { 2 | 3 | .label { 4 | display: block; 5 | font-weight: 500; 6 | font-size: .75rem; 7 | margin-bottom: .25rem; 8 | } 9 | } 10 | 11 | .progress-bar { 12 | &:extend(.rounded); 13 | position: relative; 14 | width: 400px; 15 | max-width: 100%; 16 | height: 10px; 17 | border: 1px solid @color-border--dark-gray; 18 | margin-bottom: 1rem; 19 | 20 | .progress { 21 | height: 100%; 22 | background: @green--accent-700; 23 | transition: width 0.2s; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/css/com/prompt.less: -------------------------------------------------------------------------------- 1 | .close-prompt { 2 | position: absolute; 3 | top: -7px; 4 | left: -7px; 5 | background: @color-bg--light-gray; 6 | color: #fff; 7 | width: 20px; 8 | height: 20px; 9 | line-height: 1.5; 10 | border-radius: 50%; 11 | border: 2px solid #fff; 12 | cursor: pointer; 13 | font-size: 11.25px; 14 | text-align: center; 15 | -webkit-text-stroke: .5px @blue; 16 | 17 | &:hover { 18 | color: fadeout(#fff, 25%); 19 | } 20 | } -------------------------------------------------------------------------------- /assets/css/com/stripe.less: -------------------------------------------------------------------------------- 1 | #card-element { 2 | &:extend(.rounded); 3 | border: 1px solid @color-border--light-gray; 4 | padding: .5rem; 5 | margin-bottom: 1rem; 6 | 7 | + .form-control-feedback { 8 | margin-top: -.75rem; 9 | color: @color-text--form-warning; 10 | 11 | &:empty { 12 | display: none; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /assets/css/com/tools-cta.less: -------------------------------------------------------------------------------- 1 | .tools-cta { 2 | &:extend(.card); 3 | 4 | .tool { 5 | 6 | &:first-of-type { 7 | margin-bottom: 1.5rem; 8 | } 9 | 10 | @media (min-width: @tablet-horizontal) { 11 | margin-bottom: .5rem; 12 | } 13 | 14 | .info { 15 | &:extend(.flex); 16 | flex-wrap: nowrap !important; 17 | } 18 | 19 | h3 { 20 | margin: 0 0 .25rem 0; 21 | } 22 | 23 | a { 24 | color: @color-link; 25 | 26 | &.title { 27 | font-weight: 500; 28 | font-size: .825rem; 29 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 30 | } 31 | 32 | &:hover { 33 | &:extend(.link); 34 | } 35 | } 36 | 37 | h5 { 38 | margin: 0; 39 | margin-bottom: 5px; 40 | font-size: .9rem; 41 | } 42 | 43 | .description { 44 | font-size: .8rem; 45 | } 46 | 47 | .links { 48 | color: @color-link; 49 | margin-top: 5px; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /assets/css/com/tooltip.less: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | &:extend(.antialiased); 3 | position: absolute; 4 | right: 0; 5 | top: 35px; 6 | z-index: 5; 7 | background: @color-text; 8 | color: #fff; 9 | padding: 5px 10px; 10 | font-size: .75rem; 11 | border-radius: 3px; 12 | line-height: 1; 13 | 14 | &:before { 15 | position: absolute; 16 | top: -5px; 17 | right: 10px; 18 | content: ''; 19 | display: block; 20 | width: 0; 21 | height: 0; 22 | border-left: 5px solid transparent; 23 | border-right: 5px solid transparent; 24 | border-bottom: 5px solid @color-text; 25 | font-weight: 500; 26 | } 27 | } -------------------------------------------------------------------------------- /assets/css/font-awesome/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /assets/css/font-awesome/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /assets/css/font-awesome/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /assets/css/font-awesome/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /assets/css/font-awesome/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /assets/css/font-awesome/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /assets/css/font-awesome/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /assets/css/font-awesome/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | .sr-only() { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | .sr-only-focusable() { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /assets/css/font-awesome/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /assets/css/font-awesome/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /assets/css/font-awesome/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /assets/css/font-awesome/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /assets/css/forms/account-change-password.less: -------------------------------------------------------------------------------- 1 | .form-account-change-password { 2 | max-width: 300px; 3 | margin: 3rem auto; 4 | } 5 | .form-account-change-password-heading { 6 | font-weight: 200; 7 | } 8 | -------------------------------------------------------------------------------- /assets/css/forms/account-upgrade.less: -------------------------------------------------------------------------------- 1 | .form-account-upgrade { 2 | width: 25em; 3 | margin: 0 auto; 4 | 5 | .StripeElement { 6 | background-color: white; 7 | padding: 8px 12px; 8 | border-radius: 4px; 9 | border: 1px solid #ddd; 10 | box-shadow: 0 1px 3px 0 #e6ebf1; 11 | -webkit-transition: box-shadow 150ms ease; 12 | transition: box-shadow 150ms ease; 13 | } 14 | 15 | .StripeElement--focus { 16 | box-shadow: 0 1px 3px 0 #cfd7df; 17 | } 18 | 19 | .StripeElement--invalid { 20 | border-color: #fa755a; 21 | } 22 | 23 | .StripeElement--webkit-autofill { 24 | background-color: #fefde5 !important; 25 | } 26 | } -------------------------------------------------------------------------------- /assets/css/forms/archive-settings.less: -------------------------------------------------------------------------------- 1 | .form-archive-settings { 2 | 3 | // this form places all controls inline with each other 4 | // that requires a few adjustments to the action buttons to fit correctly 5 | 6 | display: flex; 7 | align-items: center; 8 | 9 | .form-group { 10 | flex: 1; 11 | margin: 0; 12 | } 13 | 14 | .actions { 15 | margin: 0 0 0 1rem; 16 | 17 | .btn { 18 | padding: 0.6rem; 19 | height: auto; 20 | margin-top: 8px; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /assets/css/forms/forgot-password.less: -------------------------------------------------------------------------------- 1 | .form-forgot-password { 2 | max-width: 300px; 3 | margin: 3rem auto; 4 | } 5 | .form-forgot-password-heading { 6 | font-weight: 200; 7 | } -------------------------------------------------------------------------------- /assets/css/forms/login.less: -------------------------------------------------------------------------------- 1 | .form-login { 2 | max-width: 300px; 3 | margin: 3rem auto; 4 | } 5 | .form-login-heading { 6 | font-weight: 200; 7 | } -------------------------------------------------------------------------------- /assets/css/forms/new-archive.less: -------------------------------------------------------------------------------- 1 | #add-archive-form { 2 | 3 | + .form-followup { 4 | 5 | div[data-target] { 6 | &:extend(.flex); 7 | width: 100%; 8 | align-items: center; 9 | justify-content: space-between; 10 | padding: .75rem 0; 11 | border-bottom: 1px solid @color-border--light-gray; 12 | 13 | &:first-of-type { 14 | padding-top: 0; 15 | } 16 | 17 | &:last-of-type { 18 | padding-bottom: 0; 19 | border-bottom: 0; 20 | } 21 | 22 | &:hover { 23 | 24 | p { 25 | cursor: pointer; 26 | color: @color-link; 27 | } 28 | } 29 | } 30 | 31 | .content { 32 | display: none; 33 | padding: .75rem 0; 34 | 35 | &.visible { 36 | display: block; 37 | flex-basis: 100%; 38 | } 39 | 40 | iframe { 41 | max-width: 366px; 42 | height: 205px; 43 | } 44 | } 45 | } 46 | 47 | .dat-picker-container { 48 | position: relative; 49 | 50 | .dat-picker { 51 | display: none; 52 | position: absolute; 53 | z-index: 1; 54 | right: 4px; 55 | top: 4px; 56 | 57 | .btn { 58 | height: 26px; 59 | font-weight: 400; 60 | } 61 | } 62 | 63 | &.enabled { 64 | .dat-picker { 65 | display: inline; 66 | } 67 | 68 | input { 69 | padding-right: 80px; 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /assets/css/forms/register.less: -------------------------------------------------------------------------------- 1 | .form-register { 2 | padding: 3rem 0 6rem; 3 | } 4 | .form-register-heading { 5 | } -------------------------------------------------------------------------------- /assets/css/forms/reset-password.less: -------------------------------------------------------------------------------- 1 | .form-reset-password { 2 | max-width: 300px; 3 | margin: 3rem auto; 4 | } 5 | .form-reset-password-heading { 6 | font-weight: 200; 7 | } -------------------------------------------------------------------------------- /assets/css/layout/all.less: -------------------------------------------------------------------------------- 1 | @import "containers"; 2 | @import "nav"; 3 | @import "footer"; 4 | -------------------------------------------------------------------------------- /assets/css/layout/containers.less: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | max-width: 1200px; 4 | margin: auto; 5 | padding: 0 7px; 6 | 7 | &.large { 8 | max-width: 1000px; 9 | } 10 | 11 | &.big { 12 | max-width: 850px; 13 | } 14 | 15 | &.medium { 16 | max-width: 700px; 17 | } 18 | 19 | &.small { 20 | // TODO: use this instead of manually setting .card-form width -tbv 21 | max-width: 400px; 22 | } 23 | 24 | @media (min-width: @tablet-vertical) { 25 | padding: 0 15px; 26 | } 27 | 28 | @media (min-width: @tablet-horizontal) { 29 | padding: 0 25px; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /assets/css/layout/footer.less: -------------------------------------------------------------------------------- 1 | .footer { 2 | color: rgba(0,0,0,.85); 3 | padding: 2rem 0; 4 | font-size: .8rem; 5 | border-top: 1px solid rgba(0,0,0,.1); 6 | 7 | h2 { 8 | margin-bottom: .5rem; 9 | text-transform: uppercase; 10 | font-size: inherit; 11 | } 12 | 13 | .hashbase-logo { 14 | display: block; 15 | margin-bottom: .5rem; 16 | } 17 | 18 | .about { 19 | 20 | p { 21 | margin-bottom: .5rem; 22 | font-size: .8rem; 23 | color: rgba(0,0,0,.75); 24 | } 25 | 26 | a { 27 | &:extend(.link); 28 | color: @color-link; 29 | } 30 | } 31 | 32 | .about, .footer-links { 33 | width: 100%; 34 | margin-bottom: 1.5rem; 35 | } 36 | 37 | .footer-links { 38 | 39 | ul { 40 | margin-bottom: 1.5rem; 41 | } 42 | 43 | li { 44 | margin-bottom: .25rem; 45 | } 46 | 47 | a { 48 | color: @color-link; 49 | 50 | &:hover { 51 | &:extend(.link); 52 | } 53 | } 54 | } 55 | 56 | @media (min-width: @desktop) { 57 | padding: 3em 0; 58 | 59 | .about, .footer-links { 60 | display: inline-block; 61 | margin-bottom: 0; 62 | vertical-align: top; 63 | width: 48%; 64 | } 65 | 66 | .about { 67 | margin-right: 1.5rem; 68 | } 69 | 70 | .footer-links { 71 | display: inline-flex; 72 | justify-content: flex-end; 73 | } 74 | 75 | .footer-links .hashbase, 76 | .footer-links .external { 77 | display: inline-block; 78 | vertical-align: top; 79 | margin-left: 4rem; 80 | } 81 | } 82 | 83 | &.light { 84 | background: none; 85 | color: @color-text; 86 | font-weight: 400; 87 | border-top: 0; 88 | 89 | .container { 90 | padding-top: 2rem; 91 | border-top: 1px solid rgba(0,0,0,.1); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /assets/css/main.less: -------------------------------------------------------------------------------- 1 | @import './font-awesome/font-awesome'; 2 | @import './com/jumbotron'; 3 | 4 | @import './forms/register'; 5 | @import './forms/login'; 6 | @import './forms/forgot-password'; 7 | @import './forms/reset-password'; 8 | @import './forms/account-upgrade'; 9 | @import './forms/account-change-password'; 10 | @import './forms/new-archive'; 11 | 12 | @import "base/all"; 13 | @import "layout/all"; 14 | @import "pages/base"; 15 | @import "com/all"; 16 | @import "utils/all"; 17 | -------------------------------------------------------------------------------- /assets/css/pages/about.less: -------------------------------------------------------------------------------- 1 | @import './base'; 2 | 3 | main.about { 4 | 5 | .section.card { 6 | max-width: 640px; 7 | 8 | h3 { 9 | margin-top: 0.5rem; 10 | } 11 | p { 12 | max-width: none; 13 | } 14 | a { 15 | color: @color-link; 16 | } 17 | } 18 | 19 | .section.team .member { 20 | display: inline-block; 21 | text-align: center; 22 | margin-right: 3rem; 23 | 24 | img { 25 | &:extend(.rounded); 26 | width: 125px; 27 | height: 125px; 28 | } 29 | 30 | .title { 31 | margin: .5rem 0; 32 | } 33 | 34 | .links { 35 | &:extend(.flex); 36 | &:extend(.rounded); 37 | border: 1px solid; 38 | justify-content: space-between; 39 | margin: auto; 40 | font-size: 1rem; 41 | 42 | a { 43 | flex: 1; 44 | 45 | &:hover { 46 | background: @color-text; 47 | color: #fff; 48 | } 49 | 50 | &:not(:last-child) { 51 | border-right: 1px solid; 52 | } 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /assets/css/pages/account.less: -------------------------------------------------------------------------------- 1 | @import './base'; 2 | @import '../com/card'; 3 | 4 | main.account { 5 | 6 | .container { 7 | &:extend(.card); 8 | } 9 | 10 | a:not(.username-link) { 11 | &:extend(.link); 12 | } 13 | 14 | .actions { 15 | vertical-align: bottom; 16 | 17 | > * { 18 | margin-right: .5rem; 19 | } 20 | } 21 | 22 | .section.plan { 23 | 24 | .card-number { 25 | &:extend(.monospace-font); 26 | font-size: .85rem; 27 | display: inline-block; 28 | line-height: 100%; 29 | vertical-align: bottom; 30 | } 31 | 32 | img.card-type { 33 | width: 27px; 34 | margin-left: 5px; 35 | vertical-align: bottom; 36 | line-height: 100%; 37 | } 38 | } 39 | 40 | #form-card-update { 41 | display: none; 42 | 43 | &.open { 44 | display: block; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /assets/css/pages/admin-dashboard.less: -------------------------------------------------------------------------------- 1 | @import './base'; 2 | 3 | .admin-dashboard { 4 | 5 | h1 .admin-nav { 6 | font-weight: normal; 7 | font-size: 1.4rem; 8 | margin-left: 0.5rem; 9 | 10 | a { 11 | color: @color-link; 12 | } 13 | } 14 | 15 | #stats { 16 | tbody td { 17 | font-size: 3rem; 18 | text-align: center; 19 | width: 120px; 20 | } 21 | } 22 | 23 | .record { 24 | display: flex; 25 | 26 | .record-content { 27 | flex: 1; 28 | 29 | textarea { 30 | width: 100%; 31 | max-width: 1000px; 32 | overflow-x: auto; 33 | font-family: monospace; 34 | font-size: 16px; 35 | line-height: 24px; 36 | } 37 | } 38 | 39 | .record-actions { 40 | flex: 0 0 400px; 41 | margin-left: 1rem; 42 | } 43 | } 44 | 45 | #status { 46 | padding: 5px 10px; 47 | font-weight: 700; 48 | font-size: .85rem; 49 | border-radius: 3px; 50 | -webkit-font-smoothing: antialiased; 51 | text-transform: uppercase; 52 | color: #fff; 53 | margin: 1rem 0; 54 | 55 | &.open { 56 | background: shade(@red, 10%); 57 | } 58 | 59 | &.closed { 60 | background: shade(@green, 10%); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /assets/css/pages/base.less: -------------------------------------------------------------------------------- 1 | @import '../base/all'; 2 | @import '../utils/all'; 3 | 4 | main { 5 | padding: 4rem 0; 6 | background: @color-bg; 7 | 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6 { 14 | margin-bottom: 1rem; 15 | } 16 | 17 | h2, 18 | h3, 19 | h4, 20 | h5, 21 | h6 { 22 | margin-top: 1.5rem; 23 | } 24 | 25 | .section { 26 | 27 | &:not(:last-child) { 28 | margin-bottom: 2rem; 29 | } 30 | 31 | p, 32 | form { 33 | max-width: 600px; 34 | margin: 1rem 0; 35 | } 36 | 37 | p:last-child { 38 | margin-bottom: 0; 39 | } 40 | 41 | a { 42 | &:extend(.link); 43 | } 44 | } 45 | } 46 | 47 | // nested ordered lists 48 | ol ol { 49 | list-style-type: lower-alpha; 50 | } 51 | 52 | main.terms, 53 | main.privacy, 54 | main.acceptable-use { 55 | 56 | p:not(:last-child) { 57 | margin-bottom: 1rem; 58 | } 59 | 60 | ul { 61 | margin-left: 40px; 62 | list-style-type: disc; 63 | margin-bottom: 1rem; 64 | } 65 | } -------------------------------------------------------------------------------- /assets/css/pages/error.less: -------------------------------------------------------------------------------- 1 | @import '../com/link'; 2 | 3 | main.error { 4 | text-align: center; 5 | 6 | .alert { 7 | margin: 1rem auto; 8 | } 9 | 10 | a { 11 | &:extend(.link); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /assets/css/pages/pricing.less: -------------------------------------------------------------------------------- 1 | @import "./base"; 2 | 3 | main.pricing { 4 | 5 | .tiers { 6 | max-width: 600px; 7 | margin: 0 auto; 8 | } 9 | 10 | .tier { 11 | &:extend(.rounded); 12 | display: inline-block; 13 | position: relative; 14 | border: 1px solid @color-border--gray; 15 | background: @color-bg--card-form; 16 | width: 100%; 17 | margin-bottom: 1rem; 18 | cursor: pointer; 19 | 20 | .heading { 21 | &:extend(.flex); 22 | justify-content: space-between; 23 | border-bottom: 1px solid @color-border--gray; 24 | padding: .5rem 1rem; 25 | font-weight: 500; 26 | 27 | .title { 28 | font-size: 1.4rem; 29 | } 30 | 31 | .price { 32 | margin-top: 5px; // TODO: ew -tbv 33 | } 34 | } 35 | 36 | .space { 37 | text-align: center; 38 | padding: 1.2rem 0; 39 | 40 | span { 41 | display: block; 42 | font-size: 3rem; 43 | text-align: center; 44 | line-height: 1; 45 | } 46 | } 47 | 48 | .btn.signup { 49 | position: absolute; 50 | right: 1rem; 51 | bottom: 1rem; 52 | 53 | &.transparent { 54 | border-color: @color-border--gray; 55 | } 56 | } 57 | 58 | &:hover { 59 | &:extend(.box-shadow); 60 | } 61 | 62 | &.pro { 63 | background: @color-bg--card-form; // TODO rename this -tbv 64 | border-color: @color-border--light-blue; 65 | 66 | .heading { 67 | border-color: @color-border--light-blue; 68 | } 69 | 70 | .space span { 71 | font-weight: 500; 72 | } 73 | } 74 | 75 | @media (min-width: @tablet-horizontal) { 76 | width: 45%; 77 | height: 160px; 78 | margin-bottom: 0; 79 | margin-right: 1rem; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /assets/css/pages/profile.less: -------------------------------------------------------------------------------- 1 | @import "./base"; 2 | 3 | main.profile { 4 | padding: 1rem 0; 5 | 6 | .content-container { 7 | &:extend(.flex); 8 | } 9 | 10 | .content { 11 | &:extend(.card); 12 | } 13 | 14 | .content, 15 | .sidebar { 16 | margin-bottom: 2rem; 17 | flex-basis: 100%; 18 | } 19 | 20 | @media (min-width: @tablet-horizontal) { 21 | 22 | .content { 23 | flex: 2; 24 | } 25 | 26 | .sidebar { 27 | flex: 1; 28 | margin-left: 2rem; 29 | } 30 | 31 | } 32 | 33 | .breadcrumbs { 34 | vertical-align: middle; 35 | } 36 | 37 | .breadcrumbs + .btn.new-archive { 38 | display: inline-block; 39 | margin-left: 1rem; 40 | } 41 | 42 | .archives .heading { 43 | margin: .25rem .75rem; 44 | 45 | * { 46 | display: inline-block; 47 | } 48 | 49 | h2 { 50 | text-transform: uppercase; 51 | font-size: .75rem; 52 | font-weight: 400; 53 | margin: 0; 54 | } 55 | 56 | .btn.new-archive { 57 | float: right; 58 | } 59 | } 60 | 61 | .sidebar { 62 | 63 | > * { 64 | margin-bottom: 1.5rem; 65 | } 66 | 67 | p { 68 | margin-bottom: 1rem; 69 | } 70 | 71 | .send-email { 72 | font-size: .9rem; 73 | } 74 | 75 | .activity { 76 | font-size: .85rem; 77 | 78 | h2 { 79 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 80 | text-transform: uppercase; 81 | font-size: .8rem; 82 | font-weight: 400; 83 | margin-bottom: .5rem; 84 | margin-top: 0; 85 | border-bottom: 1px solid @color-border--light-gray; 86 | } 87 | } 88 | } 89 | 90 | /* TODO this is so hackish and should be removed when we have a more clear 91 | * conception of how to display lists of archives to users -tbv 92 | */ 93 | .archive-footer { 94 | border-bottom: 0; 95 | } 96 | 97 | @media (min-width: @tablet-horizontal) { 98 | .content { 99 | display: flex; 100 | } 101 | 102 | .archives { 103 | flex: 2; 104 | padding-right: 4rem; 105 | } 106 | 107 | .sidebar { 108 | display: block; 109 | flex: 1; 110 | 111 | h2 { 112 | margin-top: 0; 113 | } 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /assets/css/pages/support.less: -------------------------------------------------------------------------------- 1 | @import './base'; 2 | 3 | main.support { 4 | 5 | a { 6 | &:extend(.link); 7 | } 8 | 9 | .card { 10 | text-align: center; 11 | 12 | a { 13 | color: @color-link; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /assets/css/utils/all.less: -------------------------------------------------------------------------------- 1 | @import "decorators"; 2 | -------------------------------------------------------------------------------- /assets/css/utils/decorators.less: -------------------------------------------------------------------------------- 1 | .rounded { 2 | border-radius: 3px; 3 | overflow: hidden; 4 | } 5 | 6 | .rounded-extra { 7 | border-radius: 5px; 8 | overflow: hidden; 9 | } 10 | 11 | .box-shadow { 12 | box-shadow: 0 20px 60px -10px fadeout(black, 80%); 13 | } 14 | 15 | .text-shadow { 16 | text-shadow: fadeout(black, 85%) 1px 1px 1px; 17 | } 18 | 19 | .flex { 20 | display: flex; 21 | flex-wrap: wrap; 22 | } 23 | 24 | .overflow-ellipsis { 25 | overflow: hidden; 26 | text-overflow: ellipsis; 27 | white-space: nowrap; 28 | } 29 | 30 | .nobreak { 31 | white-space: nowrap; 32 | } 33 | 34 | .text-muted { 35 | color: @color-text--muted; 36 | } -------------------------------------------------------------------------------- /assets/fonts/Catamaran-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/fonts/Catamaran-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /assets/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/RobotoMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/fonts/RobotoMono-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /assets/html/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page not found - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 |
10 |
11 |

404 - Page not found

12 |
13 |
14 | <% include com/footer-light.html %> 15 | <% include com/stdjs.html %> 16 | 17 | 18 | -------------------------------------------------------------------------------- /assets/html/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | About - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | 9 | <%- include('com/nav.html', {navClass: ''}) %> 10 | 11 |
12 |
13 |

Hashbase is shutting down

14 | 15 |
16 |

17 | We've sadly decided it's time to shut down Hashbase. 18 | The team is moving on to new projects, and we no longer have the 19 | resources to keep the service running. 20 |

21 |

22 | Thank you all for the years of support and care. We wouldn't 23 | have gotten anywhere without you. 24 |

25 |

26 | The source code for Hashbase is available here. 27 |

28 |

29 | — Paul 30 |

31 |
32 |
33 | 34 |
35 | <% include com/footer.html %> 36 | <% include com/stdjs.html %> 37 | 38 | 39 | -------------------------------------------------------------------------------- /assets/html/account-cancel-plan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cancel Pro Plan: <%= sessionUser.username %> - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 | 10 |
11 |
12 | 28 |
29 |
30 | 31 | <% include com/footer.html %> 32 | <% include com/stdjs.html %> 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /assets/html/account-canceled-plan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Your plan has been canceled - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 | 10 |
11 |
12 | 22 | 23 | 24 |
25 |
26 | 27 | <% include com/footer.html %> 28 | <% include com/stdjs.html %> 29 | 30 | 31 | -------------------------------------------------------------------------------- /assets/html/account-change-password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Change Password - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | 9 | <%- include('com/nav.html', {navClass: ''}) %> 10 | 11 |
12 |
13 | 36 |
37 |
38 | 39 | <% include com/footer-light.html %> 40 | <% include com/stdjs.html %> 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /assets/html/account-update-email.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Update Email Address- <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 | 10 |
11 |
12 |
13 | 14 |

Update your email address

15 | 16 | 17 | 18 |

19 | The email address associated with your Hashbase account is 20 | <%= sessionUser.email %> 21 |

22 | 23 |

24 | 25 | 26 | 27 |

28 | 29 |

30 | 31 | 32 | 33 |

34 | 35 |
36 | « Back to account 37 | 38 |
39 |
40 |
41 |
42 | 43 | <% include com/footer-light.html %> 44 | <% include com/stdjs.html %> 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /assets/html/account-upgrade.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Upgrade: <%= sessionUser.username %> - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 | 10 |
11 |
12 |
13 |

14 | 15 |

16 | 10 GB of storage 17 |

18 |

19 | Upgrade to Hashbase Pro for $7 per month. 20 |

21 | 22 |

23 | 26 |

27 |
28 | 29 |
30 | 31 | 32 | 33 |
34 |
Service fee7.00
35 |
Sales tax (<%= salesTax %>%)+ 0.58
36 |
Total$7.58
37 |
38 |
39 | 43 |
44 |
45 |
46 |
47 | 48 | <% include com/footer-light.html %> 49 | <% include com/stdjs.html %> 50 | 51 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /assets/html/account-upgraded.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | You've been upgraded - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 | 10 |
11 |
12 | 24 | 25 | 26 |
27 |
28 | 29 | <% include com/footer.html %> 30 | <% include com/stdjs.html %> 31 | 32 | 33 | -------------------------------------------------------------------------------- /assets/html/admin-dashboard-archives.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Admin Dashboard - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | 9 | 10 | <%- include('com/nav.html', {navClass: ''}) %> 11 | 12 |
13 |
14 |

Admin Dashboard 15 | 16 | stats | 17 | users | 18 | archives | 19 | reports 20 | 21 |

22 | 23 |

Admins only. Weirdos get out.

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
KeyNameOwnerDisk UsageTotal SizeUpload date
KeyNameOwnerDisk UsageTotal SizeUpload date
47 | 48 |
49 |
50 | 51 | <% include com/footer.html %> 52 | <% include com/stdjs.html %> 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /assets/html/admin-dashboard-report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Admin Dashboard - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | 9 | <%- include('com/nav.html', {navClass: ''}) %> 10 | 11 |
12 |
13 |

Admin Dashboard 14 | 15 | stats | 16 | users | 17 | archives | 18 | reports 19 | 20 |

21 | 22 | 23 | 24 |

Admins only. Weirdos get out.

25 | 26 |
27 |
28 | <% if (record.status === 'open') { %> 29 |

30 | Status: Needs review 31 |

32 | <% } else { %> 33 |

34 | Status: Resolved 35 |

36 | <% } %> 37 | 38 |
39 |
40 | 41 |
42 |
43 | <% if (record.status === 'open') { %> 44 | 45 | <% } else { %> 46 | 47 | <% } %> 48 |
49 |
50 | 51 |
52 |
53 | 54 | <% include com/footer.html %> 55 | <% include com/stdjs.html %> 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /assets/html/admin-dashboard-reports.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Admin Dashboard - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | 9 | 10 | <%- include('com/nav.html', {navClass: ''}) %> 11 | 12 |
13 |
14 |

Admin Dashboard 15 | 16 | stats | 17 | users | 18 | archives | 19 | reports 20 | 21 |

22 | 23 |

Admins only. Weirdos get out.

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
ArchiveArchive ownerReported byReasonReported atNotesStatus
ArchiveArchive ownerReported byReasonReported atNotesStatus
49 | 50 |
51 |
52 | 53 | <% include com/footer.html %> 54 | <% include com/stdjs.html %> 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /assets/html/admin-dashboard-user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Admin Dashboard - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | 9 | <%- include('com/nav.html', {navClass: ''}) %> 10 | 11 |
12 |
13 |

Admin Dashboard 14 | 15 | stats | 16 | users | 17 | archives | 18 | reports 19 | 20 |

21 | 22 | 23 | 24 |

Admins only. Weirdos get out.

25 | 26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 | <% if (user.suspension) { %> 35 | 36 | <% } else { %> 37 | 38 | <% } %> 39 |
40 |
41 |
42 | <% if (!user.isEmailVerified) { %> 43 | 44 | <% } %> 45 |
46 |
47 |
48 | 49 |
50 |
51 | 52 | <% include com/footer.html %> 53 | <% include com/stdjs.html %> 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /assets/html/admin-dashboard-users.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Admin Dashboard - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | 9 | 10 | <%- include('com/nav.html', {navClass: ''}) %> 11 | 12 |
13 |
14 |

Admin Dashboard 15 | 16 | stats | 17 | users | 18 | archives | 19 | reports 20 | 21 |

22 | 23 |

Admins only. Weirdos get out.

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
IDUsernameEmailArchivesUsageQuotaPlanVerified?Suspended?Join Date
IDUsernameEmailArchivesUsageQuotaPlanVerified?Suspended?Join Date
55 | 56 |
57 |
58 | 59 | <% include com/footer.html %> 60 | <% include com/stdjs.html %> 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /assets/html/admin-visits-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
EventURLUserSessionIPUser TypeBrowserVersionOSDate
EventURLUserSessionIPUser TypeBrowserVersionOSDate
-------------------------------------------------------------------------------- /assets/html/com/activity.html: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /assets/html/com/archive-list-item.ejs: -------------------------------------------------------------------------------- 1 | <% if (archive.owner) { %> 2 |
  • 3 | 4 | 5 | <%= archive.numPeers %>
    6 |
    peers
    7 |
    8 | 9 |
    10 | 25 | 26 | <% if (archive.manifest && archive.manifest.description) { %> 27 |

    28 | <%= archive.manifest.description %> 29 |

    30 | <% } %> 31 | 32 | 33 | <% var date = nicedate(archive.createdAt) %> 34 | <%= date %> 35 | <% if (date !== 'yesterday' && date !== 'just now') { %> 36 | ago 37 | <% } %> 38 | 39 | 40 | 49 |
    50 | 51 | 65 |
  • 66 | <% } %> -------------------------------------------------------------------------------- /assets/html/com/dashboard.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |
    5 |
    6 | <%= bytes(diskUsage)%> 7 | hosted 8 |
    9 | 10 |
    11 | <%= peerCount %> 12 | peers 13 |
    14 | 15 | 16 | <% if (sessionUser.archives) { %> 17 | <%= sessionUser.archives.length %> 18 | <% } else { %> 19 | 0 20 | <% } %> 21 | archives 22 | 23 |
    24 | 25 |
    26 |
    27 | 32 | 33 |
    34 | <% if (!sessionUser.archives || !sessionUser.archives.length) { %> 35 |

    36 | No archives 37 |

    38 | <% } %> 39 | 40 | 56 |
    57 |
    58 | 59 | <% if (sessionUser.archives && sessionUser.archives.length > 5) { %> 60 | 61 | <%= sessionUser.archives.length - 5 %> more archives... 62 | 63 | <% } %> 64 |
    65 |
    66 |
    -------------------------------------------------------------------------------- /assets/html/com/featured-content.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /assets/html/com/features.html: -------------------------------------------------------------------------------- 1 |

    Features

    2 | 3 |
    4 |
    5 |

    Reliable rehosting

    6 |

    7 | Hashbase acts as a "super peer" and rehosts your Dat archives, so your files 8 | are always available, even when you're offline. 9 |

    10 |
    11 | 12 |
    13 |

    Archive history backup

    14 |

    15 | Dat maintains a log of all the changes you make to your archive's 16 | files. In addition to storing your archive's files, we store a backup 17 | of the entire history log. 18 |

    19 |
    20 | 21 |
    22 |

    HTTPS mirroring

    23 |

    24 | In addition to rehosting your files on the Dat peer-to-peer network, we 25 | mirror the content on the Web so you can share it with anyone. 26 |

    27 |
    28 | 29 |
    30 |

    DNS shortnames

    31 |

    32 | Pick a custom shortname for each of your archives, like 33 | catsrcute.alice.hashbase.io. 34 |

    35 |
    36 |
    -------------------------------------------------------------------------------- /assets/html/com/footer-light.html: -------------------------------------------------------------------------------- 1 | 41 | -------------------------------------------------------------------------------- /assets/html/com/footer.html: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /assets/html/com/full-nav.html: -------------------------------------------------------------------------------- 1 | 36 |
    37 | Hashbase is shutting down. Learn more. 38 |
    -------------------------------------------------------------------------------- /assets/html/com/mobile-nav.html: -------------------------------------------------------------------------------- 1 | 11 | 48 | -------------------------------------------------------------------------------- /assets/html/com/nav.html: -------------------------------------------------------------------------------- 1 | <% include mobile-nav.html %> 2 | <% include full-nav.html %> 3 | <% include session-alerts.html %> -------------------------------------------------------------------------------- /assets/html/com/session-alerts.html: -------------------------------------------------------------------------------- 1 |
    2 | <% sessionAlerts.forEach(alert => { %> 3 | <%= alert.message %> <%= alert.details %> 4 | <% }) %> 5 |
    -------------------------------------------------------------------------------- /assets/html/com/stdhead.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/html/com/stdjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/html/com/tools-cta.html: -------------------------------------------------------------------------------- 1 |
    2 |

    Tools

    3 |
    4 |
    5 |

    6 | Beaker Browser 7 |

    8 |
    9 |
    10 | Browse and publish to the p2p Web with Beaker. 11 | 12 | 17 |
    18 |
    19 |
    20 |
    21 |

    22 | Dat Desktop and CLI 23 |

    24 |
    25 |
    26 | Sync files with the Dat CLI and desktop application. 27 | 32 |
    33 |
    34 |
    35 |
    36 |
    37 | -------------------------------------------------------------------------------- /assets/html/com/your-archives.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Your archives

    4 | 5 | <%= bytes(diskUsage) %>/<%= bytes(diskQuota) %> 6 | <% if (diskUsage > diskQuota) { %> 7 | 8 | <% } %> 9 | 10 |
    11 | 12 | <% if (!sessionUser.archives.length) { %> 13 |

    14 | No archives 15 |

    16 | <% } %> 17 | 18 | 32 | <% if (sessionUser.archives.length > 10) { %> 33 | 34 | View <%= sessionUser.archives.length - 10 %> more archives... 35 | 36 | <% } %> 37 |
    38 | -------------------------------------------------------------------------------- /assets/html/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= error.status || 500 %> - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | 9 | <%- include('com/nav.html', {navClass: ''}) %> 10 | 11 |
    12 |
    13 |

    <%= error.status || 500 %> - Error

    14 | 15 |

    Lost? Contact Support

    16 |
    17 |
    18 | 19 | <% include com/footer.html %> 20 | <% include com/stdjs.html %> 21 | 22 | 23 | -------------------------------------------------------------------------------- /assets/html/explore-activity.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Activity - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 | 10 |
    11 |
    12 |

    Recent activity

    13 | <% include com/activity.html %> 14 |
    15 |
    16 | 17 | <% include com/footer.html %> 18 | <% include com/stdjs.html %> 19 | 20 | -------------------------------------------------------------------------------- /assets/html/explore.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Explore - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 | 10 |
    11 |
    12 |

    Explore users

    13 |
    14 | <% users.forEach(u => { %> 15 | <%= u.username %> 16 | <% }) %> 17 |
    18 |
    19 |
    20 | 21 | <% include com/footer.html %> 22 | <% include com/stdjs.html %> 23 | 24 | -------------------------------------------------------------------------------- /assets/html/forgot-password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forgot Password - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 |
    10 | 11 |
    12 | 13 |

    Forgot your password?

    14 |

    15 | Enter your email and we'll send you instructions to reset your password. 16 |

    17 | 18 | 19 |

    20 | 21 | 22 | 23 |

    24 | 25 |
    26 | « Back to login 27 | 28 |
    29 |
    30 | 31 |

    32 | New? Create an account 33 |

    34 | 35 |
    36 | 37 | <% include com/footer-light.html %> 38 | <% include com/stdjs.html %> 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /assets/html/hero.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    Hosting for the
    peer-to-peer Web.

    5 |

    6 | Keep your files online, even when your computer is turned off. 7 |

    8 | 9 | Learn more 10 |
    11 | 12 | 13 | 14 | 15 |
    16 |
    -------------------------------------------------------------------------------- /assets/html/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Login - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 | 10 |
    11 |
    12 | 38 |
    39 |
    40 | 41 | <% include com/footer.html %> 42 | <% include com/stdjs.html %> 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /assets/html/pricing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pricing - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | 9 | <%- include('com/nav.html', {navClass: ''}) %> 10 | 11 |
    12 |
    13 | 14 | 38 | 39 | <% include com/features.html %> 40 |
    41 |
    42 | 43 | <% include com/footer.html %> 44 | <% include com/stdjs.html %> 45 | 46 | 47 | -------------------------------------------------------------------------------- /assets/html/register-pro.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Join Hashbase Pro - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 | 10 |
    11 |

    12 |

    13 | 14 | 15 | 16 |

    Set up a payment method

    17 | 18 | 19 |

    20 | 23 | 24 |

    25 | 26 |
    27 | 28 | 29 | 30 | 31 |
    32 |
    Service fee7.00
    33 |
    Sales tax (<%= salesTax %>%)+ 0.58
    34 |
    Total$7.58
    35 |
    36 | 37 |
    38 | 39 |
    40 | 43 |
    44 | 45 |
    46 | 47 | <% include com/footer.html %> 48 | <% include com/stdjs.html %> 49 | 50 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /assets/html/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Join - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 | 10 |
    11 |

    12 |

    13 | 14 | 15 |

    16 | Join <%= appInfo.brandname %> 17 | <% if (isProSignup) { %> 18 | Pro 19 | <% } %> 20 |

    21 | 22 | 23 |

    24 | 25 | 26 | 27 |

    28 | 29 |

    30 | 31 | 33 | 34 |

    35 | 36 |

    37 | 38 | 39 | 42 |

    43 | 44 |

    45 | 46 | 47 | 48 |

    49 | 50 |
    51 | <% if (isProSignup) { %> 52 | 53 | <% } else { %> 54 | 55 | <% } %> 56 |
    57 | 60 |
    61 | 62 |
    63 | 64 | <% include com/footer.html %> 65 | <% include com/stdjs.html %> 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /assets/html/registered.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Check your email - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 | 10 |
    11 |
    12 | 21 |
    22 |
    23 | 24 | <% include com/footer.html %> 25 | <% include com/stdjs.html %> 26 | 27 | 28 | -------------------------------------------------------------------------------- /assets/html/reset-password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reset Password - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | <%- include('com/nav.html', {navClass: ''}) %> 9 | 10 |
    11 |
    12 |
    13 | 14 |

    Change your password

    15 | 16 | 17 | 18 |

    19 | 20 | 21 | 22 |

    23 | 24 |
    25 | « Back to login 26 | 27 |
    28 |
    29 |
    30 |
    31 | 32 | <% include com/footer.html %> 33 | <% include com/stdjs.html %> 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /assets/html/support.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Support - <%= appInfo.brandname %> 5 | <% include com/stdhead.html %> 6 | 7 | 8 | 9 | <%- include('com/nav.html', {navClass: ''}) %> 10 | 11 |
    12 |
    13 |

    Support

    14 | 15 |
    16 | Contact us at support@beakerbrowser.com if your question is not answered here. 17 |
    18 | 19 |

    FAQ

    20 | 21 |

    What is a Dat archive?

    22 |

    23 | A Dat archive is a collection of files that can be efficiently shared 24 | with others on a peer-to-peer network. 25 |

    26 | 27 |

    How do I create a Dat archive?

    28 |

    29 | You can use the 30 | Beaker browser, the 31 | official Dat command line tool, or the 32 | official Dat desktop app. 33 |

    34 | 35 |

    36 | Can I self host Hashbase? 37 |

    38 |

    39 | Yes. The software that powers Hashbase is open source and easy to deploy. 40 |

    41 |

    42 | You may also find 43 | Homebase 44 | is a good choice. 45 |

    46 |
    47 |
    48 | 49 | <% include com/footer.html %> 50 | <% include com/stdjs.html %> 51 | 52 | 53 | -------------------------------------------------------------------------------- /assets/images/apps/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/apps/editor.png -------------------------------------------------------------------------------- /assets/images/apps/nexus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fritter copy 2 5 | Created with Sketch. 6 | 7 | 8 | @ 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/images/apps/photo-album.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | photo-album 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/images/apps/rss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/apps/rss.png -------------------------------------------------------------------------------- /assets/images/cc-americanexpress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/cc-americanexpress.png -------------------------------------------------------------------------------- /assets/images/cc-dinersclub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/cc-dinersclub.png -------------------------------------------------------------------------------- /assets/images/cc-discover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/cc-discover.png -------------------------------------------------------------------------------- /assets/images/cc-jcb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/cc-jcb.png -------------------------------------------------------------------------------- /assets/images/cc-mastercard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/cc-mastercard.png -------------------------------------------------------------------------------- /assets/images/cc-visa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/cc-visa.png -------------------------------------------------------------------------------- /assets/images/demo-screencap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/demo-screencap.jpg -------------------------------------------------------------------------------- /assets/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/favicon-16x16.png -------------------------------------------------------------------------------- /assets/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/favicon-32x32.png -------------------------------------------------------------------------------- /assets/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/favicon-96x96.png -------------------------------------------------------------------------------- /assets/images/logo-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/logo-blue.png -------------------------------------------------------------------------------- /assets/images/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/logo-white.png -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/paul.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/paul.jpg -------------------------------------------------------------------------------- /assets/images/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/sort_asc.png -------------------------------------------------------------------------------- /assets/images/sort_asc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/sort_asc_disabled.png -------------------------------------------------------------------------------- /assets/images/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/sort_both.png -------------------------------------------------------------------------------- /assets/images/sort_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/sort_desc.png -------------------------------------------------------------------------------- /assets/images/sort_desc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/sort_desc_disabled.png -------------------------------------------------------------------------------- /assets/images/tara.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/hashbase/6398dfcd5b23ec1d20c0d60641cffe270584b4c4/assets/images/tara.jpg -------------------------------------------------------------------------------- /assets/js/account-cancel-plan.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | // account cancel plan page js 4 | $(function () { 5 | // form submit 6 | $('.form-account-cancel-plan').on('submit', function (e) { 7 | e.preventDefault() 8 | 9 | toggleSpinner(true) 10 | 11 | // post to api 12 | var xhr = $.post('/v2/accounts/account/cancel-plan', { 13 | _csrf: $('[name=_csrf]').val() 14 | }) 15 | xhr.done(function (res) { 16 | // success, redirect 17 | window.location = '/account/canceled-plan' 18 | }) 19 | xhr.fail(function (res) { 20 | // failure, render errors 21 | toggleSpinner(false) 22 | try { 23 | var resObj = JSON.parse(res.responseText) 24 | } catch (e) {} 25 | $('#errors').text((resObj && resObj.message) || 'Internal server error. Please contact support.') 26 | }) 27 | }) 28 | 29 | function toggleSpinner (on) { 30 | if (on) { 31 | $('#submit-btn').attr('disabled', 'disabled').html('Processing...') 32 | } else { 33 | $('#submit-btn').attr('disabled', null).html('Yes, cancel it') 34 | } 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /assets/js/account-change-password.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | // account change password page js 4 | $(function () { 5 | $('.form-account-change-password').on('submit', function (e) { 6 | e.preventDefault() 7 | 8 | // serialize form values 9 | var values = {} 10 | $(this).serializeArray().forEach(function (value) { 11 | values[value.name] = value.value 12 | }) 13 | 14 | // post to api 15 | var xhr = $.post('/v2/accounts/account/password', values) 16 | xhr.done(function (res) { 17 | // success, redirect to account page 18 | window.location = '/account?updated=1' 19 | }) 20 | xhr.fail(function (res) { 21 | // failure, render errors 22 | try { 23 | renderErrors(JSON.parse(res.responseText)) 24 | } catch (e) { 25 | renderErrors(res.responseText) 26 | } 27 | }) 28 | }) 29 | 30 | function renderErrors (json) { 31 | // general error 32 | $('#error-general').text(json.message || json) 33 | 34 | // individual form errors 35 | var details = json.details || {} 36 | ;(['oldPassword', 'newPassword']).forEach(function (name) { 37 | if (details[name]) { 38 | $('#error-' + name) 39 | .text(details[name].msg) 40 | .parent() 41 | .addClass('warning') 42 | } else { 43 | $('#error-' + name) 44 | .text('') 45 | .parent() 46 | .removeClass('warning') 47 | } 48 | }) 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /assets/js/account-update-email.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | // update email page js 4 | $(function () { 5 | $('.form-update-email').on('submit', function (e) { 6 | e.preventDefault() 7 | 8 | // serialize form values 9 | var values = {} 10 | $(this).serializeArray().forEach(function (value) { 11 | values[value.name] = value.value 12 | }) 13 | 14 | // post to api 15 | var xhr = $.post('/v2/accounts/account/email', values) 16 | xhr.done(function (res) { 17 | $('#success-msg').text('Click the verification link sent to ' + values.newEmail + ' to finish updating your account.') 18 | $('#error-general').text('') 19 | $('.form-desc').text('') 20 | $('input').val('') 21 | }) 22 | xhr.fail(function (res) { 23 | // failure, render errors 24 | try { 25 | renderErrors(JSON.parse(res.responseText)) 26 | } catch (e) { 27 | renderErrors(res.responseText) 28 | } 29 | }) 30 | }) 31 | 32 | function renderErrors (json) { 33 | // general error 34 | $('#error-general').text(json.message) 35 | 36 | // individual form errors 37 | var details = json.details || {} 38 | ;(['newEmail', 'password']).forEach(function (name) { 39 | if (details[name]) { 40 | $('#error-' + name) 41 | .text(details[name].msg) 42 | .parent() 43 | .addClass('warning') 44 | } else { 45 | $('#error-' + name) 46 | .text('') 47 | .parent() 48 | .removeClass('warning') 49 | } 50 | }) 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /assets/js/account-upgrade.js: -------------------------------------------------------------------------------- 1 | /* global $ Stripe */ 2 | 3 | // account upgrade page js 4 | $(function () { 5 | // create stripe elements 6 | var stripe = Stripe(window.params.stripePK) 7 | var elements = stripe.elements() 8 | var card = elements.create('card', {style: { 9 | base: { 10 | color: '#32325d', 11 | lineHeight: '24px', 12 | fontFamily: 'Helvetica Neue', 13 | fontSize: '16px', 14 | '::placeholder': { 15 | color: '#aab7c4' 16 | } 17 | }, 18 | invalid: { 19 | color: '#fa755a', 20 | iconColor: '#fa755a' 21 | } 22 | }}) 23 | card.mount('#card-element') 24 | 25 | // render errors 26 | card.addEventListener('change', function (e) { 27 | $('#card-errors').text(e.error ? e.error.message : '') 28 | $('#submit-btn').attr('disabled', !e.complete ? 'disabled' : null) 29 | }) 30 | 31 | // form submit 32 | $('#form-account-upgrade').on('submit', function (e) { 33 | e.preventDefault() 34 | 35 | toggleSpinner(true) 36 | stripe.createToken(card).then(function (result) { 37 | if (result.error) { 38 | toggleSpinner(false) 39 | console.error('Error', result) 40 | $('#error-general').text(result.error.message) 41 | return 42 | } 43 | 44 | // post to api 45 | var token = result.token 46 | var xhr = $.post('/v2/accounts/account/upgrade', { 47 | _csrf: $('#form-account-upgrade [name=_csrf]').val(), 48 | token: token 49 | }) 50 | xhr.done(function (res) { 51 | // success, redirect 52 | window.location = '/account/upgraded' 53 | }) 54 | xhr.fail(function (res) { 55 | // failure, render errors 56 | toggleSpinner(false) 57 | try { 58 | var resObj = JSON.parse(res.responseText) 59 | } catch (e) {} 60 | console.error('Error', res) 61 | $('#error-general').text((resObj && resObj.message) || 'Internal server error. Please contact support.') 62 | }) 63 | }) 64 | }) 65 | 66 | function toggleSpinner (on) { 67 | if (on) { 68 | $('#submit-btn').attr('disabled', 'disabled').html('Processing...') 69 | } else { 70 | $('#submit-btn').attr('disabled', null).html('Upgrade') 71 | } 72 | } 73 | }) 74 | -------------------------------------------------------------------------------- /assets/js/account.js: -------------------------------------------------------------------------------- 1 | /* global $ Stripe */ 2 | 3 | // account upgrade page js 4 | $(function () { 5 | var updateCardForm = $('#form-card-update') 6 | 7 | // create stripe elements 8 | var stripe = Stripe(window.params.stripePK) 9 | var elements = stripe.elements() 10 | var card = elements.create('card', {style: { 11 | base: { 12 | color: '#32325d', 13 | lineHeight: '24px', 14 | fontFamily: 'Helvetica Neue', 15 | fontSize: '16px', 16 | '::placeholder': { 17 | color: '#aab7c4' 18 | } 19 | }, 20 | invalid: { 21 | color: '#fa755a', 22 | iconColor: '#fa755a' 23 | } 24 | }}) 25 | card.mount('#card-element') 26 | 27 | // show form 28 | $('#show-update-card-form').click(function () { 29 | updateCardForm.addClass('open') 30 | }) 31 | 32 | // render errors 33 | card.addEventListener('change', function (e) { 34 | $('#card-errors').text(e.error ? e.error.message : '') 35 | $('#submit-btn').attr('disabled', !e.complete ? 'disabled' : null) 36 | }) 37 | 38 | // form submit 39 | updateCardForm.on('submit', function (e) { 40 | e.preventDefault() 41 | 42 | toggleSpinner(true) 43 | stripe.createToken(card).then(function (result) { 44 | if (result.error) { 45 | toggleSpinner(false) 46 | $('#card-errors').text(result.error.message) 47 | return 48 | } 49 | 50 | // post to api 51 | var token = result.token 52 | var last4 = token.card.last4 53 | var cardImagePath = '/assets/images/cc-' + token.card.brand.toLowerCase().replace(' ', '') + '.png' 54 | 55 | var xhr = $.post('/v2/accounts/account/update-card', { 56 | _csrf: updateCardForm.find('[name=_csrf]').val(), 57 | token: token 58 | }) 59 | xhr.done(function (res) { 60 | $('#billing-alert-success').text('Your payment information has been updated') 61 | $('#last-4').text(last4) 62 | $('#card-brand').attr('src', cardImagePath) 63 | toggleSpinner(false) 64 | updateCardForm.removeClass('open') 65 | }) 66 | xhr.fail(function (res) { 67 | // failure, render errors 68 | toggleSpinner(false) 69 | try { 70 | var resObj = JSON.parse(res.responseText) 71 | } catch (e) {} 72 | console.error('Error', res) 73 | $('#card-errors').text((resObj && resObj.message) || 'Internal server error. Please contact support.') 74 | }) 75 | }) 76 | }) 77 | 78 | function toggleSpinner (on) { 79 | if (on) { 80 | $('#submit-btn').attr('disabled', 'disabled').html('') 81 | } else { 82 | $('#submit-btn').attr('disabled', null).html(' Upgrade') 83 | } 84 | } 85 | }) 86 | -------------------------------------------------------------------------------- /assets/js/admin-dashboard-archives.js: -------------------------------------------------------------------------------- 1 | /* global $ makeSafe moment */ 2 | 3 | // admin archive tools 4 | $(function () { 5 | setupArchivesTable() 6 | }) 7 | 8 | function setupArchivesTable () { 9 | var table = $('.archives-table') 10 | table = table.DataTable({ 11 | order: [[ 5, 'desc' ]], 12 | pageLength: 50, 13 | ajax: { 14 | url: '/v2/admin/archives?view=dashboard', 15 | headers: {accept: 'application/json'}, 16 | data: {}, 17 | dataSrc: 'archives' 18 | }, 19 | columns: [ 20 | {data: colKey('key')}, 21 | {data: colValue('name')}, 22 | {data: colValue('owner')}, 23 | {data: colValue('diskUsage'), type: 'file-size'}, 24 | {data: colValue('totalSize'), type: 'file-size'}, 25 | {data: colDate('createdAt')} 26 | ] 27 | }) 28 | table.on('click', 'tr', function () { 29 | window.open('/v2/admin/archives/' + table.row($(this)).data().key) 30 | }) 31 | } 32 | 33 | // helpers to construct the data 34 | function colKey (col) { 35 | return row => { 36 | var v = '' + row[col] 37 | return makeSafe(v.slice(0, 6) + '..' + v.slice(-2)) 38 | } 39 | } 40 | function colValue (col) { 41 | return row => { 42 | var v = row[col] 43 | if (v || v === 0) { 44 | return makeSafe(v.toString()) 45 | } 46 | return `(${makeSafe('' + v)})` 47 | } 48 | } 49 | function colDate (col) { 50 | return row => moment(row[col]).format('YYYY/MM/DD') 51 | } 52 | -------------------------------------------------------------------------------- /assets/js/admin-dashboard-report.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | // admin user tools 4 | $(function () { 5 | // auto-size the record content 6 | var textarea = $('textarea') 7 | textarea.height(textarea[0].scrollHeight) 8 | 9 | // save 10 | $('#save-btn').on('click', function () { 11 | try { 12 | var data = JSON.parse(textarea.val()) 13 | } catch (e) { 14 | return onError({responseText: e.toString()}, 0, 'Error parsing JSON') 15 | } 16 | $('#error-general').text('') 17 | $.ajax(window.location.pathname, {method: 'post', contentType: 'application/json; charset=utf-8', dataType: 'json', data: JSON.stringify(data)}) 18 | .done(onUpdate) 19 | .fail(onError) 20 | }) 21 | 22 | // close 23 | $('#close-btn').on('click', function () { 24 | $('#error-general').text('') 25 | $.ajax(window.location.pathname + '/close', {method: 'post', contentType: 'application/json; charset=utf-8', data: JSON.stringify({})}) 26 | .done(onUpdate) 27 | .fail(onError) 28 | }) 29 | 30 | // open 31 | $('#open-btn').on('click', function () { 32 | $('#error-general').text('') 33 | $.ajax(window.location.pathname + '/open', {method: 'post', contentType: 'application/json; charset=utf-8', data: JSON.stringify({})}) 34 | .done(onUpdate) 35 | .fail(onError) 36 | }) 37 | }) 38 | 39 | function onUpdate () { 40 | window.location.reload() 41 | } 42 | 43 | function onError (jqXHR, _, err) { 44 | $('#error-general').text(err + ' ' + jqXHR.responseText) 45 | } 46 | -------------------------------------------------------------------------------- /assets/js/admin-dashboard-reports.js: -------------------------------------------------------------------------------- 1 | /* global $ makeSafe moment */ 2 | 3 | // admin user tools 4 | $(function () { 5 | setupReportsTable() 6 | }) 7 | 8 | function setupReportsTable () { 9 | var table = $('.reports-table') 10 | table = table.DataTable({ 11 | order: [[ 1, 'desc' ]], 12 | pageLength: 50, 13 | ajax: { 14 | url: '/v2/admin/reports?view=dashboard', 15 | headers: {accept: 'application/json'}, 16 | data: {}, 17 | dataSrc: 'reports' 18 | }, 19 | columns: [ 20 | {data: colValue('archiveKey')}, 21 | {data: colValue('archiveOwner')}, 22 | {data: colValue('reportingUser')}, 23 | {data: colValue('reason')}, 24 | {data: colDate('createdAt')}, 25 | {data: colValue('notes')}, 26 | {data: colValue('status')} 27 | ] 28 | }) 29 | table.on('click', 'tr', function () { 30 | window.location = '/v2/admin/reports/' + table.row($(this)).data().id 31 | }) 32 | } 33 | 34 | function colValue (col) { 35 | return row => { 36 | var v = row[col] 37 | if (v || v === 0) { 38 | return makeSafe(v.toString()) 39 | } 40 | return `(${makeSafe('' + v)})` 41 | } 42 | } 43 | 44 | function colDate (col) { 45 | return row => moment(row[col]).format('YYYY/MM/DD') 46 | } 47 | -------------------------------------------------------------------------------- /assets/js/admin-dashboard-user.js: -------------------------------------------------------------------------------- 1 | /* global $ window */ 2 | 3 | // admin user tools 4 | $(function () { 5 | // auto-size the record content 6 | var textarea = $('.record-content textarea') 7 | textarea.height(textarea[0].scrollHeight) 8 | 9 | // save 10 | $('#save-btn').on('click', function () { 11 | try { 12 | var data = JSON.parse(textarea.val()) 13 | } catch (e) { 14 | return onError({responseText: e.toString()}, 0, 'Error parsing JSON') 15 | } 16 | 17 | $('#error-general').text('') 18 | $.ajax(window.location.pathname, {method: 'post', contentType: 'application/json; charset=utf-8', dataType: 'json', data: JSON.stringify(data)}) 19 | .done(onUpdate) 20 | .fail(onError) 21 | }) 22 | 23 | // suspend 24 | $('#suspend-btn').on('click', function () { 25 | var data = {reason: window.prompt('Reason?')} 26 | if (!data.reason) return 27 | data = JSON.stringify(data) 28 | $('#error-general').text('') 29 | $.ajax(window.location.pathname + '/suspend', {method: 'post', contentType: 'application/json; charset=utf-8', data}) 30 | .done(onUpdate) 31 | .fail(onError) 32 | }) 33 | 34 | // unsuspend 35 | $('#unsuspend-btn').on('click', function () { 36 | if (!window.confirm('Unsuspend?')) return 37 | $('#error-general').text('') 38 | $.ajax(window.location.pathname + '/unsuspend', {method: 'post', contentType: 'application/json; charset=utf-8', data: JSON.stringify({})}) 39 | .done(onUpdate) 40 | .fail(onError) 41 | }) 42 | 43 | // resend confirmation email 44 | $('#resend-email-confirmation-btn').on('click', function () { 45 | if (!window.confirm('Resend confirmation email?')) return 46 | $('#error-general').text('') 47 | $.ajax(window.location.pathname + '/resend-email-confirmation', {method: 'post', contentType: 'application/json; charset=utf-8', data: JSON.stringify({})}) 48 | .done(onUpdate) 49 | .fail(onError) 50 | }) 51 | }) 52 | 53 | function onUpdate () { 54 | window.location.reload() 55 | } 56 | 57 | function onError (jqXHR, _, err) { 58 | $('#error-general').text(err + ' ' + jqXHR.responseText) 59 | } 60 | -------------------------------------------------------------------------------- /assets/js/admin-dashboard-users.js: -------------------------------------------------------------------------------- 1 | /* global $ makeSafe moment */ 2 | 3 | // admin user tools 4 | $(function () { 5 | setupUsersTable() 6 | }) 7 | 8 | function setupUsersTable () { 9 | var table = $('.users-table') 10 | table = table.DataTable({ 11 | order: [[ 9, 'desc' ]], 12 | pageLength: 50, 13 | ajax: { 14 | url: '/v2/admin/users?view=dashboard', 15 | headers: {accept: 'application/json'}, 16 | data: {}, 17 | dataSrc: 'users' 18 | }, 19 | columns: [ 20 | {data: colValue('id')}, 21 | {data: colValue('username')}, 22 | {data: colValue('email')}, 23 | {data: colValue('numArchives')}, 24 | {data: colValue('diskUsage'), type: 'file-size'}, 25 | {data: colValue('diskQuota'), type: 'file-size'}, 26 | {data: colValue('plan')}, 27 | {data: colBool('isEmailVerified')}, 28 | {data: suspension}, 29 | {data: colDate('createdAt')} 30 | ] 31 | }) 32 | table.on('click', 'tr', function () { 33 | window.open('/v2/admin/users/' + table.row($(this)).data().id) 34 | }) 35 | } 36 | 37 | // helpers to construct the data 38 | function colValue (col) { 39 | return row => { 40 | var v = row[col] 41 | if (v || v === 0) { 42 | return makeSafe(v.toString()) 43 | } 44 | return `(${makeSafe('' + v)})` 45 | } 46 | } 47 | function colBool (col) { 48 | return row => `` 49 | } 50 | function suspension (row) { 51 | return row.suspension ? `SUSPENDED` : '' 52 | } 53 | function colDate (col) { 54 | return row => moment(row[col]).format('YYYY/MM/DD') 55 | } 56 | -------------------------------------------------------------------------------- /assets/js/admin-visits-list.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | // admin tools on archive 4 | $(function () { 5 | setupVisitorsTable() 6 | }) 7 | 8 | function setupVisitorsTable () { 9 | $('.visits-table').DataTable({ 10 | ajax: { 11 | url: '/v2/admin/analytics/visits-count', 12 | data: {groupBy: 'url', unique: '1'}, 13 | dataSrc: '' 14 | }, 15 | columns: [ 16 | {data: 'event'}, 17 | {data: 'url'}, 18 | {data: 'session'}, 19 | {data: 'session'}, 20 | {data: 'ip'}, 21 | {data: 'ip'}, 22 | {data: 'browser'}, 23 | {data: 'version'}, 24 | {data: 'os'}, 25 | {data: 'date'} 26 | ] 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /assets/js/archive-admin.js: -------------------------------------------------------------------------------- 1 | /* global $ params */ 2 | 3 | // admin tools on archive 4 | $(function () { 5 | $('#admin-remove-archive').on('click', function () { 6 | if (window.confirm('Remove this archive?')) { 7 | $.post('/v2/admin/archives/' + params.key + '/remove', {key: params.key}, function (response, status) { 8 | if (status !== 'success') { 9 | console.error(status, response) 10 | } 11 | window.location = '/' + params.owner 12 | }) 13 | } else { 14 | 15 | } 16 | }) 17 | 18 | $('#admin-toggle-featured').click(function () { 19 | var act = params.isFeatured ? 'unfeature' : 'feature' 20 | $.post('/v2/admin/archives/' + params.key + '/' + act, {}, function (response, status) { 21 | if (status !== 'success') { 22 | return console.error(status, response) 23 | } 24 | window.location.reload() 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /assets/js/archive.js: -------------------------------------------------------------------------------- 1 | /* global $ params */ 2 | 3 | // archive page js 4 | $(function () { 5 | var renameForm = $('#rename-form') 6 | var renameFormInput = $('#rename-form #input-name') 7 | var removeForm = $('#remove-archive-form') 8 | var urlBtns = $('.link-btns .label') 9 | 10 | urlBtns.forEach(function (el) { 11 | el.onclick = updateActiveURL 12 | }) 13 | 14 | $('#show-remove-archive-form').on('click', function () { 15 | removeForm.addClass('open') 16 | }) 17 | 18 | $('#cancel-remove-archive').on('click', function (e) { 19 | e.preventDefault() 20 | removeForm.removeClass('open') 21 | }) 22 | 23 | renameFormInput.on('keyup', function (e) { 24 | var name = renameFormInput.val() 25 | var isChanged = name !== params.name 26 | var url = 'dat://' + (name ? (name + '.' + params.hostname) : params.key) 27 | $('#feedback-name .is-will-be').text(isChanged ? 'will be' : 'is') 28 | $('#feedback-name .link').attr('href', url) 29 | $('#feedback-name .link').text(name ? url : (url.slice(0, 12) + '..' + url.slice(-2))) 30 | if (isChanged) $('#rename-form .btn').removeAttr('disabled') 31 | else $('#rename-form .btn').attr('disabled', true) 32 | }) 33 | 34 | renameForm.on('submit', function (e) { 35 | e.preventDefault() 36 | 37 | // serialize form values 38 | var values = {} 39 | $(this).serializeArray().forEach(function (value) { 40 | values[value.name] = value.value 41 | }) 42 | if (!values.name) { 43 | delete values.name 44 | } 45 | 46 | var xhr = $.post('/v2/archives/item/' + params.key, values) 47 | xhr.done(function (res) { 48 | // success, redirect 49 | window.location = '/' + params.owner + '/' + (values.name || params.key) 50 | }) 51 | 52 | xhr.fail(function (res) { 53 | // failure, render errors 54 | try { 55 | renderErrors(JSON.parse(res.responseText)) 56 | } catch (e) { 57 | renderErrors(res.responseText) 58 | } 59 | }) 60 | }) 61 | 62 | removeForm.on('submit', function (e) { 63 | e.preventDefault() 64 | 65 | // serialize form values 66 | var values = {} 67 | $(this).serializeArray().forEach(function (value) { 68 | values[value.name] = value.value 69 | }) 70 | 71 | var xhr = $.post('/v2/archives/remove', values) 72 | xhr.done(function (res) { 73 | // success, redirect 74 | window.location = '/profile' 75 | }) 76 | 77 | xhr.fail(function (res) { 78 | // failure, render errors 79 | try { 80 | renderErrors(JSON.parse(res.responseText)) 81 | } catch (e) { 82 | renderErrors(res.responseText) 83 | } 84 | }) 85 | }) 86 | 87 | function updateActiveURL (e) { 88 | urlBtns.forEach(function (el) { 89 | el.classList.remove('selected') 90 | }) 91 | 92 | e.target.classList.add('selected') 93 | } 94 | 95 | function renderErrors (json) { 96 | // general error 97 | $('#error-general').text(json.message || json) 98 | } 99 | }) 100 | -------------------------------------------------------------------------------- /assets/js/clipboard.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | // clipboard js 4 | $(function () { 5 | var copyButton = $('.copy-to-clipboard') 6 | 7 | copyButton.click(function (e) { 8 | e.preventDefault() 9 | e.stopPropagation() 10 | 11 | // create a hidden input 12 | var input = document.createElement('textarea') 13 | document.body.appendChild(input) 14 | 15 | // get the text to select from the target element 16 | var targetEl = document.querySelector(this.dataset.target) 17 | 18 | // set the input's value and select the text 19 | input.value = targetEl.tagName === 'A' ? targetEl.getAttribute('href') : targetEl.innerText 20 | input.select() 21 | 22 | // input.style.position = 'relative' 23 | 24 | // copy 25 | document.execCommand('copy') 26 | document.body.removeChild(input) 27 | 28 | // show feedback 29 | var feedbackEl = document.querySelector(this.dataset.feedbackEl) 30 | feedbackEl.classList.add('tooltip') 31 | feedbackEl.innerText = 'Copied to clipboard' 32 | 33 | setTimeout(function () { 34 | feedbackEl.innerText = '' 35 | feedbackEl.classList.remove('tooltip') 36 | }, 1500) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /assets/js/forgot-password.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | // forgot password page js 4 | $(function () { 5 | $('.form-forgot-password').on('submit', function (e) { 6 | e.preventDefault() 7 | 8 | // serialize form values 9 | var values = {} 10 | $(this).serializeArray().forEach(function (value) { 11 | values[value.name] = value.value 12 | }) 13 | 14 | // post to api 15 | var xhr = $.post('/v2/accounts/forgot-password', values) 16 | xhr.done(function (res) { 17 | // success, tell user 18 | $('#success-msg').text('Check your email inbox for a reset link. Didn’t get one? Check that you entered your email address correctly.') 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /assets/js/frontpage.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | // tabbed archives list js 4 | $(function () { 5 | [].forEach.call(document.querySelectorAll('img[data-src]'), function (img) { 6 | img.setAttribute('src', img.getAttribute('data-src')) 7 | img.onload = function () { 8 | img.removeAttribute('data-src') 9 | } 10 | }) 11 | var viewButtons = $('.archives-view-link') 12 | var views = $('.archives-view') 13 | 14 | $('#dismiss-get-started-btn').click(function (e) { 15 | $('#get-started-container')[0].style.display = 'none' 16 | }) 17 | 18 | viewButtons.click(function (e) { 19 | viewButtons.removeClass('active') 20 | views.removeClass('active') 21 | 22 | $(e.target).addClass('active') 23 | $(e.target.dataset.view).addClass('active') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /assets/js/jquery.dataTables.file-size-sorting-plugin.js: -------------------------------------------------------------------------------- 1 | /* globals jQuery */ 2 | 3 | /** 4 | * When dealing with computer file sizes, it is common to append a post fix 5 | * such as B, KB, MB or GB to a string in order to easily denote the order of 6 | * magnitude of the file size. This plug-in allows sorting to take these 7 | * indicates of size into account. 8 | * 9 | * A counterpart type detection plug-in is also available. 10 | * 11 | * @name File size 12 | * @summary Sort abbreviated file sizes correctly (8MB, 4KB, etc) 13 | * @author Allan Jardine - datatables.net 14 | * 15 | * @example 16 | * $('#example').DataTable( { 17 | * columnDefs: [ 18 | * { type: 'file-size', targets: 0 } 19 | * ] 20 | * } ); 21 | */ 22 | 23 | jQuery.fn.dataTable.ext.type.order['file-size-pre'] = function (data) { 24 | var matches = data.match(/^(\d+(?:\.\d+)?)\s*([a-z]+)/i) 25 | var multipliers = { 26 | b: 1, 27 | bytes: 1, 28 | kb: 1000, 29 | kib: 1024, 30 | mb: 1000000, 31 | mib: 1048576, 32 | gb: 1000000000, 33 | gib: 1073741824, 34 | tb: 1000000000000, 35 | tib: 1099511627776, 36 | pb: 1000000000000000, 37 | pib: 1125899906842624 38 | } 39 | 40 | if (matches) { 41 | var multiplier = multipliers[matches[2].toLowerCase()] 42 | return parseFloat(matches[1]) * multiplier 43 | } 44 | return -1 45 | } 46 | -------------------------------------------------------------------------------- /assets/js/login.js: -------------------------------------------------------------------------------- 1 | /* global $ URLSearchParams location */ 2 | 3 | // login page js 4 | $(function () { 5 | var redirect 6 | if (window.URLSearchParams) { 7 | var queryParams = new URLSearchParams(location.search) 8 | redirect = queryParams.get('redirect') || '' 9 | } else { // This is needed for older browsers (MS Edge on or before december 2017) that do not support URLSearchParams 10 | var getQueryVariable = function (variable) { 11 | var query = window.location.search.substring(1) 12 | var vars = query.split('&') 13 | for (var i = 0; i < vars.length; i++) { 14 | var pair = vars[i].split('=') 15 | if (decodeURIComponent(pair[0]) === variable) { 16 | return decodeURIComponent(pair[1]) 17 | } 18 | } 19 | } 20 | redirect = getQueryVariable('redirect') || '' 21 | } 22 | 23 | $('.form-login').on('submit', function (e) { 24 | e.preventDefault() 25 | 26 | // serialize form values 27 | var values = {} 28 | $(this).serializeArray().forEach(function (value) { 29 | values[value.name] = value.value 30 | }) 31 | 32 | // post to api 33 | var xhr = $.post('/v2/accounts/login', values) 34 | xhr.done(function (res) { 35 | // success, redirect 36 | window.location = '/' + redirect 37 | }) 38 | xhr.fail(function (res) { 39 | // failure, render errors 40 | try { 41 | renderErrors(JSON.parse(res.responseText)) 42 | } catch (e) { 43 | renderErrors(res.responseText) 44 | } 45 | }) 46 | }) 47 | 48 | function renderErrors (json) { 49 | // general error 50 | $('#error-general').text(json.message || json) 51 | 52 | // individual form errors 53 | var details = json.details || {} 54 | ;(['username', 'password']).forEach(function (name) { 55 | if (details[name]) { 56 | $('#error-' + name) 57 | .text(details[name].msg) 58 | .parent() 59 | .addClass('warning') 60 | } else { 61 | $('#error-' + name) 62 | .text('') 63 | .parent() 64 | .removeClass('warning') 65 | } 66 | }) 67 | } 68 | }) 69 | -------------------------------------------------------------------------------- /assets/js/nav.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | // nav js 4 | $(function () { 5 | var dropdownMenu = $('.nav .dropdown-menu') 6 | var dropdownMenuToggle = $('.nav .dropdown-menu-link') 7 | var mobileNav = $('.mobile-nav') 8 | var mobileNavToggle = $('.mobile-nav-toggle') 9 | 10 | function toggleMenu () { 11 | dropdownMenu.toggleClass('open') 12 | 13 | if (dropdownMenu.hasClass('open')) { 14 | $(document.body).on('click', toggleMenu) 15 | } else { 16 | $(document.body).off('click', toggleMenu) 17 | } 18 | } 19 | 20 | dropdownMenuToggle.click(function (e) { 21 | e.stopPropagation() 22 | toggleMenu() 23 | }) 24 | 25 | mobileNavToggle.click(function () { 26 | mobileNav.toggleClass('open') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /assets/js/register.js: -------------------------------------------------------------------------------- 1 | /* global $ location */ 2 | 3 | // register page js 4 | $(function () { 5 | // autofill email if email query paramater set 6 | if (location.search && location.search.substring(1).split('=').indexOf('email') !== -1) { 7 | var email = location.search.substring(1).split('=')[1] 8 | $('input[name="email"]').val(decodeURIComponent(email)) 9 | } 10 | 11 | $('#input-password-confirm').on('blur', function (e) { 12 | if (e.target.value !== $('#input-password')[0].value) { 13 | $('#error-password-confirm') 14 | .text('Passwords don\'t match') 15 | .parent() 16 | .addClass('warning') 17 | } else { 18 | $('#error-password-confirm') 19 | .text('') 20 | .parent() 21 | .removeClass('warning') 22 | } 23 | }) 24 | 25 | $('#register').on('submit', function (e) { 26 | e.preventDefault() 27 | 28 | // serialize form values 29 | var values = {} 30 | $(this).serializeArray().forEach(function (value) { 31 | values[value.name] = value.value 32 | }) 33 | 34 | // post to api 35 | var xhr = $.post('/v2/accounts/register', values) 36 | xhr.done(function (res) { 37 | // success, redirect 38 | if (location.search && location.search.substring(1).split('=').indexOf('pro') !== -1) { 39 | window.location = '/register/pro?id=' + res.id + '&email=' + escape(values.email) 40 | } else { 41 | window.location = '/registered?email=' + escape(values.email) 42 | } 43 | }) 44 | xhr.fail(function (res) { 45 | // failure, render errors 46 | try { 47 | renderErrors(JSON.parse(res.responseText)) 48 | } catch (e) { 49 | renderErrors({message: res.responseText}) 50 | } 51 | }) 52 | }) 53 | 54 | function renderErrors (json) { 55 | // general error 56 | $('#error-general').text(json.message || json) 57 | 58 | // individual form errors 59 | var details = json.details || {} 60 | ;(['username', 'email', 'password']).forEach(function (name) { 61 | if (details[name]) { 62 | $('#error-' + name) 63 | .text(details[name].msg) 64 | .parent() 65 | .addClass('warning') 66 | } else { 67 | $('#error-' + name) 68 | .text('') 69 | .parent() 70 | .removeClass('warning') 71 | } 72 | }) 73 | } 74 | }) 75 | -------------------------------------------------------------------------------- /assets/js/report.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | // report form js 4 | $(function () { 5 | $('#show-report-archive-form').on('click', showReportArchiveForm) 6 | $('#report-archive-form').on('submit', submitReport) 7 | 8 | $('#cancel-report-btn').on('click', hideForm) 9 | 10 | function hideForm () { 11 | $('.modal-form-container').removeClass('visible') 12 | } 13 | 14 | function showReportArchiveForm () { 15 | $('.modal-form-container').addClass('visible') 16 | } 17 | 18 | function submitReport (e) { 19 | e.preventDefault() 20 | 21 | // serialize form values 22 | var values = {} 23 | $(this).serializeArray().forEach(function (value) { 24 | values[value.name] = value.value 25 | }) 26 | 27 | var xhr = $.post('/v2/reports/add', values) 28 | xhr.done(function (res) { 29 | hideForm() 30 | $('#feedback-general').text('Thanks, your report has been sent to the Hashbase admins') 31 | }) 32 | 33 | xhr.fail(function (res) { 34 | // failure, render errors 35 | try { 36 | renderErrors(JSON.parse(res.responseText)) 37 | } catch (e) { 38 | renderErrors(res.responseText) 39 | } 40 | }) 41 | } 42 | 43 | function renderErrors (json) { 44 | // general error 45 | $('form #error-general').text(json.message || json) 46 | } 47 | }) 48 | -------------------------------------------------------------------------------- /assets/js/reset-password.js: -------------------------------------------------------------------------------- 1 | /* global $ URL */ 2 | 3 | // reset password page js 4 | $(function () { 5 | $('.form-reset-password').on('submit', function (e) { 6 | e.preventDefault() 7 | 8 | // serialize form values 9 | var values = {} 10 | $(this).serializeArray().forEach(function (value) { 11 | values[value.name] = value.value 12 | }) 13 | 14 | // pull username and nonce from the url 15 | var url = new URL(window.location) 16 | values.username = url.searchParams.get('username') 17 | values.nonce = url.searchParams.get('nonce') 18 | 19 | // post to api 20 | var xhr = $.post('/v2/accounts/account/password', values) 21 | xhr.done(function (res) { 22 | // success, redirect to login 23 | window.location = '/login?reset=1' 24 | }) 25 | xhr.fail(function (res) { 26 | // failure, render errors 27 | try { 28 | renderErrors(JSON.parse(res.responseText)) 29 | } catch (e) { 30 | renderErrors(res.responseText) 31 | } 32 | }) 33 | }) 34 | 35 | function renderErrors (json) { 36 | // general error 37 | $('#error-general').text(json.message || json) 38 | 39 | // individual form errors 40 | var details = json.details || {} 41 | ;(['newPassword']).forEach(function (name) { 42 | if (details[name]) { 43 | $('#error-' + name) 44 | .text(details[name].msg) 45 | .parent() 46 | .addClass('warning') 47 | } else { 48 | $('#error-' + name) 49 | .text('') 50 | .parent() 51 | .removeClass('warning') 52 | } 53 | }) 54 | } 55 | }) 56 | -------------------------------------------------------------------------------- /assets/js/ua.js: -------------------------------------------------------------------------------- 1 | /* global $ navigator localStorage */ 2 | 3 | // ua js 4 | $(function () { 5 | var beakerPrompts = $('.beaker-prompt') 6 | var usingBeaker = navigator && navigator.userAgent.includes('BeakerBrowser') 7 | 8 | if (!usingBeaker && localStorage.hasDismissedBeakerPrompt !== '1') { 9 | Array.from(beakerPrompts).forEach(function (el) { 10 | el.classList.remove('hidden') 11 | $(el).click(function (e) { 12 | el.style.display = 'none' 13 | localStorage.hasDismissedBeakerPrompt = 1 14 | }) 15 | }) 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /assets/js/upload-progress.js: -------------------------------------------------------------------------------- 1 | /* global $ EventSource */ 2 | 3 | // upload progress bar 4 | $(function () { 5 | var sizeContainer = $('#archive-size') 6 | var progressBarContainer = $('.progress-bar-container') 7 | var progressBar = progressBarContainer.find('.progress-bar') 8 | var progressInner = progressBar.find('.progress') 9 | var label = progressBarContainer.find('.label') 10 | var key = progressBar.data('key') 11 | 12 | onProgress(window.params.progress) 13 | 14 | var events = new EventSource('/v2/archives/item/' + key + '?view=status') 15 | events.addEventListener('message', function (e) { 16 | var datas = (e.data || '').split(' ') 17 | onProgress(datas[0], datas[1]) 18 | }) 19 | 20 | function onProgress (progress, diskUsage) { 21 | if (typeof progress !== 'undefined') { 22 | progressBar.attr('aria-valuenow', progress) 23 | progressInner.attr('style', 'width: ' + progress + '%') 24 | label.find('span').html(progress + '%') 25 | if (progress < 100) { 26 | label.find('i').show() 27 | } else { 28 | label.find('i').hide() 29 | } 30 | } 31 | if (diskUsage) { 32 | sizeContainer.text(diskUsage) 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /assets/js/user-admin.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | // user page admin js 4 | $(function () { 5 | var sendEmailForm = $('#send-email') 6 | 7 | sendEmailForm.on('submit', function (e) { 8 | e.preventDefault() 9 | 10 | // serialize form values 11 | var values = {} 12 | $(this).serializeArray().forEach(function (value) { 13 | values[value.name] = value.value 14 | }) 15 | 16 | // post to api 17 | $.post('/v2/admin/users/' + values['username'] + '/send-email', values, function (res, status) { 18 | if (status !== 'success') console.error(status, res) 19 | else { 20 | $('#send-email-success').text('Sent message to ' + values['username']) 21 | sendEmailForm[0].reset() 22 | } 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /assets/js/util.js: -------------------------------------------------------------------------------- 1 | window.makeSafe = function makeSafe (str) { 2 | return str.replace(//g, '>').replace(/&/g, '&').replace(/"/g, '') 3 | } 4 | -------------------------------------------------------------------------------- /assets/js/zepto-patches.js: -------------------------------------------------------------------------------- 1 | /* global $:false */ 2 | { 3 | // monkey-patch $.post so that it always sends JSON 4 | function parseArguments (url, data, success, dataType) { 5 | if ($.isFunction(data)) { 6 | dataType = success 7 | success = data 8 | data = undefined 9 | } 10 | if (!$.isFunction(success)) { 11 | dataType = success 12 | success = undefined 13 | } 14 | return { 15 | url: url, 16 | data: data, 17 | success: success, 18 | dataType: dataType 19 | } 20 | } 21 | $.post = function (/* url, data, success, dataType */) { 22 | var options = parseArguments.apply(null, arguments) 23 | options.type = 'POST' 24 | if (options.data) { 25 | if (typeof options.data !== 'string') { 26 | options.data = JSON.stringify(options.data) 27 | } 28 | options.contentType = 'application/json' 29 | } 30 | return $.ajax(options) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | var config = require('./lib/config') 2 | var createApp = require('./index') 3 | var log = require('debug')('LE') 4 | var figures = require('figures') 5 | 6 | async function start () { 7 | var app = await createApp(config) 8 | if (config.letsencrypt) { 9 | var greenlockExpress = require('greenlock-express') 10 | var debug = config.letsencrypt.debug !== false 11 | var agreeTos = config.letsencrypt.agreeTos !== false 12 | greenlockExpress.create({ 13 | version: 'draft-11', 14 | server: debug ? 'https://acme-staging-v02.api.letsencrypt.org/directory' : 'https://acme-v02.api.letsencrypt.org/directory', 15 | debug, 16 | agreeTos, 17 | approveDomains: app.approveDomains, 18 | app, 19 | log 20 | }).listen(80, 443) 21 | } else { 22 | app.listen(config.port, () => { 23 | console.log(figures.tick, `Server started on http://127.0.0.1:${config.port}`) 24 | }) 25 | } 26 | } 27 | start() 28 | -------------------------------------------------------------------------------- /config.defaults.yml: -------------------------------------------------------------------------------- 1 | dir: ./.hashbase 2 | brandname: Hashbase 3 | hostname: hashbase.local 4 | proxy: true 5 | port: 8080 6 | letsencrypt: false 7 | sites: false 8 | rateLimiting: true 9 | csrf: true 10 | 11 | # usage limits 12 | defaultDiskUsageLimit: 100mb 13 | defaultNamedArchivesLimit: 15 14 | proDiskUsageLimit: 10gb 15 | proNamedArchivesLimit: 100 16 | bandwidthLimit: 17 | up: 1mb 18 | down: 1mb 19 | 20 | # processing jobs 21 | jobs: 22 | popularArchivesIndex: 30s 23 | userDiskUsage: 5m 24 | deleteDeadArchives: 5m 25 | 26 | # cache settings 27 | cache: 28 | metadataStorage: 65536 29 | contentStorage: 65536 30 | tree: 65536 31 | 32 | # monitoring 33 | pm2: true 34 | alerts: 35 | diskUsage: 1gb 36 | 37 | # user settings 38 | registration: 39 | open: false 40 | allowed: 41 | - alice@mail.com 42 | - bob@mail.com 43 | reservedNames: 44 | - admin 45 | - root 46 | - support 47 | - noreply 48 | - users 49 | - archives 50 | admin: 51 | email: '' 52 | password: '' 53 | 54 | # email settings 55 | email: 56 | transport: stub 57 | sender: '"Hashbase" ' 58 | 59 | # login sessions 60 | sessions: 61 | algorithm: HS256 62 | secret: THIS MUST BE REPLACED! 63 | expiresIn: 1h 64 | -------------------------------------------------------------------------------- /contributors.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Paul Frazee 3 | catchphrase: "If it ain't P2P, it ain't free" 4 | website: https://twitter.com/pfrazee 5 | --- 6 | name: Joe Hand 7 | catchphrase: "Building things by hand." 8 | website: https://joeahand.com 9 | --- 10 | name: Yvo Brevoort 11 | catchphrase: "Simply edit all the things" 12 | website: https://twitter.com/YvoBrevoort -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Docs 2 | 3 | - [Contributing Guidelines](../CONTRIBUTING.md) 4 | 5 | ### APIs 6 | 7 | - [Web API](./webapis.md). Complete description of all endpoints. 8 | 9 | ### Flows 10 | 11 | - [Registration Flow](./flows/registration.md). User-registration and verification. 12 | - [Forgot Password Flow](./flows/forgot-password.md). User password reset. 13 | - [Dat Ownership Proof Flow](./flows/dat-ownership-proof.md). How ownership of a dat by a specific user is verified. 14 | 15 | ### Components 16 | 17 | - [Jobs](./components/jobs.md). Behaviors that either get triggered by a message, or auto-triggered by the scheduler. 18 | - [Triggers](./components/triggers.md). Any file-indexing is handled by Triggers, which watch for changes to specific paths and archives, then queue jobs automatically when a change is detected. 19 | - [Locks](./components/locks.md). Locks are used internally to create regions of async code that will only be entered one at a time. 20 | 21 | ### Schemas 22 | 23 | - [Access Scopes](./schemas/access-scopes.md). The different permissions available to users. 24 | - [LevelDB](./schemas/leveldb.md). LevelDB layout and objects. 25 | - [Events](./schemas/events.md). Events emitted by various components. -------------------------------------------------------------------------------- /docs/components/jobs.md: -------------------------------------------------------------------------------- 1 | # Jobs Component 2 | 3 | Jobs are behaviors that either get triggered by a message, or auto-triggered by the scheduler. 4 | 5 | ## Jobs API 6 | 7 | The jobs manager is an event broker. When it comes time to scale horizontally, it will be internally rewritten to use RabbitMQ. 8 | 9 | ```js 10 | jobs.queue(name[, data]) // add a one-time job 11 | jobs.requeue(job) // remove, then re-add the job to the queue 12 | jobs.markDone(job) // remove the job from the queue 13 | jobs.addHandler(name, job => ...) // add a handler for the job 14 | jobs.removeHandler(handlerId) // remove a handler 15 | ``` 16 | 17 | Example of setting up jobs: 18 | 19 | ```js 20 | jobs.queue('verify-profile-dat', { userId: '...', url: '...' }) 21 | jobs.queue('clean-unverified-users') 22 | ``` 23 | 24 | Example of handling jobs: 25 | 26 | ```js 27 | var { hostname } = config 28 | jobs.addHandler('verify-profile-dat', job => { 29 | var { userId, url } = job.data 30 | readDatFile(`${url}/proofs/${hostname}`, (err, data) => { 31 | // ... 32 | jobs.markDone(job) 33 | }) 34 | }) 35 | ``` 36 | 37 | ## Scheduler API 38 | 39 | The scheduler adds cron-style timers/intervals to queue jobs at certain times. 40 | 41 | ```js 42 | scheduler.add(name, when[, data]) // schedule a job (cron syntax) 43 | scheduler.list([name]) // list active jobs 44 | scheduler.remove(scheduleId) // remove a scheduled job 45 | ``` 46 | 47 | Example of scheduling jobs: 48 | 49 | ```js 50 | // should be run during app startup 51 | scheduler.add('clean-unverified-users', '0 0 0 * * *') // run at midnight every day 52 | ``` 53 | 54 | ## Jobs 55 | 56 | ### Verify Profile Dat 57 | 58 | - Name: `verify-profile-dat' 59 | - Task: Read the proof file in the profile, verify the proof, and update the user record. 60 | - Data: 61 | - `userId`: ID of the account that is attached to the profile 62 | - `url`: URL of the profile-dat 63 | - Preconditions: 64 | - User account should have its email verified 65 | - Profile-dat and the proof-file should be locally available 66 | 67 | ### Dead Archive Cleanup 68 | 69 | - Name: `clean-dead-archives` 70 | - Task: Deletes any archives referenced in [`dead-archives`](https://github.com/joehand/hypercloud/wiki/Archives-Schema#layout) (no hosting users). 71 | 72 | ### Unverified User Cleanup 73 | 74 | - Name: `clean-unverified-users` 75 | - Task: Deletes any user records older than a day with `isEmailVerified==false` -------------------------------------------------------------------------------- /docs/components/locks.md: -------------------------------------------------------------------------------- 1 | # Locks Component 2 | 3 | Locks are used internally to create regions of async code that will only be entered one at a time. Locks are necessary to coordinate multi-step changes to the level databases. 4 | 5 | Take care to coordinate the locks across the codebase. Some lock identifiers need to be reused in multiple code regions. Be careful not to use an identifier twice in a row, without first releasing, since that will stall the request. 6 | 7 | ## Usage 8 | 9 | ```js 10 | var lock = require('./lock') 11 | 12 | async function foo () { 13 | var release = await lock('bar') 14 | try { 15 | // do work 16 | } finally { 17 | release() 18 | } 19 | } 20 | ``` 21 | 22 | Be sure to always use a try/finally block. 23 | 24 | ## Locks in use 25 | 26 | - `users`. Must be used any time updates are made to the users DB. 27 | - `archives`. Must be used any time an update is made to the archives DB. 28 | - `archiver-job`. Used to make sure only one job runs at once (to avoid overloading the thread). -------------------------------------------------------------------------------- /docs/components/triggers.md: -------------------------------------------------------------------------------- 1 | # Triggers Component 2 | 3 | Any file-indexing is handled by Triggers, which watch for changes to specific paths and archives, then queue jobs automatically when a change is detected. 4 | 5 | In some ways, Triggers are an alternative control mechanism to HTTP requests. Rather than a GET/POST, clients write and upload files which cause updates in the hypercloud instance. 6 | 7 | ## Triggers API 8 | 9 | ```js 10 | triggers.add(pathSpec, handler) // add a trigger & handler function 11 | triggers.list() // list the active triggers 12 | triggers.remove(handerId) // remove a trigger & handler 13 | ``` 14 | 15 | The `pathSpec` may be a string or regex. 16 | 17 | Example usage: 18 | 19 | ```js 20 | triggers.add('/proofs/hypercloud.com', (archive, entry) => { 21 | jobs.queue('verify-profile-dat', { datUrl: archive.url }) 22 | }) 23 | 24 | triggers.add(new RegExp('/dats/[a-z0-9]'), (archive, entry) => { 25 | // ... 26 | }) 27 | ``` -------------------------------------------------------------------------------- /docs/flows/dat-ownership-proof.md: -------------------------------------------------------------------------------- 1 | # Dat Ownership Proof Flow 2 | 3 | This describes a process for asserting ownership of a Dat by writing a pre-defined payload, then syncing the dat to hypercloud. 4 | 5 | > This spec was originally part of the registration flow. It's now being preserved, as a general-purpose flow, until we have a deployment plan for it. 6 | 7 | ## Step 1. Claim ownership (POST /v2/archives/claim) 8 | 9 | User POSTS `/v2/archives/claim` while authenticated with body (JSON): 10 | 11 | ``` 12 | { 13 | key: String, they key of the dat, or 14 | url: String, the url of the dat 15 | } 16 | ``` 17 | 18 | Server generates the `proof` (a non-expiring JWT) with the following content: 19 | 20 | ``` 21 | { 22 | id: String, id of the user 23 | url: String, the URL of the dat 24 | } 25 | ``` 26 | 27 | Server responds 200 with the body: 28 | 29 | ``` 30 | { 31 | proof: String, the encoded JWT 32 | hostname: String, the hostname of this service 33 | } 34 | ``` 35 | 36 | ## Step 2. Write proof 37 | 38 | User writes the `proof` to the `/proofs/:hostname` file of their profile dat. User then syncs the updated dat to the service. 39 | 40 | User GETS `/v2/archives/item/:key?view=proofs` periodically to watch for successful sync. 41 | 42 | ## Step 3. Validate claim 43 | 44 | Server receives proof-file in the dat. After checking the JWT signature, the server updates archive record to indicate the verified ownership. -------------------------------------------------------------------------------- /docs/flows/forgot-password.md: -------------------------------------------------------------------------------- 1 | # Forgot Password Flow 2 | 3 | ## Step 1. Trigger flow (POST /v2/accounts/forgot-password) 4 | 5 | User POSTS to `/v2/accounts/forgot-password` with body: 6 | 7 | ``` 8 | { 9 | email: String 10 | } 11 | ``` 12 | 13 | A random 32-byte email-verification nonce is created and saved the user record. The user record indicates: 14 | 15 | forgotPasswordNonce|passwordHash|passwordSalt 16 | --------------------------------------------- 17 | XXX|old|old 18 | 19 | Server sends an email to the user with the `forgotPasswordNonce`. 20 | 21 | Server responds 200 with JSON indicating to check email. 22 | 23 | ## Step 2. Update password (POST /v2/accounts/account/password) 24 | 25 | User POSTS `/v2/accounts/account/password` with body: 26 | 27 | ``` 28 | { 29 | username: String, username of the account 30 | nonce: String, verification nonce 31 | newPassword: String, new password 32 | } 33 | ``` 34 | 35 | Server updates user record to indicate: 36 | 37 | forgotPasswordNonce|passwordHash|passwordSalt 38 | --------------------------------------------- 39 | null|new|new -------------------------------------------------------------------------------- /docs/flows/registration.md: -------------------------------------------------------------------------------- 1 | # User Registration Flow 2 | 3 | ## Step 1. Register (POST /v2/accounts/register) 4 | 5 | User POSTS to `/v2/accounts/register` with body: 6 | 7 | ``` 8 | { 9 | email: String 10 | username: String 11 | password: String 12 | } 13 | ``` 14 | 15 | Server creates a new account for the user. A random 32-byte email-verification nonce is created. The user record indicates: 16 | 17 | scopes|isEmailVerified|emailVerificationNonce 18 | ------|---------------|---------------- 19 | none|false|XXX 20 | 21 | Server sends an email to the user with the `emailVerificationNonce`. 22 | 23 | Server responds 200 with HTML/JSON indicating to check email. 24 | 25 | ## Step 2. Verify (GET or POST /v2/accounts/verify) 26 | 27 | User GETS or POSTS `/v2/accounts/verify` with query-params or body: 28 | 29 | ``` 30 | { 31 | username: String, username of the account 32 | nonce: String, verification nonce 33 | } 34 | ``` 35 | 36 | Server updates user record to indicate: 37 | 38 | scopes|isEmailVerified|emailVerificationNonce 39 | ------|---------------|---------------- 40 | user|true|null 41 | 42 | Sever generates session JWT and responds 200 with auth=token. -------------------------------------------------------------------------------- /docs/schemas/access-scopes.md: -------------------------------------------------------------------------------- 1 | # Access Scopes 2 | 3 | Scopes are hypercloud's term for "permissions" 4 | 5 | - `user` - base permission, must be set for any non-public API to be used 6 | - `admin:dats` - control dats via API 7 | - `admin:users` - control users via API -------------------------------------------------------------------------------- /docs/schemas/events.md: -------------------------------------------------------------------------------- 1 | ## UsersDB 2 | 3 | Emits: 4 | 5 | ```js 6 | usersDB.on('create', (record) => {}) 7 | usersDB.on('put', (record) => {}) 8 | usersDB.on('del', (record) => {}) 9 | usersDB.on('add-archive', ({userId, archiveKey, name}, record) => {}) 10 | usersDB.on('remove-archive', ({userId, archiveKey}, record) => {}) 11 | ``` 12 | 13 | ## ArchivesDB 14 | 15 | ```js 16 | archivesDB.emit('create', (record) => {}) 17 | archivesDB.emit('put', (record) => {}) 18 | archivesDB.emit('del', (record) => {}) 19 | archivesDB.emit('add-hosting-user', ({key, userId}, record) => {}) 20 | archivesDB.emit('remove-hosting-user', ({key, userId}, record) => {}) 21 | ``` -------------------------------------------------------------------------------- /docs/schemas/leveldb.md: -------------------------------------------------------------------------------- 1 | # Level Database Schema 2 | 3 | Layout and schemas of the data in the LevelDB. 4 | 5 | ## Layout 6 | 7 | - `main` 8 | - `archives`: Map of `key => Archive object`. 9 | - `archives-index`: Index of `createdAt => key` 10 | - `accounts`: Map of `id => Account object`. 11 | - `accounts-index`: Index of `username => id`, `email => id`, `profileUrl => id`. 12 | - `global-activity`: Map of `timestamp => Event object`. 13 | - `global-activity-users-index`: Set of `username:timestamp => null` for doing user filtering. 14 | - `dead-archives`: Map of `key => undefined`. A listing of archives with no hosting users, and which need to be deleted. 15 | 16 | ## Archive object 17 | 18 | Schema: 19 | 20 | ``` 21 | { 22 | key: String, the archive key 23 | 24 | hostingUsers: Array(String), list of user-ids hosting the archive 25 | 26 | updatedAt: Number, the timestamp of the last update 27 | createdAt: Number, the timestamp of creation time 28 | } 29 | ``` 30 | 31 | ## Account object 32 | 33 | Schema: 34 | 35 | ``` 36 | { 37 | id: String, the assigned id 38 | username: String, the chosen username 39 | passwordHash: String, hashed password 40 | passwordSalt: String, salt used on hashed password 41 | 42 | email: String 43 | pendingEmail: String, the user's new email address pending verification 44 | profileURL: String, the url of the profile dat 45 | archives: [{ 46 | key: String, uploaded archive's key 47 | name: String, optional shortname for the archive 48 | }, ..] 49 | scopes: Array(String), the user's access scopes 50 | suspension: String, if suspended, will be set to an explanation 51 | updatedAt: Number, the timestamp of the last update 52 | createdAt: Number, the timestamp of creation time 53 | 54 | isEmailVerified: Boolean 55 | emailVerificationNonce: String, the random verification nonce (register flow) 56 | 57 | forgotPasswordNonce: String, the random verification nonce (forgot password flow) 58 | 59 | isProfileDatVerified: Boolean 60 | profileVerifyToken: String, the profile verification token (stored so the user can refetch it) 61 | } 62 | ``` 63 | 64 | ## Report object 65 | 66 | Schema: 67 | ``` 68 | { 69 | id: String, the id of this report 70 | 71 | archiveKey: String, the archive key 72 | 73 | archiveOwner: String, the user ID of the archive’s owner 74 | reportingUser: String, the user ID of the user that reported it 75 | 76 | reason: String, the reason for reporting the archive 77 | status: String, the status of the report. Can be ‘open’ or ‘closed’ 78 | notes: String, administrative notes on this report (used internally) 79 | 80 | createdAt: Number, the timestamp of the report 81 | updatedAt: Number, the timestamp the report was last updated 82 | } 83 | ``` 84 | 85 | ## Event object 86 | 87 | Schema: 88 | 89 | ``` 90 | { 91 | ts: Number, the timestamp of the event 92 | userid: String, the user who made the change 93 | username: String, the name of the user who made the change 94 | action: String, the label for the action 95 | params: Object, a set of arbitrary KVs relevant to the action 96 | } 97 | ``` -------------------------------------------------------------------------------- /lib/analytics.js: -------------------------------------------------------------------------------- 1 | const mtb36 = require('monotonic-timestamp-base36') 2 | const ms = require('ms') 3 | 4 | // exported api 5 | // = 6 | 7 | module.exports.middleware = function (cloud) { 8 | return (req, res, next) => { 9 | var peaID = req.cookies['pea-id'] 10 | if (!peaID) { 11 | // set analytics cookie 12 | peaID = mtb36() 13 | res.cookie('pea-id', peaID, { 14 | domain: cloud.config.hostname, 15 | httpOnly: true, 16 | maxAge: ms('1y'), 17 | sameSite: 'Lax' 18 | }) 19 | } 20 | 21 | req.logAnalytics = function (event, extra = {}) { 22 | var referer = req.headers.referer 23 | if (referer && referer.startsWith('https://' + cloud.config.hostname)) { 24 | referer = null 25 | } 26 | extra.method = req.method 27 | extra.user = res.locals.sessionUser ? res.locals.sessionUser.username : null 28 | cloud.analytics.logEvent({ 29 | event: event, 30 | url: req.path, 31 | session: peaID, 32 | userAgent: req.headers['user-agent'], 33 | ip: req.ip, 34 | referer, 35 | extra 36 | }) 37 | } 38 | 39 | // log request 40 | req.logAnalytics('visit') 41 | 42 | next() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/apis/reports.js: -------------------------------------------------------------------------------- 1 | const {UnauthorizedError, ForbiddenError} = require('../const') 2 | 3 | // exported api 4 | // = 5 | 6 | module.exports = class ReportsAPI { 7 | constructor (cloud) { 8 | this.reportsDB = cloud.reportsDB 9 | this.usersDB = cloud.usersDB 10 | } 11 | 12 | async add (req, res) { 13 | // validate session 14 | if (!res.locals.session) throw new UnauthorizedError() 15 | if (!res.locals.session.scopes.includes('user')) throw new ForbiddenError() 16 | 17 | // validate & sanitize input 18 | req.checkBody('archiveKey').isDatHash() 19 | req.checkBody('archiveOwner').isAlphanumeric() 20 | req.checkBody('reason').isAlphanumeric() 21 | ;(await req.getValidationResult()).throw() 22 | 23 | var { archiveKey, archiveOwner, reason } = req.body 24 | 25 | try { 26 | // fetch the archive owner's record 27 | var archiveOwnerRecord = await this.usersDB.getByUsername(archiveOwner) 28 | var report = Object.assign({}, { 29 | archiveKey, 30 | archiveOwner: archiveOwnerRecord.id, 31 | reason, 32 | reportingUser: res.locals.session.id 33 | }) 34 | 35 | // create the report 36 | await this.reportsDB.create(report) 37 | } catch (e) { 38 | return res.status(422).json({ 39 | message: 'There were errors in your submission' 40 | }) 41 | } 42 | 43 | // respond 44 | res.status(200).end() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/apis/service.js: -------------------------------------------------------------------------------- 1 | var {NotImplementedError} = require('../const') 2 | 3 | // exported api 4 | // = 5 | 6 | module.exports = class ServicesAPI { 7 | constructor (cloud) { 8 | this.config = cloud.config 9 | this.usersDB = cloud.usersDB 10 | this.activityDB = cloud.activityDB 11 | this.archivesDB = cloud.archivesDB 12 | } 13 | 14 | async frontpage (req, res, next) { 15 | var contentType = req.accepts(['html', 'json']) 16 | if (contentType === 'json') throw new NotImplementedError() 17 | next() 18 | } 19 | 20 | async psaDoc (req, res) { 21 | return res.status(200).json({ 22 | PSA: 1, 23 | title: this.config.brandname, 24 | description: 'A public peer service for Dat', 25 | links: [{ 26 | rel: 'https://archive.org/services/purl/purl/datprotocol/spec/pinning-service-account-api', 27 | title: 'User accounts API', 28 | href: '/v2/accounts' 29 | }, { 30 | rel: 'https://archive.org/services/purl/purl/datprotocol/spec/pinning-service-dats-api', 31 | title: 'Dat pinning API', 32 | href: '/v2/archives' 33 | }] 34 | }) 35 | } 36 | 37 | async explore (req, res, next) { 38 | if (req.query.view === 'activity') { 39 | return res.json({ 40 | activity: await this.activityDB.listGlobalEvents({ 41 | limit: 25, 42 | lt: req.query.start, 43 | reverse: true 44 | }) 45 | }) 46 | } 47 | if (req.query.view === 'featured') { 48 | return res.json({ 49 | featured: (await this.archivesDB.list({featuredOnly: true, getExtra: true})).map(mapArchiveObject) 50 | }) 51 | } 52 | if (req.query.view === 'popular') { 53 | return res.json({ 54 | popular: (await this.archivesDB.list({ 55 | sort: 'popular', 56 | limit: 25, 57 | cursor: req.query.start 58 | })).map(mapArchiveObject) 59 | }) 60 | } 61 | if (req.query.view === 'recent') { 62 | return res.json({ 63 | recent: (await this.archivesDB.list({ 64 | sort: 'createdAt', 65 | limit: 25, 66 | cursor: req.query.start, 67 | getExtra: true 68 | })).map(mapArchiveObject) 69 | }) 70 | } 71 | next() 72 | } 73 | } 74 | 75 | function mapArchiveObject (archive) { 76 | return { 77 | key: archive.key, 78 | numPeers: archive.numPeers, 79 | name: archive.name, 80 | title: archive.manifest ? archive.manifest.title : null, 81 | description: archive.manifest ? archive.manifest.description : null, 82 | owner: archive.owner ? archive.owner.username : null, 83 | createdAt: archive.createdAt 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const extend = require('deep-extend') 4 | const yaml = require('js-yaml') 5 | const figures = require('figures') 6 | 7 | function load (name) { 8 | var str, doc 9 | var filepath = path.join(__dirname, `../config.${name}.yml`) 10 | 11 | try { 12 | str = fs.readFileSync(filepath, 'utf8') 13 | } catch (e) { 14 | return {} 15 | } 16 | 17 | try { 18 | doc = yaml.safeLoad(str) 19 | } catch (e) { 20 | console.log('Failed to parse', filepath, e) 21 | return {} 22 | } 23 | 24 | return doc 25 | } 26 | 27 | // load the config 28 | var env = process.env.NODE_ENV || 'development' 29 | var defaultCfg = load('defaults') 30 | var envCfg = load(env) 31 | module.exports = extend(defaultCfg, envCfg, { env }) 32 | 33 | // some warnings 34 | if (!module.exports.csrf) { 35 | console.log(figures.warning, 'WARNING: CSRF is DISABLED') 36 | } 37 | if (!module.exports.stripe) { 38 | console.log(figures.warning, 'WARNING: Stripe payments are DISABLED') 39 | } 40 | -------------------------------------------------------------------------------- /lib/const.js: -------------------------------------------------------------------------------- 1 | exports.DAT_KEY_REGEX = /([0-9a-f]{64})/i 2 | exports.DAT_URL_REGEX = /^(dat:\/\/[0-9a-f]{64})/i 3 | exports.DAT_NAME_REGEX = /^([0-9a-zA-Z-]*)$/i 4 | 5 | exports.ADMIN_MODIFIABLE_FIELDS_USER = [ 6 | 'username', 7 | 'email', 8 | 'scopes', 9 | 'suspension', 10 | 'createdAt', 11 | 'plan', 12 | 'diskQuota', 13 | 'namedArchiveQuota', 14 | 'isEmailVerified', 15 | 'stripeCustomerId', 16 | 'stripeSubscriptionId', 17 | 'stripeTokenId', 18 | 'stripeCardId', 19 | 'stripeCardBrand', 20 | 'stripeCardCountry', 21 | 'stripeCardCVCCheck', 22 | 'stripeCardExpMonth', 23 | 'stripeCardExpYear', 24 | 'stripeCardLast4' 25 | ] 26 | 27 | exports.ADMIN_MODIFIABLE_FIELDS_REPORT = [ 28 | 'status', 29 | 'notes' 30 | ] 31 | 32 | // active_users cohort states 33 | exports.COHORT_STATE_REGISTERED = 1 34 | exports.COHORT_STATE_ACTIVATED = 2 35 | exports.COHORT_STATE_ACTIVE = 3 36 | 37 | exports.BadRequestError = class BadRequestError extends Error { 38 | constructor (message) { 39 | super(message) 40 | this.name = 'BadRequestError' 41 | this.status = 400 42 | this.body = { 43 | message: message || 'Bad request', 44 | badRequest: true 45 | } 46 | } 47 | } 48 | 49 | exports.NotFoundError = class NotFoundError extends Error { 50 | constructor (message) { 51 | super(message) 52 | this.name = 'NotFoundError' 53 | this.status = 404 54 | this.body = { 55 | message: message || 'Resource not found', 56 | notFound: true 57 | } 58 | } 59 | } 60 | 61 | exports.UnauthorizedError = class UnauthorizedError extends Error { 62 | constructor (message) { 63 | super(message) 64 | this.name = 'UnauthorizedError' 65 | this.status = 401 66 | this.body = { 67 | message: message || 'You must sign in to access this resource', 68 | notAuthorized: true 69 | } 70 | } 71 | } 72 | 73 | exports.ForbiddenError = class ForbiddenError extends Error { 74 | constructor (message) { 75 | super(message) 76 | this.name = 'ForbiddenError' 77 | this.status = 403 78 | this.body = { 79 | message: message || 'You dont have the rights to access this resource', 80 | forbidden: true 81 | } 82 | } 83 | } 84 | 85 | exports.NotImplementedError = class NotImplementedError extends Error { 86 | constructor (message) { 87 | super(message) 88 | this.name = 'NotImplementedError' 89 | this.status = 501 90 | this.body = { 91 | message: message || 'Resources not yet implemented', 92 | notImplemented: true 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/crypto.js: -------------------------------------------------------------------------------- 1 | var promisify = require('es6-promisify') 2 | var { createHash, randomBytes } = require('crypto') 3 | 4 | // promisify some methods 5 | randomBytes = promisify(randomBytes) 6 | 7 | // exported api 8 | // = 9 | 10 | exports.randomBytes = randomBytes 11 | 12 | exports.shasum = shasum 13 | function shasum (buf) { 14 | if (typeof buf !== 'string' && !Buffer.isBuffer(buf)) { 15 | buf = JSON.stringify(buf) 16 | } 17 | return createHash('sha256') 18 | .update(buf, Buffer.isBuffer(buf) ? null : 'utf8') 19 | .digest('hex') 20 | } 21 | 22 | exports.hashPassword = async function (password) { 23 | // generate a new salt and hash the password with it 24 | var passwordSalt = await randomBytes(16) 25 | password = Buffer.from(password, 'utf8') 26 | return { 27 | passwordHash: shasum(Buffer.concat([passwordSalt, password])), 28 | passwordSalt: passwordSalt.toString('hex') 29 | } 30 | } 31 | 32 | exports.verifyPassword = function (password, userRecord) { 33 | // verify that the given password, when hashed, is the same as the password on record 34 | var passwordHash = shasum(Buffer.concat([ 35 | Buffer.from(userRecord.passwordSalt, 'hex'), 36 | Buffer.from(password, 'utf8') 37 | ])) 38 | return (passwordHash === userRecord.passwordHash) 39 | } 40 | -------------------------------------------------------------------------------- /lib/dbs/activity.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var SQL = require('sql-template-strings') 3 | 4 | // constants 5 | // = 6 | 7 | // valid actions 8 | const ACTIONS = [ 9 | 'add-archive', 10 | 'del-archive', 11 | 'update-archive' 12 | ] 13 | 14 | // exported api 15 | // = 16 | 17 | class ActivityDB { 18 | constructor (cloud) { 19 | this.sqlite = cloud.db 20 | } 21 | 22 | async setup () { 23 | // noop 24 | } 25 | 26 | // basic ops 27 | // = 28 | 29 | async writeGlobalEvent (record, opts = {}) { 30 | assert(record && typeof record === 'object') 31 | assert(typeof record.userid === 'string', 'Valid userid type') 32 | assert(typeof record.username === 'string', 'Valid username type') 33 | assert(ACTIONS.includes(record.action), 'Valid action type') 34 | if (!opts.doNotModify) { 35 | record.ts = Date.now() 36 | } 37 | var {ts, userid, username, action, params} = ActivityDB.serialize(record) 38 | await this.sqlite.run(SQL` 39 | INSERT INTO activity 40 | (ts, userid, username, action, params) 41 | VALUES 42 | (${ts}, ${userid}, ${username}, ${action}, ${params}) 43 | `) 44 | return record 45 | } 46 | 47 | async delGlobalEvent (key) { 48 | assert(typeof key === 'string') 49 | await this.sqlite.run(SQL`DELETE FROM activity WHERE key = ${key}`) 50 | } 51 | 52 | // getters 53 | // = 54 | 55 | async listGlobalEvents ({limit, lt, gt, lte, gte, reverse} = {}) { 56 | var query = SQL`SELECT * FROM activity` 57 | if (lt) query.append(SQL` WHERE key < ${lt}`) 58 | if (lte) query.append(SQL` WHERE key <= ${lte}`) 59 | if (gt) query.append(SQL` WHERE key > ${gt}`) 60 | if (gte) query.append(SQL` WHERE key >= ${gte}`) 61 | if (!reverse) query.append(SQL` ORDER BY key`) 62 | else query.append(SQL` ORDER BY key DESC`) 63 | if (limit) query.append(SQL` LIMIT ${limit}`) 64 | var records = await this.sqlite.all(query) 65 | return records.map(ActivityDB.deserialize) 66 | } 67 | 68 | async listUserEvents (username, {limit, lt, gt, lte, gte, reverse} = {}) { 69 | var query = SQL`SELECT * FROM activity WHERE username = ${username}` 70 | if (lt) query.append(SQL` AND key < ${lt}`) 71 | if (lte) query.append(SQL` AND key <= ${lte}`) 72 | if (gt) query.append(SQL` AND key > ${gt}`) 73 | if (gte) query.append(SQL` AND key >= ${gte}`) 74 | if (!reverse) query.append(SQL` ORDER BY key`) 75 | else query.append(SQL` ORDER BY key DESC`) 76 | if (limit) query.append(SQL` LIMIT ${limit}`) 77 | var records = await this.sqlite.all(query) 78 | return records.map(ActivityDB.deserialize) 79 | } 80 | 81 | // helpers 82 | // = 83 | 84 | static serialize (record) { 85 | if (!record) return null 86 | var r2 = Object.assign({}, record) 87 | if ('params' in r2) r2.params = JSON.stringify(record.params) 88 | return r2 89 | } 90 | 91 | static deserialize (record) { 92 | if (!record) return null 93 | var r2 = Object.assign({}, record) 94 | if ('params' in r2) r2.params = JSON.parse(r2.params) 95 | return r2 96 | } 97 | } 98 | module.exports = ActivityDB 99 | -------------------------------------------------------------------------------- /lib/dbs/base.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events') 2 | var SQL = require('sql-template-strings') 3 | 4 | class Base extends EventEmitter { 5 | constructor (cloud, table, primaryKey) { 6 | super() 7 | this.cloud = cloud 8 | this.table = table 9 | this.primaryKey = primaryKey 10 | this.columns = [] 11 | } 12 | 13 | async setup () { 14 | this.columns = (await this.cloud.db.all(`pragma table_info(${this.table});`)).map(column => column.name) 15 | } 16 | 17 | createInsertQuery (record) { 18 | var query = SQL`INSERT INTO` 19 | query.append(` ${this.table} `) 20 | 21 | var first = true 22 | query.append(`(`) 23 | this.columns.forEach(k => { 24 | if (!(k in record)) return 25 | if (!first) query.append(`, `) 26 | query.append(k) 27 | first = false 28 | }) 29 | query.append(`)`) 30 | 31 | query.append(` VALUES `) 32 | 33 | first = true 34 | query.append(`(`) 35 | this.columns.forEach(k => { 36 | if (!(k in record)) return 37 | if (!first) query.append(`, `) 38 | query.append(SQL`${record[k]}`) 39 | first = false 40 | }) 41 | query.append(`)`) 42 | 43 | // console.log('createInsertQuery', this.table, this.primaryKey, this.columns, record, query) 44 | 45 | return query 46 | } 47 | 48 | createUpdateQuery (record) { 49 | var query = SQL`UPDATE` 50 | query.append(` ${this.table} SET `) 51 | 52 | var first = true 53 | this.columns.forEach(k => { 54 | if (k === this.primaryKey) return 55 | if (!(k in record)) return 56 | if (!first) query.append(`, `) 57 | query.append(k) 58 | query.append(SQL` = ${record[k]}`) 59 | first = false 60 | }) 61 | 62 | query.append(` WHERE ${this.primaryKey}`) 63 | query.append(SQL` = ${record[this.primaryKey]}`) 64 | 65 | // console.log('createUpdateQuery', this.table, this.primaryKey, this.columns, record, query) 66 | return query 67 | } 68 | } 69 | 70 | module.exports = Base 71 | -------------------------------------------------------------------------------- /lib/dbs/legacy-leveldb/activity.js: -------------------------------------------------------------------------------- 1 | var sublevel = require('subleveldown') 2 | var collect = require('stream-collector') 3 | 4 | // exported api 5 | // = 6 | 7 | class ActivityDB { 8 | constructor (db) { 9 | // create levels 10 | this.globalActivityDB = sublevel(db, 'global-activity', { valueEncoding: 'json' }) 11 | } 12 | 13 | // getters 14 | // = 15 | 16 | listGlobalEvents (opts) { 17 | return new Promise((resolve, reject) => { 18 | collect(this.globalActivityDB.createReadStream(opts), (err, res) => { 19 | if (err) reject(err) 20 | else resolve(res.map(toNiceObj)) 21 | }) 22 | }) 23 | } 24 | } 25 | module.exports = ActivityDB 26 | 27 | // default user-record values 28 | ActivityDB.defaults = () => ({ 29 | ts: null, 30 | userid: null, 31 | username: null, 32 | action: null, 33 | params: {} 34 | }) 35 | 36 | // helper to convert {key:, value:} to just {values...} 37 | function toNiceObj (obj) { 38 | obj.value.key = obj.key 39 | return obj.value 40 | } 41 | -------------------------------------------------------------------------------- /lib/dbs/legacy-leveldb/archives.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events') 2 | var assert = require('assert') 3 | var sublevel = require('subleveldown') 4 | var collect = require('stream-collector') 5 | var through = require('through2') 6 | 7 | // exported api 8 | // = 9 | 10 | class ArchivesDB extends EventEmitter { 11 | constructor (db) { 12 | super() 13 | this.archivesDB = sublevel(db, 'archives', { valueEncoding: 'json' }) 14 | } 15 | 16 | // getters 17 | // = 18 | 19 | async getByKey (key) { 20 | assert(typeof key === 'string') 21 | try { 22 | return await this.archivesDB.get(key) 23 | } catch (e) { 24 | if (e.notFound) return null 25 | throw e 26 | } 27 | } 28 | 29 | list ({cursor, limit, reverse, sort, getExtra} = {}) { 30 | return new Promise((resolve, reject) => { 31 | var opts = {limit, reverse} 32 | // find indexes require a start- and end-point 33 | if (sort === 'createdAt') { 34 | if (reverse) { 35 | opts.lt = cursor || '\xff' 36 | opts.gte = 0 37 | } else { 38 | opts.gt = cursor || 0 39 | opts.lte = '\xff' 40 | } 41 | } else if (typeof cursor !== 'undefined') { 42 | // set cursor according to reverse 43 | if (reverse) opts.lt = cursor 44 | else opts.gt = cursor 45 | } 46 | // fetch according to sort 47 | var stream = this.archivesDB.createValueStream(opts) 48 | // "join" additional info 49 | if (getExtra) { 50 | stream = stream.pipe(through.obj(async (record, enc, cb) => { 51 | try { 52 | cb(null, await this.getByKey(record.key)) 53 | } catch (e) { 54 | cb(e) 55 | } 56 | })) 57 | } 58 | // collect into an array 59 | collect(stream, (err, res) => { 60 | if (err) reject(err) 61 | else resolve(res.filter(Boolean)) 62 | }) 63 | }) 64 | } 65 | } 66 | module.exports = ArchivesDB 67 | 68 | // default user-record values 69 | ArchivesDB.defaults = () => ({ 70 | key: null, 71 | 72 | hostingUsers: [], // NOTE currently just 1 entry is allowed 73 | 74 | // denormalized data 75 | name: false, // stored canonically in the hosting user record 76 | ownerName: '', // stored canonically in the hosting user record 77 | 78 | // stats 79 | diskUsage: undefined, 80 | numBlocks: 0, 81 | numDownloadedBlocks: 0, 82 | numBytes: 0, 83 | numFiles: 0, 84 | 85 | updatedAt: 0, 86 | createdAt: 0 87 | }) 88 | -------------------------------------------------------------------------------- /lib/dbs/legacy-leveldb/featured-archives.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const sublevel = require('subleveldown') 3 | 4 | // exported api 5 | // = 6 | 7 | class FeaturedArchivesDB { 8 | constructor (db) { 9 | this.featuredDB = sublevel(db, 'featured-archives', { valueEncoding: 'json' }) 10 | } 11 | 12 | // getters 13 | // = 14 | 15 | async has (key) { 16 | assert(typeof key === 'string') 17 | try { 18 | await this.featuredDB.get(key) 19 | return true // if it doesnt fail, the key exists 20 | } catch (e) { 21 | return false 22 | } 23 | } 24 | } 25 | module.exports = FeaturedArchivesDB 26 | -------------------------------------------------------------------------------- /lib/dbs/legacy-leveldb/reports.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events') 2 | var assert = require('assert') 3 | var sublevel = require('subleveldown') 4 | var collect = require('stream-collector') 5 | 6 | // exported api 7 | // = 8 | 9 | class ReportsDB extends EventEmitter { 10 | constructor (db) { 11 | super() 12 | this.reportsDB = sublevel(db, 'reports', { valueEncoding: 'json' }) 13 | } 14 | 15 | // getters 16 | // = 17 | 18 | list ({cursor, limit, reverse, sort} = {}) { 19 | return new Promise((resolve, reject) => { 20 | var opts = {limit, reverse} 21 | // find indexes require a start- and end-point 22 | if (sort && sort !== 'id') { 23 | if (reverse) { 24 | opts.lt = cursor || '\xff' 25 | opts.gte = '\x00' 26 | } else { 27 | opts.gt = cursor || '\x00' 28 | opts.lte = '\xff' 29 | } 30 | } else if (typeof cursor !== 'undefined') { 31 | // set cursor according to reverse 32 | if (reverse) opts.lt = cursor 33 | else opts.gt = cursor 34 | } 35 | // fetch according to sort 36 | var stream = this.reportsDB.createValueStream(opts) 37 | // collect into an array 38 | collect(stream, (err, res) => { 39 | if (err) reject(err) 40 | else resolve(res) 41 | }) 42 | }) 43 | } 44 | 45 | async getByID (id) { 46 | assert(typeof id === 'string') 47 | try { 48 | return await this.reportsDB.get(id) 49 | } catch (e) { 50 | if (e.notFound) return null 51 | throw e 52 | } 53 | } 54 | } 55 | 56 | module.exports = ReportsDB 57 | 58 | // default user-record values 59 | ReportsDB.defaults = () => ({ 60 | archiveKey: null, 61 | 62 | archiveOwner: null, 63 | reportingUser: null, 64 | 65 | reason: '', 66 | status: 'open', 67 | notes: '', 68 | 69 | createdAt: 0 70 | }) 71 | -------------------------------------------------------------------------------- /lib/dbs/legacy-leveldb/users.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var sublevel = require('subleveldown') 3 | var collect = require('stream-collector') 4 | var EventEmitter = require('events') 5 | 6 | // exported api 7 | // = 8 | 9 | class UsersDB extends EventEmitter { 10 | constructor (db) { 11 | super() 12 | this.accountsDB = sublevel(db, 'accounts', { valueEncoding: 'json' }) 13 | } 14 | 15 | // getters 16 | // = 17 | 18 | async getByID (id) { 19 | assert(typeof id === 'string') 20 | try { 21 | return await this.accountsDB.get(id) 22 | } catch (e) { 23 | if (e.notFound) return null 24 | throw e 25 | } 26 | } 27 | 28 | list ({cursor, limit, reverse, sort} = {}) { 29 | return new Promise((resolve, reject) => { 30 | var opts = {limit, reverse} 31 | // find indexes require a start- and end-point 32 | if (sort && sort !== 'id') { 33 | if (reverse) { 34 | opts.lt = cursor || '\xff' 35 | opts.gte = '\x00' 36 | } else { 37 | opts.gt = cursor || '\x00' 38 | opts.lte = '\xff' 39 | } 40 | } else if (typeof cursor !== 'undefined') { 41 | // set cursor according to reverse 42 | if (reverse) opts.lt = cursor 43 | else opts.gt = cursor 44 | } 45 | // fetch according to sort 46 | var stream = this.accountsDB.createValueStream(opts) 47 | // collect into an array 48 | collect(stream, (err, res) => { 49 | if (err) reject(err) 50 | else resolve(res) 51 | }) 52 | }) 53 | } 54 | 55 | createValueStream (opts) { 56 | return this.accountsDB.createValueStream(opts) 57 | } 58 | } 59 | module.exports = UsersDB 60 | 61 | // default user-record values 62 | UsersDB.defaults = () => ({ 63 | username: null, 64 | passwordHash: null, 65 | passwordSalt: null, 66 | 67 | email: null, 68 | profileURL: null, 69 | scopes: [], 70 | suspension: null, 71 | archives: [], 72 | updatedAt: 0, 73 | createdAt: 0, 74 | 75 | plan: 'basic', 76 | diskUsage: 0, 77 | 78 | diskQuota: null, 79 | namedArchiveQuota: undefined, 80 | 81 | isEmailVerified: false, 82 | emailVerifyNonce: null, 83 | 84 | forgotPasswordNonce: null, 85 | 86 | isProfileDatVerified: false, 87 | profileVerifyToken: null, 88 | 89 | stripeCustomerId: null, 90 | stripeSubscriptionId: null, 91 | stripeTokenId: null, 92 | stripeCardId: null, 93 | stripeCardBrand: null, 94 | stripeCardCountry: null, 95 | stripeCardCVCCheck: null, 96 | stripeCardExpMonth: null, 97 | stripeCardExpYear: null, 98 | stripeCardLast4: null 99 | }) 100 | 101 | // default user-record archive values 102 | UsersDB.archiveDefaults = () => ({ 103 | key: null, 104 | name: null 105 | }) 106 | -------------------------------------------------------------------------------- /lib/dbs/reports.js: -------------------------------------------------------------------------------- 1 | var Base = require('./base') 2 | var assert = require('assert') 3 | var SQL = require('sql-template-strings') 4 | 5 | // exported api 6 | // = 7 | 8 | class ReportsDB extends Base { 9 | constructor (cloud) { 10 | super(cloud, 'reports', 'id') 11 | this.sqlite = cloud.db 12 | } 13 | 14 | // basic ops 15 | // = 16 | 17 | async create (record) { 18 | assert(record && typeof record === 'object') 19 | assert(typeof record.archiveKey === 'string') 20 | assert(typeof record.archiveOwner === 'string') 21 | assert(typeof record.reportingUser === 'string') 22 | assert(typeof record.reason === 'string') 23 | let {archiveKey, archiveOwner, reportingUser, reason} = record 24 | await this.sqlite.run(SQL` 25 | INSERT INTO reports 26 | (archiveKey, archiveOwner, reportingUser, reason, createdAt, updatedAt) 27 | VALUES 28 | (${archiveKey}, ${archiveOwner}, ${reportingUser}, ${reason}, ${Date.now()}, ${Date.now()}) 29 | `) 30 | this.emit('create', record) 31 | return record 32 | } 33 | 34 | async put (record) { 35 | assert(record && typeof record === 'object') 36 | assert(typeof record.id === 'number') 37 | record.updatedAt = Date.now() 38 | await this.sqlite.run(this.createUpdateQuery(record)) 39 | this.emit('put', record) 40 | } 41 | 42 | async del (record) { 43 | assert(record && typeof record === 'object') 44 | assert(typeof record.id === 'string') 45 | await this.sqlite.run(SQL`DELETE FROM reports WHERE id = ${record.id}`) 46 | this.emit('del', record) 47 | } 48 | 49 | // getters 50 | 51 | async getByID (id) { 52 | assert(typeof id === 'string') 53 | return this.sqlite.get(SQL`SELECT * FROM reports WHERE id = ${id}`) 54 | } 55 | 56 | async getByArchiveKey (key) { 57 | assert(typeof key === 'string') 58 | return this.sqlite.all(SQL`SELECT * FROM reports WHERE archiveKey = ${key}`) 59 | } 60 | 61 | async getByArchiveOwner (id) { 62 | assert(typeof id === 'string') 63 | return this.sqlite.all(SQL`SELECT * FROM reports WHERE archiveOwner = ${id}`) 64 | } 65 | 66 | async getByReportingUser (id) { 67 | assert(typeof id === 'string') 68 | return this.sqlite.all(SQL`SELECT * FROM reports WHERE reportingUser = ${id}`) 69 | } 70 | 71 | list ({cursor, limit, reverse, sort} = {}) { 72 | // construct query 73 | var query = SQL`SELECT * FROM reports` 74 | sort = sort || 'id' 75 | if (cursor) { 76 | query.append(` WHERE ${sort} `) 77 | if (reverse) query.append(SQL`< ${cursor}`) 78 | else query.append(SQL`> ${cursor}`) 79 | } 80 | query.append(` ORDER BY ${sort}`) 81 | if (reverse) query.append(SQL` DESC`) 82 | if (limit) query.append(SQL` LIMIT ${limit}`) 83 | 84 | // run query 85 | return this.sqlite.all(query) 86 | } 87 | } 88 | 89 | module.exports = ReportsDB 90 | -------------------------------------------------------------------------------- /lib/dbs/schemas.js: -------------------------------------------------------------------------------- 1 | const LegacyLeveldb = require('./legacy-leveldb') 2 | 3 | // exported api 4 | // = 5 | 6 | class Schemas { 7 | constructor (cloud) { 8 | this.cloud = cloud 9 | } 10 | 11 | async runCorrections () { 12 | // none currently 13 | } 14 | 15 | async migrate () { 16 | await this.cloud.db.migrate({migrationsPath: './lib/dbs/migrations'}) 17 | await LegacyLeveldb.migrateAsNeeded(this.cloud, this.cloud.config.dir) 18 | } 19 | } 20 | module.exports = Schemas 21 | -------------------------------------------------------------------------------- /lib/lock.js: -------------------------------------------------------------------------------- 1 | var AwaitLock = require('await-lock') 2 | 3 | // wraps await-lock in a simpler interface, with many possible locks 4 | // usage: 5 | /* 6 | var lock = require('./lock') 7 | async function foo () { 8 | var release = await lock('bar') 9 | // ... 10 | release() 11 | } 12 | */ 13 | 14 | var locks = {} 15 | module.exports = async function (key) { 16 | if (!(key in locks)) locks[key] = new AwaitLock() 17 | 18 | var lock = locks[key] 19 | await lock.acquireAsync() 20 | return lock.release.bind(lock) 21 | } 22 | -------------------------------------------------------------------------------- /lib/mailer.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var nodemailer = require('nodemailer') 3 | var templates = { 4 | verification: require('./templates/mail/verification'), 5 | 'forgot-password': require('./templates/mail/forgot-password'), 6 | 'verify-update-email': require('./templates/mail/verify-update-email'), 7 | 'support': require('./templates/mail/support') 8 | } 9 | 10 | // exported api 11 | // = 12 | 13 | module.exports = class Mailer { 14 | constructor (config) { 15 | this.hostname = config.hostname 16 | this.brandname = config.brandname 17 | this.sender = config.email.sender 18 | this._mailer = nodemailer.createTransport(config.email) 19 | } 20 | 21 | async send (tmpl, params) { 22 | assert(params.email) 23 | params = Object.assign({}, params, this) 24 | tmpl = templates[tmpl] 25 | try { 26 | return await this._mailer.sendMail({ 27 | from: this.sender, 28 | to: params.email, 29 | subject: tmpl.subject(params), 30 | text: tmpl.text(params), 31 | html: tmpl.html(params) 32 | }) 33 | } catch (err) { 34 | this.logError(err, tmpl, params) 35 | throw err 36 | } 37 | } 38 | 39 | get transport () { 40 | return this._mailer.transporter 41 | } 42 | 43 | logError (err, tmpl, params) { 44 | console.error('[ERROR] Failed to send email', tmpl, 'To:', params.email, 'Error:', err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/proofs.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var jwt = require('jsonwebtoken') 3 | 4 | // exported api 5 | // = 6 | 7 | module.exports = class Proofs { 8 | constructor (config) { 9 | this.secret = config.proofs.secret 10 | this.options = config.proofs 11 | delete this.options.secret 12 | assert(this.secret, 'config.proofs.secret is required') 13 | assert(this.options.algorithm, 'config.sessions.algorithm is required') 14 | } 15 | 16 | verify (token) { 17 | try { 18 | // return decoded session or null on failure 19 | return jwt.verify(token, this.secret, { algorithms: [this.options.algorithm] }) 20 | } catch (e) { 21 | return null 22 | } 23 | } 24 | 25 | generate (userRecord) { 26 | return jwt.sign( 27 | { 28 | id: userRecord.id, 29 | profileURL: userRecord.profileURL 30 | }, 31 | this.secret, 32 | { algorithm: this.options.algorithm } 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/sanitizers.js: -------------------------------------------------------------------------------- 1 | const bytes = require('bytes') 2 | const { DAT_KEY_REGEX } = require('./const') 3 | 4 | exports.toDatDomain = value => { 5 | return 'dat://' + DAT_KEY_REGEX.exec(value)[1] + '/' 6 | } 7 | 8 | exports.toBytes = value => { 9 | return bytes.parse(value) 10 | } 11 | 12 | exports.toLowerCase = value => value.toLowerCase() 13 | -------------------------------------------------------------------------------- /lib/sessions.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const jwt = require('jsonwebtoken') 3 | const wrap = require('co-express') 4 | 5 | // TODO s/sessionAccount/sessionUser/ 6 | 7 | // exported api 8 | // = 9 | 10 | module.exports = class Sessions { 11 | constructor (cloud) { 12 | this.config = cloud.config 13 | this.options = cloud.config.sessions 14 | this.secret = cloud.config.sessions.secret 15 | this.usersDB = cloud.usersDB 16 | delete this.options.secret 17 | assert(this.secret, 'config.sessions.secret is required') 18 | assert(this.options.algorithm, 'config.sessions.algorithm is required') 19 | } 20 | 21 | middleware () { 22 | return wrap(async (req, res, next) => { 23 | res.locals.sessionAlerts = [] 24 | 25 | // pull token out of auth or cookie header 26 | var authHeader = req.header('authorization') 27 | if (authHeader && authHeader.indexOf('Bearer') > -1) { 28 | res.locals.session = this.verify(authHeader.slice('Bearer '.length)) 29 | } else if (req.cookies && req.cookies.sess) { 30 | res.locals.session = this.verify(req.cookies.sess) 31 | } 32 | 33 | // fetch user record if there's a session 34 | if (res.locals.session) { 35 | var sessionUser = await this.usersDB.getByID(res.locals.session.id) 36 | if (!sessionUser) { 37 | res.locals.session = null 38 | return next() 39 | } 40 | res.locals.sessionUser = sessionUser 41 | 42 | // add any alerts 43 | var pct = this.config.getUserDiskQuotaPct(sessionUser) 44 | if (pct > 1) { 45 | res.locals.sessionAlerts.push({ 46 | type: 'warning', 47 | message: 'You are out of disk space!', 48 | details: 'Click here to review your account.', 49 | href: '/account' 50 | }) 51 | } else if (pct > 0.9) { 52 | res.locals.sessionAlerts.push({ 53 | type: '', 54 | message: 'You are almost out of disk space!', 55 | details: 'Click here to review your account.', 56 | href: '/account' 57 | }) 58 | } 59 | } 60 | next() 61 | }) 62 | } 63 | 64 | verify (token) { 65 | try { 66 | // return decoded session or null on failure 67 | return jwt.verify(token, this.secret, { algorithms: [this.options.algorithm] }) 68 | } catch (e) { 69 | return null 70 | } 71 | } 72 | 73 | generate (userRecord) { 74 | return jwt.sign( 75 | { 76 | id: userRecord.id, 77 | scopes: userRecord.scopes 78 | }, 79 | this.secret, 80 | this.options 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/templates/mail/forgot-password.js: -------------------------------------------------------------------------------- 1 | exports.subject = function () { 2 | return 'Forgotten password reset' 3 | } 4 | 5 | exports.text = function (params) { 6 | return ` 7 | \n 8 | **Forgotten password reset for ${params.username}.**\n 9 | \n 10 | We received a request at ${params.hostname} to reset your password.\n 11 | \n 12 | If this was you, follow this link:\n 13 | \n 14 | ${params.forgotPasswordLink}\n 15 | \n 16 | If you did not request to reset your password, please ignore this email.\n 17 | \n 18 | \n 19 | ` 20 | } 21 | 22 | exports.html = function (params) { 23 | return ` 24 |

    Forgotten password reset for ${params.username}.

    25 |

    We received a request at ${params.hostname} to reset your password. If this was you, follow this link:

    26 |

    ${params.forgotPasswordLink}

    27 |

    If you did not request to reset your password, please ignore this email.

    28 | ` 29 | } 30 | -------------------------------------------------------------------------------- /lib/templates/mail/support.js: -------------------------------------------------------------------------------- 1 | exports.subject = function (params) { 2 | return params.subject 3 | } 4 | 5 | exports.text = function (params) { 6 | return ` 7 | ${params.username},\n 8 | \n 9 | ${params.message}\n 10 | \n 11 | Thanks,\n 12 | The ${params.brandname} team 13 | ` 14 | } 15 | 16 | exports.html = function (params) { 17 | return ` 18 |

    ${params.username},

    19 |

    ${params.message}

    20 |

    Thanks,
    The ${params.brandname} team

    21 | ` 22 | } 23 | -------------------------------------------------------------------------------- /lib/templates/mail/verification.js: -------------------------------------------------------------------------------- 1 | exports.subject = function () { 2 | return 'Verify your email address' 3 | } 4 | 5 | exports.text = function (params) { 6 | return ` 7 | \n 8 | Welcome, ${params.username}, to ${params.brandname}.\n 9 | \n 10 | To verify your account, follow this link:\n 11 | \n 12 | ${params.emailVerificationLink}\n 13 | \n 14 | \n 15 | ` 16 | } 17 | 18 | exports.html = function (params) { 19 | return ` 20 |

    Welcome, ${params.username}, to ${params.brandname}.

    21 |

    To verify your account, follow this link:

    22 |

    ${params.emailVerificationLink}

    23 | ` 24 | } 25 | -------------------------------------------------------------------------------- /lib/templates/mail/verify-update-email.js: -------------------------------------------------------------------------------- 1 | exports.subject = function () { 2 | return 'Verify your email address' 3 | } 4 | 5 | exports.text = function (params) { 6 | return ` 7 | \n 8 | Hi ${params.username},\n 9 | \n 10 | You requested to change the email address assocatied with your account at ${params.brandname}. 11 | \n 12 | To verify this change, follow this link:\n 13 | \n 14 | ${params.emailVerificationLink}\n 15 | \n 16 | \n 17 | ` 18 | } 19 | 20 | exports.html = function (params) { 21 | return ` 22 |

    Hi, ${params.username}.

    23 |

    You requested to change the email address associated with your account at ${params.brandname}.

    24 |

    To verify this change, follow this link:

    25 |

    ${params.emailVerificationLink}

    26 | ` 27 | } 28 | -------------------------------------------------------------------------------- /lib/validators.js: -------------------------------------------------------------------------------- 1 | const bytes = require('bytes') 2 | const { DAT_URL_REGEX, DAT_KEY_REGEX, DAT_NAME_REGEX } = require('./const') 3 | 4 | exports.isDatURL = value => { 5 | return DAT_URL_REGEX.test(value) 6 | } 7 | 8 | exports.isDatHash = value => { 9 | return value.length === 64 && DAT_KEY_REGEX.test(value) 10 | } 11 | 12 | exports.isDatHashOrURL = value => { 13 | return exports.isDatURL(value) || exports.isDatHash(value) 14 | } 15 | 16 | exports.isDatName = value => { 17 | return DAT_NAME_REGEX.test(value) 18 | } 19 | 20 | exports.isScopesArray = value => { 21 | return Array.isArray(value) && value.filter(v => typeof v !== 'string').length === 0 22 | } 23 | 24 | exports.isSimpleEmail = value => { 25 | return typeof value === 'string' && value.indexOf('+') === -1 26 | } 27 | 28 | exports.isPassword = value => { 29 | return typeof value === 'string' && value.length >= 6 && value.length <= 100 30 | } 31 | 32 | exports.isBytes = value => { 33 | return value === null || typeof value === 'number' || !!bytes(value) 34 | } 35 | -------------------------------------------------------------------------------- /pm2.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const {hostname} = require('os') 3 | const env = 'production' 4 | process.env.NODE_ENV = env 5 | const config = require('./lib/config') 6 | 7 | module.exports = { 8 | apps: [{ 9 | script: './bin.js', 10 | name: 'hashbase', 11 | combine_logs: false, 12 | pid_file: `${config.dir}/hashbase.pid`, 13 | out_file: `${config.dir}/logs/hashbase_${hostname()}_out.log`, 14 | err_file: `${config.dir}/logs/hashbase_${hostname()}_err.log`, 15 | max_memory_restart: '2G', 16 | env: { 17 | NODE_ENV: env 18 | } 19 | }] 20 | } 21 | -------------------------------------------------------------------------------- /test/lib/dat.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const rimraf = require('rimraf') 3 | const Dat = require('dat-node') 4 | const util = require('./util') 5 | 6 | exports.makeDatFromFolder = function (dir, cb) { 7 | rimraf.sync(path.join(dir, '.dat')) 8 | Dat(dir, (err, dat) => { 9 | if (err) return cb(err) 10 | 11 | dat.archive.on('error', console.log) 12 | dat.importFiles(() => { 13 | dat.joinNetwork() 14 | 15 | var key = dat.key.toString('hex') 16 | console.log('created dat', key, 'from', dir) 17 | cb(null, dat, key) 18 | }) 19 | }) 20 | } 21 | 22 | exports.downloadDatFromSwarm = function (key, { timeout = 5e3 }, cb) { 23 | var dir = util.mktmpdir() 24 | Dat(dir, {key}, (err, dat) => { 25 | if (err) return cb(err) 26 | 27 | dat.archive.on('error', console.log) 28 | 29 | dat.joinNetwork() 30 | dat.network.once('connection', (...args) => { 31 | console.log('got connection') 32 | }) 33 | 34 | dat.archive.metadata.on('download', (index, block) => { 35 | console.log('meta download event', index) 36 | }) 37 | 38 | var to = setTimeout(() => cb(new Error('timed out waiting for download')), timeout) 39 | dat.archive.metadata.on('sync', () => { 40 | console.log('meta download finished') 41 | }) 42 | dat.archive.once('content', () => { 43 | console.log('opened') 44 | dat.archive.content.on('download', (index, block) => { 45 | console.log('content download event', index) 46 | }) 47 | dat.archive.content.on('sync', () => { 48 | console.log('content download finished') 49 | clearTimeout(to) 50 | dat.close() 51 | cb(null, dat, key) 52 | }) 53 | }) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /test/lib/server.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise-native') 2 | const createApp = require('../../index.js') 3 | const util = require('./util') 4 | 5 | var portCounter = 10000 6 | 7 | module.exports = function (cb) { 8 | if (process.env.REMOTE_URL) { 9 | return createRemoteApp(cb) 10 | } else { 11 | return createLocalApp(cb) 12 | } 13 | } 14 | 15 | function createRemoteApp (cb) { 16 | var url = process.env.REMOTE_URL 17 | console.log(`connecting to ${url}`) 18 | var app = { 19 | url, 20 | isRemote: true, 21 | req: request.defaults({ baseUrl: url, timeout: 10e3, resolveWithFullResponse: true, simple: false }), 22 | close: cb => cb() 23 | } 24 | cb(null, app) 25 | } 26 | 27 | async function createLocalApp (cb) { 28 | // setup config 29 | // = 30 | 31 | var tmpdir = util.mktmpdir() 32 | var config = { 33 | hostname: 'test.local', 34 | dir: tmpdir, 35 | port: portCounter++, 36 | defaultDiskUsageLimit: '100mb', 37 | defaultNamedArchivesLimit: 3, 38 | bandwidthLimit: {up: '600kb', down: '600kb'}, 39 | pm2: false, 40 | admin: { 41 | password: 'foobar' 42 | }, 43 | jobs: { 44 | popularArchivesIndex: '15m', 45 | userDiskUsage: '30m', 46 | deleteDeadArchives: '30m' 47 | }, 48 | registration: { 49 | open: true, 50 | reservedNames: ['reserved', 'blacklisted'] 51 | }, 52 | email: { 53 | transport: 'mock', 54 | sender: '"Test Server" ' 55 | }, 56 | cache: { 57 | metadataStorage: 65536, 58 | contentStorage: 65536, 59 | tree: 65536 60 | }, 61 | sessions: { 62 | algorithm: 'HS256', 63 | secret: 'super secret', 64 | expiresIn: '1h' 65 | }, 66 | proofs: { 67 | algorithm: 'HS256', 68 | secret: 'super secret 2' 69 | }, 70 | stripe: { 71 | secretKey: 'foo', 72 | publishableKey: 'bar' 73 | } 74 | } 75 | 76 | // create server 77 | // = 78 | 79 | var app = await createApp(config) 80 | var server = app.listen(config.port, (err) => { 81 | if (!err) console.log(`server started on http://127.0.0.1:${config.port}`) 82 | cb(err, app) 83 | }) 84 | 85 | app.isRemote = false 86 | app.url = `http://127.0.0.1:${config.port}` 87 | app.req = request.defaults({ 88 | baseUrl: app.url, 89 | resolveWithFullResponse: true, 90 | simple: false 91 | }) 92 | 93 | // wrap app.close to stop the server 94 | var orgClose = app.close 95 | app.close = cb => orgClose.call(app, () => server.close(cb)) 96 | 97 | return app 98 | } 99 | -------------------------------------------------------------------------------- /test/lib/util.js: -------------------------------------------------------------------------------- 1 | const os = require('os') 2 | const path = require('path') 3 | const fs = require('fs') 4 | 5 | exports.mktmpdir = function () { 6 | if (fs.mkdtempSync) { 7 | return fs.mkdtempSync(os.tmpdir() + path.sep + 'hypercloud-test-') 8 | } 9 | var p = (os.tmpdir() + path.sep + 'beaker-test-' + Date.now()) 10 | fs.mkdirSync(p) 11 | return p 12 | } 13 | -------------------------------------------------------------------------------- /test/scaffold/testdat1/dat.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Test Dat 1", 3 | "description": "The first test dat" 4 | } -------------------------------------------------------------------------------- /test/scaffold/testdat1/hello.txt: -------------------------------------------------------------------------------- 1 | world --------------------------------------------------------------------------------