├── .dockerignore ├── .env.example ├── .github ├── FUNDING.yml └── workflows │ ├── commitlint.yml │ ├── deploy-prod.yml │ ├── deploy-stage.yml │ ├── standard.yml │ └── unit.yml ├── .gitignore ├── .husky └── commit-msg ├── .mocharc.js ├── .nvmrc ├── Dockerfile ├── LICENSE ├── README.md ├── background └── Dockerfile ├── commitlint.config.js ├── cookies-keys.json.example ├── copyAudioFiles.sh ├── docker-compose.test.yml ├── docker-compose.yml ├── emails ├── earnings-report │ ├── html.pug │ ├── subject.pug │ └── text.pug ├── email-confirmation │ ├── html.pug │ ├── subject.pug │ └── text.pug ├── new-upload │ ├── html.pug │ ├── subject.pug │ └── text.pug ├── new-user │ ├── html.pug │ ├── subject.pug │ └── text.pug ├── password-reset │ ├── html.pug │ ├── subject.pug │ └── text.pug ├── style.css └── test │ ├── html.pug │ ├── subject.pug │ └── text.pug ├── jwk-keys.json.example ├── nginx ├── Dockerfile ├── README.md ├── dev.resonate.coop.conf ├── local.dev.conf └── prod.resonate.coop.conf ├── nodemon.json ├── package.json ├── server.mjs ├── src ├── auth │ ├── README.md │ ├── api-doc.js │ ├── apiDocs.js │ ├── configuration.js │ ├── index.js │ ├── public │ │ ├── fonts │ │ │ ├── Graphik-Regular.woff │ │ │ ├── Graphik-Regular.woff2 │ │ │ ├── Graphik-Semibold.woff │ │ │ └── Graphik-Semibold.woff2 │ │ └── style.css │ ├── redis-adapter.js │ ├── routes.js │ ├── utils.js │ └── views │ │ ├── account.pug │ │ ├── email-confirmed.pug │ │ ├── interaction-login.pug │ │ ├── interaction.pug │ │ ├── layout.pug │ │ ├── logout.pug │ │ ├── password-reset-email-sent.pug │ │ ├── password-reset-request.pug │ │ ├── password-reset-success.pug │ │ ├── password-reset.pug │ │ ├── registration-success.pug │ │ ├── registration.pug │ │ └── root-page.pug ├── config │ ├── cache.js │ ├── compression.js │ ├── cors.js │ ├── databases.js │ ├── error.js │ ├── file-status-list.js │ ├── grant.js │ ├── redis.js │ ├── session.js │ ├── sharp.js │ ├── supported-media-types.js │ └── swagger.js ├── constants.js ├── controllers │ ├── api-doc.js │ ├── apiDocs.js │ ├── artists │ │ ├── artistService.js │ │ └── routes │ │ │ ├── featured.js │ │ │ ├── index.js │ │ │ ├── updated.mjs │ │ │ └── {id} │ │ │ ├── index.js │ │ │ ├── releases.js │ │ │ └── tracks │ │ │ ├── index.js │ │ │ └── top.js │ ├── index.mjs │ ├── labels │ │ └── routes │ │ │ ├── index.js │ │ │ └── {id} │ │ │ ├── artists.js │ │ │ ├── index.js │ │ │ └── releases.js │ ├── playlists │ │ ├── routes │ │ │ ├── index.js │ │ │ └── {id}.js │ │ └── services │ │ │ └── playlistService.js │ ├── resolve │ │ └── routes │ │ │ └── index.js │ ├── search │ │ └── routes │ │ │ └── index.js │ ├── stream │ │ ├── audio.{id}.{segment}.mjs │ │ └── index.mjs │ ├── tag │ │ └── routes │ │ │ └── {tag}.js │ ├── trackgroups │ │ ├── routes │ │ │ ├── index.js │ │ │ └── {id}.js │ │ ├── schemas │ │ │ └── index.js │ │ └── services │ │ │ └── trackgroupService.js │ ├── tracks │ │ ├── routes │ │ │ ├── index.js │ │ │ └── {id}.js │ │ └── services │ │ │ └── trackService.js │ ├── user │ │ ├── admin │ │ │ ├── earnings.js │ │ │ ├── files │ │ │ │ ├── index.mjs │ │ │ │ └── {id}.mjs │ │ │ ├── playlists │ │ │ │ ├── index.js │ │ │ │ └── {id} │ │ │ │ │ ├── index.js │ │ │ │ │ └── items │ │ │ │ │ ├── add.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── remove.js │ │ │ ├── plays.mjs │ │ │ ├── queues.js │ │ │ ├── trackgroups │ │ │ │ ├── index.js │ │ │ │ └── {id} │ │ │ │ │ ├── index.js │ │ │ │ │ └── items │ │ │ │ │ ├── add.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── remove.js │ │ │ ├── tracks │ │ │ │ ├── index.js │ │ │ │ └── {id}.js │ │ │ └── users │ │ │ │ ├── index.js │ │ │ │ └── {id}.js │ │ ├── artists │ │ │ └── routes │ │ │ │ ├── index.js │ │ │ │ └── {id} │ │ │ │ ├── image.mjs │ │ │ │ └── index.js │ │ ├── authenticate.js │ │ ├── collection │ │ │ └── routes │ │ │ │ └── index.js │ │ ├── earnings.js │ │ ├── favorites │ │ │ └── routes │ │ │ │ ├── index.js │ │ │ │ └── resolve.js │ │ ├── files │ │ │ ├── index.mjs │ │ │ └── {id}.mjs │ │ ├── playlists │ │ │ └── routes │ │ │ │ ├── index.js │ │ │ │ └── {id} │ │ │ │ ├── cover.js │ │ │ │ ├── index.js │ │ │ │ ├── items │ │ │ │ ├── add.js │ │ │ │ ├── index.js │ │ │ │ └── remove.js │ │ │ │ └── privacy.js │ │ ├── plays │ │ │ └── routes │ │ │ │ ├── buy.js │ │ │ │ ├── history │ │ │ │ ├── artists.mjs │ │ │ │ └── tracks.js │ │ │ │ ├── index.mjs │ │ │ │ ├── resolve.js │ │ │ │ ├── spendings.js │ │ │ │ └── stats.js │ │ ├── products │ │ │ └── routes │ │ │ │ ├── cancel.js │ │ │ │ ├── checkout.js │ │ │ │ ├── index.js │ │ │ │ └── success.js │ │ ├── profile │ │ │ └── routes │ │ │ │ └── index.js │ │ ├── profileRedirect.js │ │ ├── stream │ │ │ └── routes │ │ │ │ ├── audio.{id}.{segment}.mjs │ │ │ │ └── {id}.js │ │ ├── stream_legacy.js │ │ ├── trackgroups │ │ │ └── routes │ │ │ │ ├── index.js │ │ │ │ └── {id} │ │ │ │ ├── cover.js │ │ │ │ ├── index.js │ │ │ │ ├── items │ │ │ │ ├── add.js │ │ │ │ ├── index.js │ │ │ │ └── remove.js │ │ │ │ ├── privacy.js │ │ │ │ └── settings.js │ │ └── tracks │ │ │ └── routes │ │ │ ├── index.js │ │ │ └── {id} │ │ │ ├── file.js │ │ │ └── index.js │ ├── users │ │ └── routes │ │ │ └── {id} │ │ │ ├── index.js │ │ │ └── playlists.js │ └── webhooks │ │ └── stripe_checkout_success.js ├── db │ ├── README.md │ ├── legacy │ │ ├── README.md │ │ ├── mysql-migrations.js │ │ ├── pg-migrations.js │ │ ├── tunnel.js │ │ └── user-api-migration.js │ ├── migrations │ │ ├── 20191017210551-playlist.js │ │ ├── 20191017210551-track_group.js │ │ ├── 20191017210557-playlist_item.js │ │ ├── 20191017210557-track_group_item.js │ │ ├── 20191023192727-file.js │ │ ├── 20200214140232-play.js │ │ ├── 202207140053-address.js │ │ ├── 202207140053-client.js │ │ ├── 202207140053-favorite.js │ │ ├── 202207140053-gf-form.js │ │ ├── 202207140053-image.js │ │ ├── 202207140053-links.js │ │ ├── 202207140053-membership_class.js │ │ ├── 202207140053-order.js │ │ ├── 202207140053-role.js │ │ ├── 202207140053-share_transaction.js │ │ ├── 202207140053-tag.js │ │ ├── 202207140053-track.js │ │ ├── 202207140053-user.js │ │ ├── 202207140053-userMeta.js │ │ ├── 202207140053-user_group.js │ │ ├── 202207140053-user_group_link.js │ │ ├── 202207140053-user_group_member.js │ │ ├── 202207140053-user_group_types.js │ │ ├── 202207140053-user_membership.js │ │ ├── 2022071410053-credit.js │ │ ├── 20221107-drop-usermeta.js │ │ ├── 20221107-gf-form.js │ │ ├── 20221107-share_transaction_legacyId.js │ │ ├── 20221107-user_membership_legacyId.js │ │ ├── 20221108-add-email-confirmation-expiration.js │ │ ├── 20221226-add-track-hls.js │ │ ├── 20221228-create-table-user-ledger-entry.js │ │ ├── 20221231-create-table-user-track-purchase.js │ │ └── 20221231-create-table-user-trackgroup-purchase.js │ ├── models │ │ ├── index.js │ │ └── resonate │ │ │ ├── address.js │ │ │ ├── client.js │ │ │ ├── credit.js │ │ │ ├── favorite.js │ │ │ ├── file.js │ │ │ ├── image.js │ │ │ ├── link.js │ │ │ ├── membership_class.js │ │ │ ├── order.js │ │ │ ├── play.js │ │ │ ├── playlist.js │ │ │ ├── playlist_item.js │ │ │ ├── role.js │ │ │ ├── share_transaction.js │ │ │ ├── tag.js │ │ │ ├── track.js │ │ │ ├── track_group.js │ │ │ ├── track_group_item.js │ │ │ ├── user.js │ │ │ ├── user_group.js │ │ │ ├── user_group_link.js │ │ │ ├── user_group_member.js │ │ │ ├── user_group_type.js │ │ │ ├── user_ledger_entry.js │ │ │ ├── user_membership.js │ │ │ ├── user_track_group_purchase.js │ │ │ └── user_track_purchase.js │ └── seeders │ │ ├── clients-seeder.js │ │ ├── immutables-seeder.js │ │ ├── test │ │ ├── 01-clients-seeder.js │ │ ├── 02-users-seeder.js │ │ ├── 03-immutables-seeder.js │ │ ├── 04-trackgroup-seeder.js │ │ └── 05-files-seeder.js │ │ ├── trackgroup-seeder.js │ │ └── users-seeder.js ├── index.mjs ├── jobs │ ├── audio-duration.js │ ├── cleanup.js │ ├── convert-audio.js │ ├── convert-image.js │ ├── create-report.js │ ├── optimize-image.js │ ├── prepare-download.js │ ├── queue-worker.js │ ├── reprocess-track.js │ ├── send-mail.js │ └── upload-b2.js ├── schemas │ ├── trackgroup.js │ └── tracks.js ├── scripts │ ├── README.md │ └── reports │ │ ├── create-report.js │ │ ├── earnings.js │ │ ├── index.js │ │ └── plays.js └── util │ ├── cover-src.js │ ├── dev.js │ ├── ffprobe-metadata.js │ ├── gen-silent-audio.js │ ├── get-audio-duration.js │ ├── links.js │ ├── logger.js │ ├── process-file.js │ ├── profile-image.js │ └── query.js ├── test ├── -- best album ever.pgsql ├── HowTheTestDataWasMade.md ├── ListOfBaselineEndpoints.md ├── MockAccessToken.js ├── README.md ├── ResetDB.js ├── Template.test.js ├── baseline │ ├── admin │ │ ├── Earnings.test.js │ │ ├── Files.test.js │ │ ├── Plays.test.js │ │ ├── Trackgroups.test.js │ │ ├── Tracks.test.js │ │ └── Users.test.js │ ├── artists │ │ └── Artists.test.js │ ├── auth │ │ ├── AccessTokenExample.test.js │ │ └── Auth.test.js │ ├── labels │ │ └── Labels.test.js │ ├── playlists │ │ └── playlists.test.js │ ├── search │ │ └── Search.test.js │ ├── tag │ │ └── Tag.test.js │ ├── trackgroups │ │ └── Trackgroups.test.js │ ├── tracks │ │ └── Tracks.test.js │ ├── user │ │ ├── MiscUserInfo.test.js │ │ ├── Products.test.js │ │ ├── User.test.js │ │ ├── UserArtist.test.js │ │ ├── UserPlays.test.js │ │ ├── UserTracks.test.js │ │ ├── files.test.js │ │ ├── playlists.test.js │ │ └── trackgroups.test.js │ └── users │ │ └── users.test.js ├── media │ ├── audio │ │ ├── 112e3c00-7727-4e7b-8e60-be0751d56a77.m4a │ │ ├── 23d45613-5b56-4ceb-9a2c-efa266beaeeb.m4a │ │ ├── 26de83c3-537a-4a09-8942-2deb1eb42a04.m4a │ │ ├── 3d141f46-de2f-4b0f-af6e-34cf6b987805.m4a │ │ ├── 4ef71341-1de3-4b19-b224-46878619f7a4.m4a │ │ ├── 57200189-5eb7-434e-a023-57baabea9eda.m4a │ │ ├── 57f0476b-c5cf-43c9-8aad-ede6bde080c4.m4a │ │ ├── 692a9e72-8278-4ae1-b9e3-e17c8773db77.m4a │ │ ├── 69560f28-2953-4ed9-9a5d-a1a4cc7db047.m4a │ │ ├── 6fe8b335-d3ae-42e2-b2ac-a26866402520.m4a │ │ ├── 7516dc73-e304-43a7-9f00-0ba387acac9b.m4a │ │ ├── 7d9df3f3-b85d-4f3f-8fbe-bf48e6f1cb51.m4a │ │ ├── 843eb984-da5b-4244-8ed7-2678948cca19.m4a │ │ ├── 8a45c5c4-4986-4140-bcf2-ac55f072ee93.m4a │ │ ├── 8b30f3c5-37e5-4cba-b99c-f3cb7b5c15d2.m4a │ │ ├── 9013c592-1576-4fa4-b2ea-9953bf8a21b0.m4a │ │ ├── 911ba504-d74b-4cd6-83f6-33937e03cd46.m4a │ │ ├── 9a99d504-9270-450f-b64f-06daed70fd5a.m4a │ │ ├── 9ba71ad7-eb4f-4a56-b8da-e3f395f590ea.m4a │ │ ├── a6cb36e6-77ff-4a37-ba83-0d828b254183.m4a │ │ ├── ac4833b7-5aa5-4205-8d4a-c8c9575a2bc0.m4a │ │ ├── b60f1759-6405-4457-9910-6da1ccd5f40f.m4a │ │ ├── bf8f778a-793e-4e66-bf19-f10ede501ea3.m4a │ │ ├── c1a1aea3-25d5-4608-93e5-7616a7d25387.m4a │ │ ├── d2cbc2b4-36a6-4854-85f9-ed0aa0b46711.m4a │ │ ├── dba9e81b-d32c-4541-a99a-c81cb8fb142b.m4a │ │ ├── de7dfe91-1122-4a64-a757-20d7817251a4.m4a │ │ ├── f27f9c60-ed1c-436c-9382-72a26abf644d.m4a │ │ ├── trim-112e3c00-7727-4e7b-8e60-be0751d56a77.m4a │ │ ├── trim-1a20b4b0-b853-4e75-8815-f5148af2b64a.m4a │ │ ├── trim-1b41b2ec-36d8-4385-8f15-bd25b55005db.m4a │ │ ├── trim-23d45613-5b56-4ceb-9a2c-efa266beaeeb.m4a │ │ ├── trim-26de83c3-537a-4a09-8942-2deb1eb42a04.m4a │ │ ├── trim-3d141f46-de2f-4b0f-af6e-34cf6b987805.m4a │ │ ├── trim-4ef71341-1de3-4b19-b224-46878619f7a4.m4a │ │ ├── trim-57200189-5eb7-434e-a023-57baabea9eda.m4a │ │ ├── trim-57f0476b-c5cf-43c9-8aad-ede6bde080c4.m4a │ │ ├── trim-692a9e72-8278-4ae1-b9e3-e17c8773db77.m4a │ │ ├── trim-69560f28-2953-4ed9-9a5d-a1a4cc7db047.m4a │ │ ├── trim-6fe8b335-d3ae-42e2-b2ac-a26866402520.m4a │ │ ├── trim-7516dc73-e304-43a7-9f00-0ba387acac9b.m4a │ │ ├── trim-7d9df3f3-b85d-4f3f-8fbe-bf48e6f1cb51.m4a │ │ ├── trim-843eb984-da5b-4244-8ed7-2678948cca19.m4a │ │ ├── trim-8a45c5c4-4986-4140-bcf2-ac55f072ee93.m4a │ │ ├── trim-8b30f3c5-37e5-4cba-b99c-f3cb7b5c15d2.m4a │ │ ├── trim-9013c592-1576-4fa4-b2ea-9953bf8a21b0.m4a │ │ ├── trim-911ba504-d74b-4cd6-83f6-33937e03cd46.m4a │ │ ├── trim-9a99d504-9270-450f-b64f-06daed70fd5a.m4a │ │ ├── trim-9ba71ad7-eb4f-4a56-b8da-e3f395f590ea.m4a │ │ ├── trim-a6cb36e6-77ff-4a37-ba83-0d828b254183.m4a │ │ ├── trim-ac4833b7-5aa5-4205-8d4a-c8c9575a2bc0.m4a │ │ ├── trim-b60f1759-6405-4457-9910-6da1ccd5f40f.m4a │ │ ├── trim-bf8f778a-793e-4e66-bf19-f10ede501ea3.m4a │ │ ├── trim-c1a1aea3-25d5-4608-93e5-7616a7d25387.m4a │ │ ├── trim-d2cbc2b4-36a6-4854-85f9-ed0aa0b46711.m4a │ │ ├── trim-dba9e81b-d32c-4541-a99a-c81cb8fb142b.m4a │ │ ├── trim-de7dfe91-1122-4a64-a757-20d7817251a4.m4a │ │ ├── trim-f27f9c60-ed1c-436c-9382-72a26abf644d.m4a │ │ ├── whiteNoise50s1.m4a │ │ ├── whiteNoise50s2.m4a │ │ ├── whiteNoise50s3.m4a │ │ ├── whiteNoise50s4.m4a │ │ ├── whiteNoise50s5.m4a │ │ ├── whiteNoise50s6.m4a │ │ ├── whiteNoise5s1.m4a │ │ ├── whiteNoise5s2.m4a │ │ ├── whiteNoise5s3.m4a │ │ ├── whiteNoise5s4.m4a │ │ ├── whiteNoise5s5.m4a │ │ └── whiteNoise5s6.m4a │ └── image │ │ ├── album_cover_01.png │ │ ├── avatar_01.png │ │ └── banner_01.png └── testConfig.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_PORT=4000 2 | APP_COOKIE_KEY=koa.sess 3 | APP_COOKIE_DOMAIN=stream.resonate.localhost 4 | APP_KEY="213" 5 | APP_KEY_2="124" 6 | APP_EMAIL="resonate.com" 7 | APP_HOST='http://localhost:4000' 8 | STATIC_MEDIA_HOST=http://localhost:4000 9 | 10 | NGINX_PORT=3001 11 | MEDIA_LOCATION=./data/media/ 12 | 13 | POSTGRES_HOSTNAME=pgsql 14 | POSTGRES_EXT_PORT=5432 15 | POSTGRES_USER=resonate 16 | POSTGRES_PASSWORD=resonate 17 | POSTGRES_DB=resonate 18 | POSTGRES_LOCAL_MACHINE_PORT=5432 19 | POSTGRES_TEST_LOCAL_MACHINE_PORT=5434 20 | 21 | POSTGRES_USER=resonate 22 | POSTGRES_PASSWORD=resonate 23 | POSTGRES_DB=resonate 24 | 25 | REDIS_HOST=redis 26 | REDIS_PORT=6379 27 | REDIS_PASSWORD=password 28 | 29 | STRIPE_KEY=test 30 | 31 | MAILGUN_API_KEY=test 32 | MAILGUN_DOMAIN=test 33 | MAILGUN_SENDER=no-reply@resonate.coop 34 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with a single Patreon username 4 | patreon: # Replace with a single Patreon username 5 | open_collective: resonate 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | commitlint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - uses: wagoid/commitlint-github-action@v4 -------------------------------------------------------------------------------- /.github/workflows/deploy-prod.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Production 2 | on: [workflow_dispatch] 3 | jobs: 4 | deploy: 5 | if: github.ref == 'refs/heads/main' 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: executing remote ssh commands using ssh key 9 | uses: appleboy/ssh-action@master 10 | with: 11 | host: ${{ secrets.HETZNER_PROD_IP_SERVER }} 12 | username: ${{ secrets.HETZNER_PROD_SERVER_USER}} 13 | key: ${{ secrets.HETZNER_PROD_SERVER_SSH_KEY }} 14 | port: 22 15 | script_stop: true 16 | script: | 17 | cd api 18 | git pull 19 | docker-compose up --no-deps -d --force-recreate api 20 | docker-compose up --no-deps -d --force-recreate background 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy-stage.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Stage 2 | on: [push] 3 | jobs: 4 | deploy: 5 | if: github.ref == 'refs/heads/main' 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: executing remote ssh commands using ssh key 9 | uses: appleboy/ssh-action@master 10 | with: 11 | host: ${{ secrets.SERVER_IP }} 12 | username: ${{ secrets.SERVER_USER}} 13 | key: ${{ secrets.SSH_KEY }} 14 | port: 22 15 | script_stop: true 16 | script: | 17 | cd api 18 | git pull 19 | docker-compose up --no-deps -d --force-recreate api 20 | docker-compose up --no-deps -d --force-recreate background 21 | -------------------------------------------------------------------------------- /.github/workflows/standard.yml: -------------------------------------------------------------------------------- 1 | name: Lint primarily with standard 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | standard: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - uses: actions/setup-node@v2 15 | 16 | - name: Install our packages 17 | run: yarn install 18 | 19 | - name: Lint 20 | run: yarn lint 21 | -------------------------------------------------------------------------------- /.github/workflows/unit.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - features/** 8 | - dependabot/** 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | docker: 15 | timeout-minutes: 10 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v1 21 | 22 | - name: Install node 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: 14.x 26 | 27 | - name: Copy env file 28 | run: | 29 | cp .env.example .env 30 | cp jwk-keys.json.example jwk-keys.json 31 | cp cookies-keys.json.example cookies-keys.json 32 | - name: Echo CI 33 | run: echo ${CI} 34 | 35 | - name: Install 36 | run: yarn install --force 37 | 38 | - name: Start containers 39 | run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up -d --build 40 | 41 | - name: Migrations 42 | run: docker exec resonate-api npx sequelize db:migrate --config src/config/databases.js --migrations-path src/db/migrations 43 | 44 | # - name: Seed data 45 | # run: docker exec resonate-api npx sequelize db:seed:all --config src/config/databases.js --seeders-path src/db/seeders/test 46 | 47 | - name: Sleep to give things time to start 48 | run: sleep 30s 49 | 50 | - name: Run tests 51 | run: docker exec resonate-api yarn mocha test --exit 52 | 53 | - name: Stop containers 54 | if: always() 55 | run: docker-compose down 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .vscode 3 | resonate-api*.code-workspace 4 | reset.sh 5 | 6 | node_modules 7 | 8 | data 9 | yarn-error.log 10 | error.log 11 | error.test.log 12 | 13 | jwk-keys.json 14 | cookies-keys.json 15 | 16 | asdf.txt 17 | asdf.json 18 | 19 | backups 20 | src/db/legacy/config.js 21 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | npx --no -- standard -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | // Config for running Mocha in watch mode, inside of resonate api test Docker container 2 | 3 | // https://www.testim.io/blog/mocharc-configuration/ 4 | // https://stackoverflow.com/questions/72479267/mocha-watch-with-docker 5 | 6 | // CI is set to true set by the CI provider (GitHub Actions in our case) 7 | // We don't need to worry about it locally. 8 | const shouldWatch = process.env.CI === "true" ? false : true; 9 | 10 | module.exports = { 11 | "reporter": "spec", 12 | "watch": shouldWatch, 13 | "watch-files": ['test/**/*.js', 'src/**/*.js', 'src/**/*.mjs'], 14 | "watch-ignore": ['node_modules'], 15 | "recursive": true 16 | }; -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jrottenberg/ffmpeg:5.0-alpine as ffmpeg 2 | 3 | FROM node:16-alpine 4 | 5 | ENV NODE_APP_DIR=/var/www/api/src 6 | WORKDIR /var/www/api 7 | 8 | COPY . . 9 | RUN yarn install --force 10 | 11 | # copy ffmpeg bins 12 | COPY --from=ffmpeg / / 13 | 14 | EXPOSE 4000 15 | 16 | CMD ["yarn", "start:dev"] 17 | -------------------------------------------------------------------------------- /background/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jrottenberg/ffmpeg:5.0-alpine as ffmpeg 2 | 3 | FROM node:16-alpine 4 | 5 | ENV NODE_APP_DIR=/var/www/api/src 6 | WORKDIR /var/www/api 7 | 8 | COPY . . 9 | RUN yarn install --force 10 | 11 | # copy ffmpeg bins 12 | COPY --from=ffmpeg / / 13 | 14 | CMD ["node", "src/jobs/queue-worker.js", "run"] 15 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /cookies-keys.json.example: -------------------------------------------------------------------------------- 1 | ["some secret key", "and also the old rotated away some time ago", "and one more"] -------------------------------------------------------------------------------- /copyAudioFiles.sh: -------------------------------------------------------------------------------- 1 | # if you need to add execute privs for this file: 2 | # chmod +x copyAudioFiles.sh 3 | 4 | rsync -av test/media/audio/ data/media/audio/ --exclude 'whiteNoise*.m4a' 5 | 6 | # if rsync is not available, you can use cp below, but it will copy the whiteNoise*.m4a files. 7 | # cp test/media/audio/*.m4a data/media/audio/ 8 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | 4 | api: 5 | networks: 6 | - api-network 7 | - redis-network 8 | build: . 9 | command: /bin/sh -c "yarn && yarn migrate:test && yarn start:dev" 10 | container_name: resonate-api 11 | env_file: .env 12 | environment: 13 | - NODE_ENV=test 14 | - CI=${CI:-false} 15 | depends_on: 16 | - redis 17 | - pgsql 18 | ports: 19 | - "4000:4000" 20 | restart: always 21 | volumes: 22 | - ./:/var/www/api 23 | - ./data/media/incoming:/data/media/incoming 24 | - ./data/media/audio:/data/media/audio 25 | - ./data/media/images:/data/media/images 26 | 27 | pgsql: 28 | image: postgres:14-alpine 29 | env_file: 30 | - .env 31 | volumes: 32 | - ./data/pgsql-test:/var/lib/postgresql/data 33 | - ./data/pgsql-test-backups:/backups 34 | container_name: resonate-pgsql-test 35 | networks: 36 | api-network: 37 | aliases: 38 | - pgsql 39 | ports: 40 | - '${POSTGRES_TEST_LOCAL_MACHINE_PORT:-5432}:5432' 41 | 42 | version: "3.7" 43 | -------------------------------------------------------------------------------- /emails/earnings-report/html.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | meta(charset="utf-8") 6 | meta(name="viewport", content="width=device-width") 7 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 8 | meta(name="x-apple-disable-message-reformatting") 9 | link(rel="stylesheet", href="style.css", data-inline) 10 | body.sans-serif 11 | p Hi #{firstName}, 12 | 13 | p 14 | | Here's your earnings report for the period from 15 | b #{from} 16 | | to 17 | b #{to} 18 | 19 | p 20 | | Reply to 21 | a(href='mailto:members@resonate.coop') members@resonate.coop 22 | | if you have any questions. 23 | 24 | p The Resonate team 25 | -------------------------------------------------------------------------------- /emails/earnings-report/subject.pug: -------------------------------------------------------------------------------- 1 | = `Resonate earnings Report` 2 | -------------------------------------------------------------------------------- /emails/earnings-report/text.pug: -------------------------------------------------------------------------------- 1 | | Hi #{firstName}, 2 | | 3 | | Here's your earnings report for the period from #{from} to #{to} 4 | | 5 | | Reply to members@resonate.coop if you have any questions. 6 | | 7 | | The Resonate team 8 | -------------------------------------------------------------------------------- /emails/email-confirmation/html.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | meta(charset="utf-8") 6 | meta(name="viewport", content="width=device-width") 7 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 8 | meta(name="x-apple-disable-message-reformatting") 9 | style 10 | include ../style.css 11 | body.sans-serif 12 | p Hi! 13 | p You asked to reset your email address. 14 | p #{user.email} 15 | p 16 | a(href=`${host}/register/emailConfirmation/${user.emailConfirmationToken}?email=${user.email}`) Click here to confirm 17 | p If this was not you, please get in touch with our worker members as soon as possible dev@resonate.coop 18 | -------------------------------------------------------------------------------- /emails/email-confirmation/subject.pug: -------------------------------------------------------------------------------- 1 | = `Resonate: Email reset` 2 | -------------------------------------------------------------------------------- /emails/email-confirmation/text.pug: -------------------------------------------------------------------------------- 1 | | Hi! 2 | | 3 | | You asked to reset your email address. 4 | | 5 | | #{user.email} 6 | | 7 | | Click this link to confirm: #{host}/register/emailConfirmation/#{user.emailConfirmationToken}?email=#{user.email} 8 | | 9 | | If this was not you, please get in touch with our worker members as soon as possible dev@resonate.coop -------------------------------------------------------------------------------- /emails/new-upload/html.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | meta(charset="utf-8") 6 | meta(name="viewport", content="width=device-width") 7 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 8 | meta(name="x-apple-disable-message-reformatting") 9 | link(rel="stylesheet", href="style.css", data-inline) 10 | body.sans-serif 11 | .pa4 12 | .overflow-auto 13 | table.f6.w-100.mw8.center(cellspacing='0') 14 | thead 15 | tr.stripe-dark 16 | th.fw6.tl.pa3.bg-white Id 17 | th.fw6.tl.pa3.bg-white Filesize 18 | th.fw6.tl.pa3.bg-white Email 19 | tbody.lh-copy 20 | tr.stripe-dark 21 | td.pa3 #{filename} 22 | td.pa3 #{filesize} 23 | td.pa3 #{email} 24 | tr.stripe-dark 25 | -------------------------------------------------------------------------------- /emails/new-upload/subject.pug: -------------------------------------------------------------------------------- 1 | = `New upload ready` 2 | -------------------------------------------------------------------------------- /emails/new-upload/text.pug: -------------------------------------------------------------------------------- 1 | | Hi #{name}, 2 | | This is just a text test. 3 | -------------------------------------------------------------------------------- /emails/new-user/html.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | meta(charset="utf-8") 6 | meta(name="viewport", content="width=device-width") 7 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 8 | meta(name="x-apple-disable-message-reformatting") 9 | style 10 | include ../style.css 11 | body.sans-serif 12 | p Hi! 13 | p Someone signed up to resonate using this email address 14 | p #{user.email} 15 | p 16 | a(href=`${host}/register/emailConfirmation/${user.emailConfirmationToken}?email=${user.email}`) Click here to confirm 17 | p If this was not you, you can safely ignore this email. 18 | -------------------------------------------------------------------------------- /emails/new-user/subject.pug: -------------------------------------------------------------------------------- 1 | = `Welcome to resonate!` 2 | -------------------------------------------------------------------------------- /emails/new-user/text.pug: -------------------------------------------------------------------------------- 1 | | Hi! 2 | | Someone signed up to resonate using this email address 3 | | 4 | | #{user.email} 5 | | 6 | | Click this link to confirm: #{host}/register/emailConfirmation/#{user.emailConfirmationToken}?email=#{user.email} 7 | | 8 | | If this was not you, you can ignore this email. -------------------------------------------------------------------------------- /emails/password-reset/html.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | meta(charset="utf-8") 6 | meta(name="viewport", content="width=device-width") 7 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 8 | meta(name="x-apple-disable-message-reformatting") 9 | style 10 | include ../style.css 11 | body.sans-serif 12 | p Hi! 13 | p Someone wants to reset the password for this email 14 | p #{user.email} 15 | p 16 | a(href=`${host}/password-reset/confirmation/${user.emailConfirmationToken}?email=${user.email}`) Click here to confirm 17 | p If this was not you, you can safely ignore this email. 18 | -------------------------------------------------------------------------------- /emails/password-reset/subject.pug: -------------------------------------------------------------------------------- 1 | = `Welcome to resonate!` 2 | -------------------------------------------------------------------------------- /emails/password-reset/text.pug: -------------------------------------------------------------------------------- 1 | | Hi! 2 | | Someone signed up to resonate using this email address 3 | | 4 | | #{user.email} 5 | | 6 | | Click this link to confirm: #{host}/register/emailConfirmation/#{user.emailConfirmationToken}?email=#{user.email} 7 | | 8 | | If this was not you, you can ignore this email. -------------------------------------------------------------------------------- /emails/style.css: -------------------------------------------------------------------------------- 1 | .sans-serif { 2 | font-family: -apple-system, BlinkMacSystemFont, 3 | 'avenir next', avenir, 4 | 'helvetica neue', helvetica, 5 | ubuntu, 6 | roboto, noto, 7 | 'segoe ui', arial, 8 | sans-serif; 9 | } 10 | 11 | p { 12 | margin-bottom: .75rem; 13 | } 14 | 15 | .b { 16 | font-weight: bold; 17 | } 18 | 19 | .fw6 { 20 | font-weight: 600; 21 | } 22 | 23 | .lh-copy { 24 | line-height: 1.5; 25 | } 26 | 27 | .mw8 { 28 | max-width: 64rem; 29 | } 30 | 31 | .w-100 { 32 | width: 100%; 33 | } 34 | 35 | .overflow-auto { 36 | overflow: auto; 37 | } 38 | 39 | .bg-white { 40 | background-color: #fff; 41 | } 42 | 43 | .pa3 { 44 | padding: 1rem; 45 | } 46 | 47 | .pa4 { 48 | padding: 2rem; 49 | } 50 | 51 | .stripe-dark:nth-child(odd) { 52 | background-color: rgba(0, 0, 0, .1); 53 | } 54 | 55 | .tl { 56 | text-align: left; 57 | } 58 | 59 | .f6 { 60 | font-size: .875rem; 61 | } 62 | 63 | .center { 64 | margin-right: auto; 65 | margin-left: auto; 66 | } -------------------------------------------------------------------------------- /emails/test/html.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | meta(charset="utf-8") 6 | meta(name="viewport", content="width=device-width") 7 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 8 | meta(name="x-apple-disable-message-reformatting") 9 | link(rel="stylesheet", href="style.css", data-inline) 10 | body.sans-serif 11 | p Hi #{firstName}, 12 | p How are you doing? 13 | -------------------------------------------------------------------------------- /emails/test/subject.pug: -------------------------------------------------------------------------------- 1 | = `This is a test` 2 | -------------------------------------------------------------------------------- /emails/test/text.pug: -------------------------------------------------------------------------------- 1 | | Hi #{name}, 2 | | This is just a text test. 3 | -------------------------------------------------------------------------------- /jwk-keys.json.example: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "d": "VEZOsY07JTFzGTqv6cC2Y32vsfChind2I_TTuvV225_-0zrSej3XLRg8iE_u0-3GSgiGi4WImmTwmEgLo4Qp3uEcxCYbt4NMJC7fwT2i3dfRZjtZ4yJwFl0SIj8TgfQ8ptwZbFZUlcHGXZIr4nL8GXyQT0CK8wy4COfmymHrrUoyfZA154ql_OsoiupSUCRcKVvZj2JHL2KILsq_sh_l7g2dqAN8D7jYfJ58MkqlknBMa2-zi5I0-1JUOwztVNml_zGrp27UbEU60RqV3GHjoqwI6m01U7K0a8Q_SQAKYGqgepbAYOA-P4_TLl5KC4-WWBZu_rVfwgSENwWNEhw8oQ", 4 | "dp": "E1Y-SN4bQqX7kP-bNgZ_gEv-pixJ5F_EGocHKfS56jtzRqQdTurrk4jIVpI-ZITA88lWAHxjD-OaoJUh9Jupd_lwD5Si80PyVxOMI2xaGQiF0lbKJfD38Sh8frRpgelZVaK_gm834B6SLfxKdNsP04DsJqGKktODF_fZeaGFPH0", 5 | "dq": "F90JPxevQYOlAgEH0TUt1-3_hyxY6cfPRU2HQBaahyWrtCWpaOzenKZnvGFZdg-BuLVKjCchq3G_70OLE-XDP_ol0UTJmDTT-WyuJQdEMpt_WFF9yJGoeIu8yohfeLatU-67ukjghJ0s9CBzNE_LrGEV6Cup3FXywpSYZAV3iqc", 6 | "e": "AQAB", 7 | "kty": "RSA", 8 | "n": "xwQ72P9z9OYshiQ-ntDYaPnnfwG6u9JAdLMZ5o0dmjlcyrvwQRdoFIKPnO65Q8mh6F_LDSxjxa2Yzo_wdjhbPZLjfUJXgCzm54cClXzT5twzo7lzoAfaJlkTsoZc2HFWqmcri0BuzmTFLZx2Q7wYBm0pXHmQKF0V-C1O6NWfd4mfBhbM-I1tHYSpAMgarSm22WDMDx-WWI7TEzy2QhaBVaENW9BKaKkJklocAZCxk18WhR0fckIGiWiSM5FcU1PY2jfGsTmX505Ub7P5Dz75Ygqrutd5tFrcqyPAtPTFDk8X1InxkkUwpP3nFU5o50DGhwQolGYKPGtQ-ZtmbOfcWQ", 9 | "p": "5wC6nY6Ev5FqcLPCqn9fC6R9KUuBej6NaAVOKW7GXiOJAq2WrileGKfMc9kIny20zW3uWkRLm-O-3Yzze1zFpxmqvsvCxZ5ERVZ6leiNXSu3tez71ZZwp0O9gys4knjrI-9w46l_vFuRtjL6XEeFfHEZFaNJpz-lcnb3w0okrbM", 10 | "q": "3I1qeEDslZFB8iNfpKAdWtz_Wzm6-jayT_V6aIvhvMj5mnU-Xpj75zLPQSGa9wunMlOoZW9w1wDO1FVuDhwzeOJaTm-Ds0MezeC4U6nVGyyDHb4CUA3ml2tzt4yLrqGYMT7XbADSvuWYADHw79OFjEi4T3s3tJymhaBvy1ulv8M", 11 | "qi": "wSbXte9PcPtr788e713KHQ4waE26CzoXx-JNOgN0iqJMN6C4_XJEX-cSvCZDf4rh7xpXN6SGLVd5ibIyDJi7bbi5EQ5AXjazPbLBjRthcGXsIuZ3AtQyR0CEWNSdM7EyM5TRdyZQ9kftfz9nI03guW3iKKASETqX2vh0Z8XRjyU", 12 | "use": "sig" 13 | }, { 14 | "crv": "P-256", 15 | "d": "K9xfPv773dZR22TVUB80xouzdF7qCg5cWjPjkHyv7Ws", 16 | "kty": "EC", 17 | "use": "sig", 18 | "x": "FWZ9rSkLt6Dx9E3pxLybhdM6xgR5obGsj5_pqmnz5J4", 19 | "y": "_n8G69C-A2Xl4xUW2lF0i8ZGZnk_KPYrhv4GbTGu5G4" 20 | } 21 | ] -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine as base 2 | 3 | FROM base as local-image 4 | 5 | COPY local.dev.conf /etc/nginx/conf.d/default.conf 6 | 7 | FROM base as stage-image 8 | 9 | RUN apk add certbot certbot-nginx 10 | RUN mkdir /etc/letsencrypt 11 | RUN mkdir -p /var/www/html 12 | RUN mkdir -p /var/lib/letsencrypt 13 | 14 | COPY dev.resonate.coop.conf /etc/nginx/conf.d/default.conf 15 | RUN SLEEPTIME=$(awk 'BEGIN{srand(); print int(rand()*(3600+1))}'); echo "0 0,12 * * * root sleep $SLEEPTIME && certbot renew -q" | tee -a /etc/crontab > /dev/null 16 | 17 | FROM base as prod-image 18 | 19 | COPY prod.resonate.coop.conf /etc/nginx/conf.d/default.conf 20 | RUN SLEEPTIME=$(awk 'BEGIN{srand(); print int(rand()*(3600+1))}'); echo "0 0,12 * * * root sleep $SLEEPTIME && certbot renew -q" | tee -a /etc/crontab > /dev/null 21 | -------------------------------------------------------------------------------- /nginx/dev.resonate.coop.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | 5 | server_name dev.resonate.coop; 6 | 7 | location ~ /.well-known/acme-challenge { 8 | allow all; 9 | root /var/www/html; 10 | } 11 | 12 | location / { 13 | return 301 https://$host$request_uri; 14 | } 15 | 16 | location @fallback { 17 | return 301 https://$host$request_uri; 18 | } 19 | 20 | location /audio { 21 | root /data/media; 22 | } 23 | } 24 | 25 | server { 26 | server_name dev.resonate.coop; 27 | 28 | listen 443 ssl http2; 29 | listen [::]:443 ssl http2; 30 | 31 | # RSA certificate 32 | ssl_certificate /etc/letsencrypt/live/dev.resonate.coop/fullchain.pem; 33 | ssl_certificate_key /etc/letsencrypt/live/dev.resonate.coop/privkey.pem; 34 | 35 | include /etc/letsencrypt/options-ssl-nginx.conf; 36 | 37 | location / { 38 | # root /var/www/html; 39 | proxy_set_header Host $host; 40 | proxy_set_header X-Real-IP $remote_addr; 41 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 42 | proxy_set_header X-Forwarded-Proto $scheme; 43 | 44 | proxy_pass http://api:4000; 45 | } 46 | 47 | location /audio { 48 | root /data/media; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /nginx/local.dev.conf: -------------------------------------------------------------------------------- 1 | server { 2 | location /audio { 3 | root /data/media; 4 | } 5 | 6 | location / { 7 | proxy_set_header Host $host; 8 | proxy_set_header X-Real-IP $remote_addr; 9 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 10 | proxy_set_header X-Forwarded-Proto $scheme; 11 | 12 | proxy_pass http://api:4000; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /nginx/prod.resonate.coop.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | 5 | server_name prod.resonate.coop; 6 | 7 | location ~ /.well-known/acme-challenge { 8 | allow all; 9 | root /var/www/html; 10 | } 11 | 12 | location / { 13 | return 301 https://$host$request_uri; 14 | } 15 | 16 | location @fallback { 17 | return 301 https://$host$request_uri; 18 | } 19 | 20 | location /audio { 21 | root /data/media; 22 | } 23 | } 24 | 25 | server { 26 | server_name prod.resonate.coop; 27 | 28 | listen 443 ssl http2; 29 | listen [::]:443 ssl http2; 30 | 31 | # RSA certificate 32 | ssl_certificate /etc/letsencrypt/live/prod.resonate.coop/fullchain.pem; 33 | ssl_certificate_key /etc/letsencrypt/live/prod.resonate.coop/privkey.pem; 34 | 35 | include /etc/letsencrypt/options-ssl-nginx.conf; 36 | 37 | location / { 38 | # root /var/www/html; 39 | proxy_set_header Host $host; 40 | proxy_set_header X-Real-IP $remote_addr; 41 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 42 | proxy_set_header X-Forwarded-Proto $scheme; 43 | 44 | proxy_pass http://api:4000; 45 | } 46 | 47 | location /audio { 48 | root /data/media; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": [ 4 | "**/data/*", 5 | "**/test/**", 6 | "**/bin/**", 7 | "**/tmp/**" 8 | ] 9 | } -------------------------------------------------------------------------------- /server.mjs: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv-safe' 2 | import app from './src/index.mjs' 3 | 4 | dotenv.config() 5 | 6 | const port = process.env.APP_PORT || 4000 7 | 8 | app.listen(port) 9 | -------------------------------------------------------------------------------- /src/auth/README.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | We use [`node-oidc-provider`](https://github.com/panva/node-oidc-provider/) to manage most of our authentication and authorization flow for us. 4 | 5 | You can see the automatic configuration at: 6 | 7 | ``` 8 | http://localhost:4000/.well-known/openid-configuration 9 | ``` 10 | 11 | Registration is served at: 12 | 13 | ``` 14 | http://localhost:4000/register 15 | ``` 16 | 17 | ## Redis 18 | 19 | It uses redis to manage sessions. You can explore the redis database using `redis-cli` or a GUI ([recommend using RESP.app](https://docs.resp.app/en/latest/)). 20 | 21 | ### CLI 22 | 23 | ``` 24 | $ docker exec -it resonate-redis redis-cli 25 | $ ping 26 | ``` 27 | 28 | ### GUI 29 | 30 | Download the GUI, connect to localhost:6379, password set in `.env`. 31 | 32 | ## Clients 33 | 34 | Clients are stored in the postgres table `clients`. We don't have a UI for client management yet, and right now we seed a test client. You can edit clients directly in the database. 35 | 36 | Next you'll need a client app. You can use beam. 37 | 38 | ## Inside Beam 39 | 40 | Make sure the code for beam is up to date. Set your `.env.local` file: 41 | 42 | ``` 43 | REACT_APP_CLIENT_SECRET=matron-fling-raging-send-herself-ninth 44 | REACT_APP_CLIENT_ID= 45 | REACT_APP_AUTHORITY=http://localhost:4000 46 | REACT_APP_AUTH_METADATA_URL=http://localhost:4000/.well-known/openid-configuration 47 | REACT_APP_API=http://localhost:4000/ 48 | ``` 49 | 50 | Your client-id you should get from the database, as it was automatically generated on seeding. It'll be the only entry in the `clients` table, and it's in the `key` column. 51 | 52 | Now 53 | 54 | ``` 55 | yarn start 56 | ``` 57 | 58 | This should start up beam. If you've installed beam before you should clear the local storage. 59 | 60 | Now, theoretically, if you click on "Log in" it should take you to a log in page at `localhost:4000/auth?xyz`. You should then be able to log in with the account you created before. -------------------------------------------------------------------------------- /src/auth/api-doc.js: -------------------------------------------------------------------------------- 1 | const apiDoc = { 2 | swagger: '2.0', 3 | info: { 4 | title: 'Resonate Tag API.', 5 | version: '2.0.0-1' 6 | }, 7 | definitions: { 8 | Error: { 9 | type: 'object', 10 | properties: { 11 | code: { 12 | type: 'string' 13 | }, 14 | message: { 15 | type: 'string' 16 | } 17 | }, 18 | required: [ 19 | 'code', 20 | 'message' 21 | ] 22 | } 23 | }, 24 | responses: { 25 | BadRequest: { 26 | description: 'Bad request', 27 | schema: { 28 | $ref: '#/definitions/Error' 29 | } 30 | }, 31 | NotFound: { 32 | description: 'No results found for this tag.', 33 | schema: { 34 | $ref: '#/definitions/Error' 35 | } 36 | } 37 | }, 38 | paths: {}, 39 | tags: [{ name: 'tag' }] 40 | } 41 | 42 | module.exports = apiDoc 43 | -------------------------------------------------------------------------------- /src/auth/apiDocs.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | const operations = { 3 | GET 4 | } 5 | 6 | function GET (ctx, next) { 7 | if (ctx.query.type === 'apiDoc') { 8 | ctx.state.apiDoc.basePath = ctx.query.basePath || '/' 9 | return (ctx.body = ctx.state.apiDoc) 10 | } 11 | return (ctx.body = ctx.state.operationDoc) 12 | } 13 | 14 | GET.apiDoc = { 15 | operationId: 'getApiDoc', 16 | description: 'Returns the requested apiDoc', 17 | parameters: [ 18 | { 19 | description: 'The type of apiDoc to return.', 20 | in: 'query', 21 | name: 'type', 22 | type: 'string', 23 | enum: ['apiDoc', 'operationDoc'] 24 | }, 25 | { 26 | description: 'A custom basePath.', 27 | in: 'query', 28 | name: 'basePath', 29 | type: 'string', 30 | enum: ['/v3/tracks', '/api/v3/tracks', '/'] 31 | } 32 | ], 33 | responses: { 34 | 200: { 35 | description: 'The requested apiDoc.', 36 | schema: { 37 | type: 'object' 38 | } 39 | }, 40 | default: { 41 | description: 'The requested apiDoc.' 42 | } 43 | } 44 | } 45 | 46 | return operations 47 | } 48 | -------------------------------------------------------------------------------- /src/auth/configuration.js: -------------------------------------------------------------------------------- 1 | const { renderError, logoutSource, postLogoutSuccessSource } = require('./utils') 2 | const keys = require('./../../jwk-keys.json') 3 | const cookies = require('./../../cookies-keys.json') 4 | 5 | module.exports = { 6 | clients: [], 7 | scopes: ['read_write'], 8 | interactions: { 9 | url (ctx, interaction) { // eslint-disable-line no-unused-vars 10 | return `/interaction/${interaction.uid}` 11 | } 12 | }, 13 | renderError, 14 | cookies: { 15 | keys: cookies 16 | }, 17 | features: { 18 | devInteractions: { enabled: false }, // defaults to true 19 | deviceFlow: { enabled: true }, // defaults to false 20 | revocation: { enabled: true }, // defaults to false, 21 | rpInitiatedLogout: { 22 | enabled: true, 23 | logoutSource, 24 | postLogoutSuccessSource 25 | } 26 | }, 27 | jwks: { 28 | keys 29 | }, 30 | ttl: { 31 | Session: 14 * 24 * 60 * 60, /* 14 days in seconds */ 32 | AccessToken: function AccessTokenTTL (ctx, token, client) { 33 | if (token.resourceServer) { 34 | return token.resourceServer.accessTokenTTL || 60 * 60 // 1 hour in seconds 35 | } 36 | return 60 * 60 37 | // return 60 * 60 // 1 hour in seconds 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/auth/index.js: -------------------------------------------------------------------------------- 1 | const { Provider } = require('oidc-provider') 2 | const configuration = require('./configuration') 3 | const routes = require('./routes') 4 | const { User } = require('../db/models') 5 | const adapter = require('./redis-adapter') 6 | 7 | configuration.findAccount = async (ctx, id, token) => { 8 | const isExisting = await User.findOne({ 9 | where: { 10 | id 11 | } 12 | }) 13 | 14 | if (isExisting) { 15 | return { 16 | accountId: id, 17 | profile: isExisting, 18 | claims: async () => { 19 | return { 20 | sub: id // it is essential to always return a sub claim 21 | 22 | // address: { 23 | // country: '000', 24 | // formatted: '000', 25 | // locality: '000', 26 | // postal_code: '000', 27 | // region: '000', 28 | // street_address: '000' 29 | // }, 30 | // birthdate: '1987-10-16', 31 | // email: 'johndoe@example.com', 32 | // email_verified: false, 33 | // family_name: 'Doe', 34 | // gender: 'male', 35 | // given_name: 'John', 36 | // locale: 'en-US', 37 | // middle_name: 'Middle', 38 | // name: 'John Doe', 39 | // nickname: 'Johny', 40 | // phone_number: '+49 000 000000', 41 | // phone_number_verified: false, 42 | // picture: 'http://lorempixel.com/400/200/', 43 | // preferred_username: 'johnny', 44 | // profile: 'https://johnswebsite.com', 45 | // updated_at: 1454704946, 46 | // website: 'http://example.com', 47 | // zoneinfo: 'Europe/Berlin' 48 | } 49 | } 50 | } 51 | } 52 | return null 53 | } 54 | 55 | const { APP_PORT = 3000, APP_HOST = `http://localhost:${APP_PORT}` } = process.env 56 | 57 | const provider = new Provider(APP_HOST, { adapter, ...configuration }) 58 | 59 | module.exports = { provider, routes } 60 | -------------------------------------------------------------------------------- /src/auth/public/fonts/Graphik-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/src/auth/public/fonts/Graphik-Regular.woff -------------------------------------------------------------------------------- /src/auth/public/fonts/Graphik-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/src/auth/public/fonts/Graphik-Regular.woff2 -------------------------------------------------------------------------------- /src/auth/public/fonts/Graphik-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/src/auth/public/fonts/Graphik-Semibold.woff -------------------------------------------------------------------------------- /src/auth/public/fonts/Graphik-Semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/src/auth/public/fonts/Graphik-Semibold.woff2 -------------------------------------------------------------------------------- /src/auth/public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: white; 3 | font-size: 18px; 4 | font-family: Graphik, -apple-system, BlinkMacSystemFont, "avenir next", avenir, helvetica, "helvetica neue", ubuntu, roboto, noto, "segoe ui", arial, sans-serif; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | flex-direction: column; 9 | height: 100vh; 10 | } 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | a { 17 | color: black; 18 | font-weight: bold; 19 | } 20 | 21 | h1 { 22 | text-align: center; 23 | } 24 | 25 | .content { 26 | min-width: 300px; 27 | max-width: 400px; 28 | display: flex; 29 | margin: 0 auto; 30 | flex-direction: column; 31 | } 32 | 33 | .content form { 34 | display: flex; 35 | flex-direction: column; 36 | 37 | } 38 | 39 | form input { 40 | font-size: 1rem; 41 | padding: 1rem; 42 | margin: .25rem; 43 | border-radius: 6px; 44 | background-color: black; 45 | border: 1px solid; 46 | color: white; 47 | } 48 | 49 | button { 50 | background-color: white; 51 | border: 1px solid black; 52 | border-radius: 6px; 53 | font-size: 1rem; 54 | margin: .25rem; 55 | padding: 1rem; 56 | color: black; 57 | cursor: pointer; 58 | } 59 | 60 | .scopes { 61 | background: #efefef; 62 | padding: 1rem; 63 | border-radius: 6px; 64 | } 65 | 66 | .scopes>li { 67 | list-style: none; 68 | } 69 | 70 | .center { 71 | text-align: center; 72 | } 73 | 74 | .error ul, 75 | .success ul { 76 | padding: 1.5rem 2rem; 77 | border-radius: 6px; 78 | list-style: none; 79 | max-width: 600px; 80 | } 81 | 82 | .error ul { 83 | background: #ffe9e9; 84 | color: red; 85 | } 86 | 87 | .success ul { 88 | background: lightblue; 89 | color: navy; 90 | } 91 | 92 | #footer, 93 | .disclaimer { 94 | text-align: center; 95 | font-size: .75rem; 96 | } -------------------------------------------------------------------------------- /src/auth/utils.js: -------------------------------------------------------------------------------- 1 | module.exports.renderError = (ctx, out, error) => { 2 | console.error(error) 3 | ctx.type = 'html' 4 | return ctx.render('root-page', { 5 | messages: { 6 | error: Object.entries(out) 7 | .map(([key, val]) => `${key}: ${val}`) 8 | } 9 | }) 10 | } 11 | 12 | module.exports.logoutSource = (ctx, form) => { 13 | ctx.type = 'html' 14 | return ctx.render('logout', { 15 | form, 16 | host: ctx.host 17 | }) 18 | } 19 | 20 | module.exports.postLogoutSuccessSource = async (ctx) => { 21 | ctx.body = ` 22 |
23 |