├── .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 |
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 |
13 |
Your plan is cancelled
14 |
15 |
You can upgrade at any point in the future.
16 |
17 |
21 |
22 |
23 |
Problem? Contact Support
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 |
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 |
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 |
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 |
13 |
You've upgraded to Pro!
14 |
15 | You will receive a receipt via email.
16 |
17 |
18 |
23 |
24 |
25 |
Problem? Contact Support
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 | Key
29 | Name
30 | Owner
31 | Disk Usage
32 | Total Size
33 | Upload date
34 |
35 |
36 |
37 |
38 | Key
39 | Name
40 | Owner
41 | Disk Usage
42 | Total Size
43 | Upload date
44 |
45 |
46 |
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 | Save
41 |
42 |
43 | <% if (record.status === 'open') { %>
44 | Close report
45 | <% } else { %>
46 | Re-open this report
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 | Archive
29 | Archive owner
30 | Reported by
31 | Reason
32 | Reported at
33 | Notes
34 | Status
35 |
36 |
37 |
38 |
39 | Archive
40 | Archive owner
41 | Reported by
42 | Reason
43 | Reported at
44 | Notes
45 | Status
46 |
47 |
48 |
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 |
Save
32 |
33 |
34 | <% if (user.suspension) { %>
35 | Unsuspend
36 | <% } else { %>
37 | Suspend
38 | <% } %>
39 |
40 |
41 |
42 | <% if (!user.isEmailVerified) { %>
43 | Resend confirmation email
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 | ID
29 | Username
30 | Email
31 | Archives
32 | Usage
33 | Quota
34 | Plan
35 | Verified?
36 | Suspended?
37 | Join Date
38 |
39 |
40 |
41 |
42 | ID
43 | Username
44 | Email
45 | Archives
46 | Usage
47 | Quota
48 | Plan
49 | Verified?
50 | Suspended?
51 | Join Date
52 |
53 |
54 |
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 | Event
5 | URL
6 | User
7 | Session
8 | IP
9 | User Type
10 | Browser
11 | Version
12 | OS
13 | Date
14 |
15 |
16 |
17 |
18 | Event
19 | URL
20 | User
21 | Session
22 | IP
23 | User Type
24 | Browser
25 | Version
26 | OS
27 | Date
28 |
29 |
30 |
--------------------------------------------------------------------------------
/assets/html/com/activity.html:
--------------------------------------------------------------------------------
1 |
2 | <% activity.forEach(event => { %>
3 |
4 |
5 | <%= event.username %>
6 |
7 |
8 | <% if (event.action === 'add-archive') { %>
9 | added
10 | <% } else if (event.action === 'update-archive') { %>
11 | updated
12 | <% } else if (event.action === 'del-archive') { %>
13 | removed
14 | <% } %>
15 |
16 | <% var name = event.params.name || (event.params.key.slice(0, 6) + '..' + event.params.key.slice(-2)) %>
17 |
18 | <%= name %>
19 |
20 |
21 |
22 | <% var date = nicedate(event.ts) %>
23 | <%= date %>
24 | <% if (date !== 'yesterday' && date !== 'just now') { %>
25 | ago
26 | <% } %>
27 |
28 | <% }) %>
29 |
30 |
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 |
--------------------------------------------------------------------------------
/assets/html/com/featured-content.html:
--------------------------------------------------------------------------------
1 |
2 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
35 |
36 |
--------------------------------------------------------------------------------
/assets/html/com/mobile-nav.html:
--------------------------------------------------------------------------------
1 |
2 | <% if (sessionUser) { %>
3 |
4 |
5 |
6 | <% } %>
7 |
8 |
9 |
10 |
11 |
12 |
47 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
<%= error.message || error.body.message %>
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 |
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 |
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 |
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 |
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 |
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 |
13 |
14 | We've sent a confirmation email to <%= email %> .
15 |
16 |
17 |
18 | No email? Contact Support
19 |
20 |
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 |
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 |
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 |
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 |
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 |
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
--------------------------------------------------------------------------------