├── .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 | Signed out 24 | 25 | 26 | 29 | 30 | ` 31 | } 32 | -------------------------------------------------------------------------------- /src/auth/views/account.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h1 Hi #{user.displayName} 5 | form(action=`/account`, method="post", autocomplete="off") 6 | input(type="email", name="email", value=`${user.email}`, placeholder="Enter your email") 7 | input(type="password", name="password", placeholder="and password") 8 | button(type="submit") 9 | text To-do: update your email -------------------------------------------------------------------------------- /src/auth/views/email-confirmed.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h2 Registration step: confirming email -------------------------------------------------------------------------------- /src/auth/views/interaction-login.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h1 Log in 5 | form(action=`/interaction/${uid}/login`, method="post", autocomplete="off") 6 | input(type="email", name="email", placeholder="Enter your email") 7 | input(type="password", name="password", placeholder="and password") 8 | button(type="submit") 9 | text Sign-in to Resonate 10 | p.disclaimer Forgot your password? #[a(href="/password-reset") Reset it]. -------------------------------------------------------------------------------- /src/auth/views/interaction.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | 4 | block content 5 | - 6 | const mappedScopes = { 'read_write': 'read & write'} 7 | 8 | div(class="login-client-image") 9 | ul(class="scopes") 10 | if [details.missingOIDCScope, details.missingOIDCClaims, details.missingResourceScopes].filter(Boolean).length === 0 11 | li The client is asking you to confirm previously given authorization 12 | - 13 | const missingOIDCScope = new Set(details.missingOIDCScope); 14 | missingOIDCScope.delete('openid'); 15 | missingOIDCScope.delete('offline_access') 16 | if missingOIDCScope.size 17 | li You're granting #[a(href=`${client.clientUrl}`) #{client.clientName}] access to: 18 | ul 19 | each scope of missingOIDCScope 20 | li #{mappedScopes[scope]} 21 | li on your account 22 | - 23 | const missingOIDCClaims = new Set(details.missingOIDCClaims); 24 | ['sub', 'sid', 'auth_time', 'acr', 'amr', 'iss'].forEach(Set.prototype.delete.bind(missingOIDCClaims)) 25 | if missingOIDCClaims.size 26 | li Claims: 27 | ul 28 | each scope of missingOIDCClaims 29 | li #{scope} 30 | - 31 | const missingResourceScopes = new Set(details.missingResourceScopes); 32 | if missingResourceScopes.size 33 | each object in Object.entries(details.missingResourceScopes) 34 | li object[0] 35 | ul 36 | each scope of object[1] 37 | li #{scope} 38 | if params.scope && params.scope.includes('offline_access') 39 | li the client is asking to have offline access to this authorization 40 | if (!details.missingOIDCScope) || !details.missingOIDCScope.includes('offline_access') 41 | p (which you have previously granted) 42 | 43 | form(action=`/interaction/${uid}/confirm`, method="post", autocomplete="off") 44 | button(type="submit", autofocus) Continue 45 | 46 | -------------------------------------------------------------------------------- /src/auth/views/layout.pug: -------------------------------------------------------------------------------- 1 | //- layout.pug 2 | html 3 | head 4 | title Resonate - #{title} 5 | block style 6 | link(rel='stylesheet', type='text/css', href='/public/style.css') 7 | link(rel='stylesheet', type='text/css', href='/fonts/Graphik-Semibold.woff2') 8 | link(rel='stylesheet', type='text/css', href='/fonts/Graphik-Regular.woff2') 9 | link(rel='stylesheet', type='text/css', href='/fonts/Graphik-Semibold.woff') 10 | link(rel='stylesheet', type='text/css', href='/fonts/Graphik-Regular.woff') 11 | 12 | block scripts 13 | //- script(src='/jquery.js') 14 | body 15 | if messages 16 | for val, key in messages 17 | div(class=key) 18 | ul 19 | each val in messages[key] 20 | li #{val} 21 | div.content 22 | block content 23 | block foot 24 | #footer 25 | p Authentication provided by #[a(href="https://resonate.coop") Resonate]. 26 | -------------------------------------------------------------------------------- /src/auth/views/logout.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h1 Log out from the entire Resonate ecosystem? 5 | p you've been logged out from the Resonate client. Do you want to log out from our identity server as well? 6 | | !{form} 7 | button(autofocus, type="submit", form="op.logoutForm" value="yes" name="logout") Yes, sign me out 8 | button(type="submit", form="op.logoutForm") No, stay signed in 9 | -------------------------------------------------------------------------------- /src/auth/views/password-reset-email-sent.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h2 Password reset request email sent -------------------------------------------------------------------------------- /src/auth/views/password-reset-request.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h1 Request password reset 5 | form(action="/password-reset", method="post", autocomplete="off") 6 | input(type="email", name="email", placeholder="Enter your email") 7 | button(type="submit") 8 | text Request password reset -------------------------------------------------------------------------------- /src/auth/views/password-reset-success.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h2 Password reset successfully -------------------------------------------------------------------------------- /src/auth/views/password-reset.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h1 Reset password 5 | form(action="/password-reset/confirmation", method="post", autocomplete="off") 6 | input(type="hidden", name="token", value=params.token) 7 | input(type="hidden", name="email", value=params.email) 8 | 9 | input(type="password", name="password", placeholder="Enter your password") 10 | button(type="submit") 11 | text reset password -------------------------------------------------------------------------------- /src/auth/views/registration-success.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h2 Check your e-mail for a confirmation! -------------------------------------------------------------------------------- /src/auth/views/registration.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h1 Register 5 | form(action="/register", method="post", autocomplete="off") 6 | input(type="email", name="email", placeholder="Enter your email") 7 | input(type="password", name="password", placeholder="and password") 8 | button(type="submit") 9 | text Register -------------------------------------------------------------------------------- /src/auth/views/root-page.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h1 Resonate API 5 | p.center Check out #[a(href="https://resonate-beam.netlify.app/") our music player] -------------------------------------------------------------------------------- /src/config/compression.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return { 3 | filter: (contentType) => { 4 | return /text/i.test(contentType) 5 | }, 6 | threshold: 2048, 7 | flush: require('zlib').Z_SYNC_FLUSH 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/config/cors.js: -------------------------------------------------------------------------------- 1 | const { Client } = require('../db/models') 2 | const { Op } = require('sequelize') 3 | 4 | module.exports = { 5 | origin: async (req) => { 6 | const hasClientsWithOrigin = await Client.findAll({ 7 | where: { 8 | 'metaData.allowed-cors-origins': { [Op.contains]: JSON.stringify(req.header.origin) } 9 | } 10 | }) 11 | 12 | if (hasClientsWithOrigin.length > 0) { 13 | return req.header.origin 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/config/databases.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug') 2 | require('dotenv').config() 3 | 4 | const config = { 5 | development: { 6 | username: process.env.POSTGRES_USER, 7 | password: process.env.POSTGRES_PASSWORD, 8 | database: process.env.POSTGRES_DB, 9 | host: process.env.POSTGRES_HOSTNAME, 10 | port: process.env.POSTGRES_EXT_PORT || 5432, 11 | // host: 'localhost', 12 | // port: 5433, 13 | dialect: 'postgres', 14 | logging: debug('sequelize') 15 | }, 16 | test: { 17 | username: process.env.POSTGRES_USER, 18 | password: process.env.POSTGRES_PASSWORD, 19 | database: process.env.POSTGRES_DB, 20 | host: process.env.POSTGRES_HOSTNAME, 21 | dialect: 'postgres', 22 | port: process.env.POSTGRES_EXT_PORT || 5435, 23 | // logging: console.log 24 | logging: debug('sequelize') 25 | }, 26 | production: { 27 | username: process.env.POSTGRES_USER, 28 | password: process.env.POSTGRES_PASSWORD, 29 | database: process.env.POSTGRES_DB, 30 | host: process.env.POSTGRES_HOSTNAME, 31 | dialect: 'postgres', 32 | port: process.env.POSTGRES_EXT_PORT || 5432, 33 | logging: false 34 | } 35 | } 36 | 37 | module.exports = config 38 | -------------------------------------------------------------------------------- /src/config/error.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return err => { 3 | return { 4 | status: err.status, 5 | message: err.message, 6 | errors: err.errors, // errors from openapi validator 7 | data: null 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/config/file-status-list.js: -------------------------------------------------------------------------------- 1 | const FILE_STATUS_LIST = ['processing', 'errored', 'ok'] 2 | 3 | module.exports = FILE_STATUS_LIST 4 | -------------------------------------------------------------------------------- /src/config/grant.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | defaults: { 3 | transport: 'session', 4 | state: true, 5 | origin: process.env.GRANT_ORIGIN, 6 | prefix: process.env.GRANT_PREFIX 7 | }, 8 | resonate: { 9 | access_url: `${process.env.OAUTH_HOST}/v1/oauth/tokens`, 10 | authorize_url: `${process.env.OAUTH_HOST}/authorize`, 11 | oauth: 2, 12 | key: process.env.OAUTH_CLIENT, 13 | secret: process.env.OAUTH_SECRET, 14 | token_endpoint_auth_method: 'client_secret_basic', 15 | scope: ['read_write'], 16 | callback: '/' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/config/redis.js: -------------------------------------------------------------------------------- 1 | 2 | const REDIS_CONFIG = { 3 | port: process.env.REDIS_PORT || 6379, 4 | host: process.env.REDIS_HOST || '127.0.0.1', 5 | password: process.env.REDIS_PASSWORD 6 | } 7 | 8 | module.exports.REDIS_CONFIG = REDIS_CONFIG 9 | -------------------------------------------------------------------------------- /src/config/session.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | key: process.env.APP_COOKIE_KEY || 'stream.koa.sess', 3 | domain: process.env.APP_COOKIE_DOMAIN || 'stream.resonate.coop', 4 | maxAge: 86400000, 5 | autoCommit: true, 6 | overwrite: true, 7 | httpOnly: true, 8 | signed: true, 9 | rolling: false, 10 | renew: false, 11 | secure: process.env.NODE_ENV === 'production', 12 | sameSite: 'None' 13 | } 14 | -------------------------------------------------------------------------------- /src/config/supported-media-types.js: -------------------------------------------------------------------------------- 1 | module.exports.HIGH_RES_AUDIO_MIME_TYPES = [ 2 | 'audio/x-flac', 3 | 'audio/vnd.wave', 4 | 'audio/aiff' 5 | ] 6 | 7 | module.exports.SUPPORTED_IMAGE_MIME_TYPES = [ 8 | 'image/png', 9 | 'image/jpeg' 10 | ] 11 | 12 | module.exports.SUPPORTED_AUDIO_MIME_TYPES = [ 13 | 'audio/aac', 14 | 'audio/aiff', 15 | 'audio/mp4', 16 | 'audio/vnd.wave', 17 | 'audio/x-flac', 18 | 'audio/x-m4a' 19 | ] 20 | 21 | module.exports.SUPPORTED_MEDIA_TYPES = [ 22 | ...module.exports.SUPPORTED_AUDIO_MIME_TYPES, 23 | ...module.exports.SUPPORTED_IMAGE_MIME_TYPES, 24 | 'text/csv' 25 | ] 26 | -------------------------------------------------------------------------------- /src/config/swagger.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return { 3 | swaggerOptions: { 4 | urls: [ 5 | { 6 | url: '/api/v3/apiDocs?type=apiDoc', 7 | name: 'Resonate API' 8 | } 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Track.status 3 | */ 4 | 5 | module.exports.TRACK_STATUS_LIST = [ 6 | 'free+paid', 7 | 'hidden', 8 | 'free', 9 | 'paid', 10 | 'deleted' 11 | ] 12 | 13 | /** 14 | * File.status 15 | */ 16 | 17 | module.exports.FILE_STATUS_LIST = [ 18 | 'processing', 19 | 'errored', 20 | 'ok' 21 | ] 22 | 23 | module.exports.TRACKGROUP_TYPES = [ 24 | 'lp', // long player 25 | 'ep', // extended play 26 | 'single', 27 | 'playlist', 28 | 'compilation', 29 | 'collection' 30 | ] 31 | 32 | /** 33 | * Roles 34 | */ 35 | 36 | const ADMIN_ROLE = 'admin' 37 | const SUPER_ADMIN_ROLE = 'superadmin' 38 | const ARTIST_ROLE = 'artist' 39 | const LABEL_ROLE = 'label' 40 | 41 | module.exports.ROLES_LIST = [ 42 | ADMIN_ROLE, 43 | SUPER_ADMIN_ROLE, 44 | ARTIST_ROLE, 45 | LABEL_ROLE 46 | ] 47 | 48 | module.exports.CREATORS_ROLES_LIST = [ 49 | ARTIST_ROLE, 50 | LABEL_ROLE 51 | ] 52 | 53 | module.exports.STAFF_ROLES_LIST = [ 54 | ADMIN_ROLE, 55 | SUPER_ADMIN_ROLE 56 | ] 57 | 58 | // module.exports = { 59 | // ADMIN_ROLE, 60 | // ARTIST_ROLE, 61 | // LABEL_ROLE, 62 | // ROLES_LIST: module.exports.RO, 63 | // CREATORS_ROLES_LIST, 64 | // STAFF_ROLES_LIST, 65 | // FILE_STATUS_LIST, 66 | // TRACKGROUP_TYPES, 67 | // TRACK_STATUS_LIST 68 | // } 69 | 70 | module.exports.apiRoot = '/api/v3' 71 | -------------------------------------------------------------------------------- /src/controllers/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/user/profile', '/api/v3/user/profile', '/'] 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/controllers/artists/artistService.js: -------------------------------------------------------------------------------- 1 | const resolveProfileImage = require('../../util/profile-image') 2 | const he = require('he') 3 | const coverSrc = require('../../util/cover-src') 4 | 5 | const artistService = (ctx) => { 6 | const single = async (item) => { 7 | const o = Object.assign({}, item.get ? item.get({ plain: true }) : item) 8 | o.displayName = he.decode(o.displayName) 9 | 10 | if (item.banner && item.banner !== '00000000-0000-0000-0000-000000000000') { 11 | o.banner = { 12 | small: coverSrc(o.banner, '625', '.jpg'), 13 | medium: coverSrc(o.banner, '1250', '.jpg'), 14 | large: coverSrc(o.banner, '2500', '.jpg') 15 | } 16 | } 17 | 18 | if (item.avatar && item.avatar !== '00000000-0000-0000-0000-000000000000') { 19 | o.avatar = { 20 | xxs: coverSrc(o.avatar, '60', '.jpg'), 21 | xs: coverSrc(o.avatar, '120', '.jpg'), 22 | s: coverSrc(o.avatar, '300', '.jpg'), 23 | m: coverSrc(o.avatar, '600', '.jpg'), 24 | l: coverSrc(o.avatar, '960', '.jpg'), 25 | xl: coverSrc(o.avatar, '1200', '.jpg'), 26 | xxl: coverSrc(o.avatar, '1500', '.jpg') 27 | } 28 | } 29 | 30 | o.images = item.owner?.legacyId 31 | ? await resolveProfileImage({ 32 | legacyId: item.owner.legacyId, 33 | userGroupId: item.id 34 | }) 35 | : {} 36 | 37 | return o 38 | } 39 | 40 | return { 41 | single, 42 | list: async (rows) => { 43 | return Promise.all(rows.map(single)) 44 | } 45 | } 46 | } 47 | 48 | module.exports = artistService 49 | -------------------------------------------------------------------------------- /src/controllers/artists/routes/featured.js: -------------------------------------------------------------------------------- 1 | const models = require('../../../db/models') 2 | const { UserGroup, TrackGroup } = models 3 | const artistService = require('../artistService') 4 | 5 | module.exports = function () { 6 | const operations = { 7 | GET 8 | } 9 | 10 | async function GET (ctx, next) { 11 | if (await ctx.cashed?.()) return 12 | 13 | try { 14 | // FIXME: it probably makes sense to have artists actually be 15 | // featured on the model directly rather than just whether their 16 | // track groups are? Dunno. 17 | 18 | const result = await UserGroup.scope('public').findAll({ 19 | include: [{ 20 | model: TrackGroup, 21 | as: 'trackgroups', 22 | where: { 23 | private: false, 24 | featured: true, 25 | enabled: true 26 | } 27 | }], 28 | limit: 10 29 | }) 30 | 31 | const data = await artistService(ctx).list(result) 32 | 33 | ctx.body = { 34 | data: data 35 | } 36 | } catch (err) { 37 | ctx.throw(ctx.status, err.message) 38 | } 39 | } 40 | 41 | GET.apiDoc = { 42 | operationId: 'getFeatured', 43 | description: 'Returns featured artists', 44 | tags: ['artists'], 45 | responses: { 46 | 200: { 47 | description: 'The requested featured artists results.', 48 | schema: { 49 | type: 'object' 50 | } 51 | }, 52 | 404: { 53 | description: 'No results were found.' 54 | } 55 | } 56 | } 57 | 58 | return operations 59 | } 60 | -------------------------------------------------------------------------------- /src/controllers/artists/routes/{id}/index.js: -------------------------------------------------------------------------------- 1 | const models = require('../../../../db/models') 2 | const artistService = require('../../artistService') 3 | const { UserGroup } = models 4 | 5 | module.exports = function () { 6 | const operations = { 7 | GET, 8 | parameters: [ 9 | { 10 | name: 'id', 11 | in: 'path', 12 | type: 'string', 13 | required: true, 14 | description: 'User Group uuid.', 15 | format: 'uuid' 16 | } 17 | ] 18 | } 19 | 20 | async function GET (ctx, next) { 21 | if (await ctx.cashed?.()) return 22 | 23 | try { 24 | const result = await UserGroup.scope('public').findOne({ 25 | where: { 26 | id: ctx.params.id 27 | } 28 | }) 29 | 30 | if (!result) { 31 | ctx.throw(404, 'Artist not found') 32 | } 33 | 34 | ctx.body = { 35 | data: await artistService(ctx).single(result) 36 | } 37 | } catch (err) { 38 | ctx.throw(ctx.status, err.message) 39 | } 40 | 41 | await next() 42 | } 43 | 44 | GET.apiDoc = { 45 | operationId: 'getArtist', 46 | description: 'Returns a single artist', 47 | tags: ['artists'], 48 | parameters: [ 49 | { 50 | name: 'id', 51 | in: 'path', 52 | type: 'string', 53 | required: true, 54 | description: 'Artist uuid', 55 | format: 'uuid' 56 | } 57 | ], 58 | responses: { 59 | 200: { 60 | description: 'The requested artist profile.', 61 | schema: { 62 | type: 'object' 63 | } 64 | }, 65 | 404: { 66 | description: 'No artist profile found.' 67 | }, 68 | default: { 69 | description: 'Unexpected error', 70 | schema: { 71 | $ref: '#/definitions/Error' 72 | } 73 | } 74 | } 75 | } 76 | 77 | return operations 78 | } 79 | -------------------------------------------------------------------------------- /src/controllers/labels/routes/{id}/index.js: -------------------------------------------------------------------------------- 1 | const { UserGroup, UserGroupLink } = require('../../../../db/models') 2 | 3 | const artistService = require('../../../artists/artistService') 4 | 5 | module.exports = function () { 6 | const operations = { 7 | GET, 8 | parameters: [ 9 | { 10 | name: 'id', 11 | in: 'path', 12 | type: 'string', 13 | required: true, 14 | description: 'Label id.', 15 | format: 'uuid' 16 | } 17 | ] 18 | } 19 | 20 | async function GET (ctx, next) { 21 | if (await ctx.cashed?.()) return 22 | 23 | try { 24 | const result = await UserGroup.scope('public').findOne({ 25 | where: { 26 | id: ctx.params.id 27 | }, 28 | include: [{ 29 | model: UserGroupLink, 30 | as: 'links' 31 | }] 32 | }) 33 | 34 | if (!result) { 35 | ctx.status = 404 36 | ctx.throw(ctx.status, 'Not found') 37 | } 38 | 39 | ctx.body = { 40 | data: await artistService(ctx).single(result) 41 | } 42 | } catch (err) { 43 | ctx.throw(ctx.status, err.message) 44 | } 45 | 46 | await next() 47 | } 48 | 49 | GET.apiDoc = { 50 | operationId: 'getLabel', 51 | description: 'Returns a single label', 52 | tags: ['labels'], 53 | parameters: [ 54 | { 55 | name: 'id', 56 | in: 'path', 57 | type: 'string', 58 | required: true, 59 | description: 'Label id', 60 | format: 'uuid' 61 | } 62 | ], 63 | responses: { 64 | 200: { 65 | description: 'The requested label profile.', 66 | schema: { 67 | type: 'object' 68 | } 69 | }, 70 | 404: { 71 | description: 'No label profile found.' 72 | }, 73 | default: { 74 | description: 'Unexpected error', 75 | schema: { 76 | $ref: '#/definitions/Error' 77 | } 78 | } 79 | } 80 | } 81 | 82 | return operations 83 | } 84 | -------------------------------------------------------------------------------- /src/controllers/playlists/services/playlistService.js: -------------------------------------------------------------------------------- 1 | const coverSrc = require('../../../util/cover-src') 2 | const trackService = require('../../tracks/services/trackService') 3 | 4 | const playlistService = (ctx) => { 5 | const single = (data) => { 6 | const ext = '.jpg' 7 | 8 | // if (ctx.accepts('image/webp')) { 9 | // ext = '.webp' 10 | // } 11 | 12 | const variants = [120, 600, 1500] 13 | 14 | return { 15 | about: data.about, 16 | cover: coverSrc(data.cover, !data.cover_metadata ? '600' : '1500', ext, !data.cover_metadata), 17 | cover_metadata: { 18 | id: data.cover 19 | }, 20 | creator: data.creator, 21 | creatorId: data.creator?.id ?? data.creatorId, 22 | id: data.id, 23 | items: data.items?.map((item) => { 24 | const track = trackService(ctx).single(item.track) 25 | return { 26 | index: item.index, 27 | track 28 | } 29 | }), 30 | images: variants.reduce((o, key) => { 31 | const variant = ['small', 'medium', 'large'][variants.indexOf(key)] 32 | 33 | return Object.assign(o, 34 | { 35 | [variant]: { 36 | width: key, 37 | height: key, 38 | url: coverSrc(data.cover, key, ext, !data.cover_metadata) 39 | } 40 | } 41 | ) 42 | }, {}), 43 | private: data.private, 44 | featured: data.featured, 45 | tags: data.tags, 46 | title: data.title 47 | } 48 | } 49 | return { 50 | single, 51 | list (rows) { 52 | return rows.map(single) 53 | } 54 | } 55 | } 56 | 57 | module.exports = playlistService 58 | -------------------------------------------------------------------------------- /src/controllers/stream/audio.{id}.{segment}.mjs: -------------------------------------------------------------------------------- 1 | import { fetchFile, findTrack } from '../user/stream/routes/audio.{id}.{segment}.mjs' 2 | 3 | export default function () { 4 | const operations = { 5 | GET, 6 | parameters: [ 7 | { name: 'id', in: 'path', type: 'string', required: true, description: 'stream id', format: 'uuid' } 8 | ] 9 | } 10 | 11 | async function GET (ctx, next) { 12 | const { id, segment } = ctx.params 13 | 14 | try { 15 | const track = await findTrack(id, ctx) 16 | await fetchFile(ctx, 17 | track.url, 18 | segment === 'playlist.m3u8' && track.get('status') !== 'free' 19 | ? 'trim-playlist.m3u8' 20 | : segment) 21 | } catch (err) { 22 | ctx.throw(ctx.status, err.message) 23 | } 24 | 25 | await next() 26 | } 27 | 28 | return operations 29 | } 30 | -------------------------------------------------------------------------------- /src/controllers/trackgroups/services/trackgroupService.js: -------------------------------------------------------------------------------- 1 | const { apiRoot } = require('../../../constants') 2 | const coverSrc = require('../../../util/cover-src') 3 | const trackService = require('../../tracks/services/trackService') 4 | 5 | const trackgroupService = (ctx) => { 6 | const ext = '.jpg' 7 | 8 | // if (ctx.accepts('image/webp')) { 9 | // ext = '.webp' 10 | // } 11 | 12 | const variants = [120, 600, 1500] 13 | 14 | const single = (trackgroup) => { 15 | const o = Object.assign({}, trackgroup.get ? trackgroup.get({ plain: true }) : trackgroup) 16 | 17 | o.slug = trackgroup.slug 18 | o.cover_metadata = trackgroup.cover 19 | ? { 20 | id: trackgroup.cover 21 | } 22 | : null 23 | 24 | o.uri = `${process.env.APP_HOST}${apiRoot}/trackgroups/${trackgroup.id}` 25 | o.createdAt = trackgroup.createdAt 26 | o.tags = trackgroup.tags 27 | 28 | o.items = trackgroup.items?.map((item) => { 29 | item.track.trackOn = [{ 30 | trackGroup: { 31 | title: trackgroup.title, 32 | id: trackgroup.id 33 | } 34 | }] 35 | const track = trackService(ctx).single(item.track) 36 | return { 37 | index: item.index, 38 | trackId: item.track.id, 39 | track 40 | } 41 | }) 42 | 43 | o.cover = coverSrc(trackgroup.cover, '600', ext, !trackgroup.cover_metadata) 44 | o.images = variants.reduce((o, key) => { 45 | const variant = ['small', 'medium', 'large'][variants.indexOf(key)] 46 | 47 | return Object.assign(o, 48 | { 49 | [variant]: { 50 | width: key, 51 | height: key, 52 | url: coverSrc(trackgroup.cover, key, ext, !trackgroup.cover_metadata) 53 | } 54 | } 55 | ) 56 | }, {}) 57 | 58 | return o 59 | } 60 | 61 | return { 62 | single, 63 | list (rows) { 64 | return rows.map(single) 65 | } 66 | } 67 | } 68 | 69 | module.exports = trackgroupService 70 | -------------------------------------------------------------------------------- /src/controllers/user/admin/earnings.js: -------------------------------------------------------------------------------- 1 | 2 | const { authenticate, hasAccess } = require('../authenticate') 3 | const { findOneArtistEarnings } = require('../../../scripts/reports/earnings') 4 | 5 | module.exports = () => { 6 | const operations = { 7 | POST: [authenticate, hasAccess('admin'), POST] 8 | } 9 | 10 | async function POST (ctx, next) { 11 | const body = ctx.request.body 12 | 13 | try { 14 | const { from: periodStart, to: periodEnd } = body.date 15 | const { creatorId } = body 16 | 17 | const { report, sums } = await findOneArtistEarnings(periodStart, periodEnd, creatorId) 18 | 19 | ctx.body = { 20 | status: 'ok', 21 | data: report, 22 | stats: sums 23 | } 24 | } catch (err) { 25 | console.error(err) 26 | ctx.throw(ctx.status, err.message) 27 | } 28 | 29 | await next() 30 | } 31 | 32 | // FIXME: is this actually doing validation? 33 | POST.apiDoc = { 34 | operationId: 'generateEarningsReport', 35 | description: 'Generate an earnings report for an artist', 36 | tags: ['admin'], 37 | parameters: [ 38 | { 39 | in: 'body', 40 | name: 'date', 41 | schema: { 42 | type: 'object', 43 | properties: { 44 | from: { 45 | type: 'string', 46 | format: 'date' 47 | }, 48 | to: { 49 | type: 'string', 50 | format: 'date' 51 | } 52 | } 53 | } 54 | }, { 55 | in: 'body', 56 | name: 'creatorId', 57 | schema: { 58 | type: 'string', 59 | format: 'uuid' 60 | } 61 | } 62 | ], 63 | responses: { 64 | 200: { 65 | description: 'The requested earning report for an user', 66 | schema: { 67 | type: 'object' 68 | } 69 | }, 70 | 404: { 71 | description: 'No results were found.' 72 | } 73 | } 74 | } 75 | 76 | return operations 77 | } 78 | -------------------------------------------------------------------------------- /src/controllers/user/admin/files/index.mjs: -------------------------------------------------------------------------------- 1 | import { authenticate, hasAccess } from '../../authenticate.js' 2 | import db from '../../../../db/models/index.js' 3 | const { File } = db 4 | 5 | export default () => { 6 | const operations = { 7 | GET: [authenticate, hasAccess('admin'), GET] 8 | } 9 | 10 | // FIXME: documentation 11 | async function GET (ctx, next) { 12 | const { limit = 100, page = 1 } = ctx.request.query 13 | 14 | try { 15 | const { rows: result, count } = await File.findAndCountAll({ 16 | limit, 17 | offset: page > 1 ? page * limit : 0 18 | }) 19 | 20 | ctx.body = { 21 | data: result, 22 | count: count, 23 | numberOfPages: Math.ceil(count / limit), 24 | status: 'ok' 25 | } 26 | } catch (err) { 27 | ctx.throw(ctx.status, err.message) 28 | } 29 | 30 | await next() 31 | } 32 | 33 | GET.apiDoc = { 34 | operationId: 'getFiles', 35 | description: 'Returns files', 36 | summary: 'Find files', 37 | tags: ['admin'], 38 | produces: [ 39 | 'application/json' 40 | ], 41 | responses: { 42 | 400: { 43 | description: 'Bad request', 44 | schema: { 45 | $ref: '#/responses/BadRequest' 46 | } 47 | }, 48 | 404: { 49 | description: 'Not found', 50 | schema: { 51 | $ref: '#/responses/NotFound' 52 | } 53 | }, 54 | default: { 55 | description: 'error payload', 56 | schema: { 57 | $ref: '#/definitions/Error' 58 | } 59 | } 60 | }, 61 | parameters: [ 62 | { 63 | description: 'The maximum number of results to return', 64 | in: 'query', 65 | name: 'limit', 66 | type: 'integer', 67 | maximum: 100 68 | }, 69 | { 70 | type: 'integer', 71 | description: 'The current page', 72 | in: 'query', 73 | name: 'page', 74 | minimum: 1 75 | } 76 | ] 77 | } 78 | 79 | return operations 80 | } 81 | -------------------------------------------------------------------------------- /src/controllers/user/admin/files/{id}.mjs: -------------------------------------------------------------------------------- 1 | import { authenticate, hasAccess } from '../../authenticate.js' 2 | import db from '../../../../db/models/index.js' 3 | const { File } = db 4 | 5 | export default () => { 6 | const operations = { 7 | PUT: [authenticate, hasAccess('admin'), PUT] 8 | } 9 | 10 | async function PUT (ctx, next) { 11 | const body = ctx.request.body 12 | 13 | try { 14 | const [, file] = await File.update(body, { 15 | where: { 16 | id: ctx.params.id 17 | }, 18 | returning: true, 19 | plain: true 20 | }) 21 | 22 | ctx.status = 201 23 | ctx.body = { 24 | data: file, 25 | status: 'ok' 26 | } 27 | } catch (err) { 28 | ctx.throw(ctx.status, err.message) 29 | } 30 | 31 | await next() 32 | } 33 | 34 | PUT.apiDoc = { 35 | operationId: 'updateFile', 36 | description: 'Update a file', 37 | tags: ['admin'], 38 | parameters: [ 39 | { 40 | name: 'id', 41 | in: 'path', 42 | type: 'string', 43 | required: true, 44 | description: 'file uuid', 45 | format: 'uuid' 46 | }, 47 | { 48 | in: 'body', 49 | name: 'file', 50 | schema: { 51 | type: 'object', 52 | additionalProperties: false, 53 | // required: [], 54 | properties: { 55 | filename: { 56 | type: 'string' 57 | } 58 | } 59 | } 60 | } 61 | ], 62 | responses: { 63 | 200: { 64 | description: 'The updated playlist', 65 | schema: { 66 | type: 'object' 67 | } 68 | }, 69 | 404: { 70 | description: 'No playlist found.' 71 | }, 72 | default: { 73 | description: 'Unexpected error', 74 | schema: { 75 | $ref: '#/definitions/Error' 76 | } 77 | } 78 | } 79 | } 80 | 81 | return operations 82 | } 83 | -------------------------------------------------------------------------------- /src/controllers/user/earnings.js: -------------------------------------------------------------------------------- 1 | const { findOneArtistEarnings } = require('../../scripts/reports/earnings') 2 | const { authenticate } = require('./authenticate') 3 | 4 | module.exports = function () { 5 | const operations = { 6 | POST: [authenticate, POST] 7 | } 8 | 9 | async function POST (ctx, next) { 10 | const body = ctx.request.body 11 | 12 | try { 13 | const { from: periodStart, to: periodEnd } = body.date 14 | 15 | const { report, sums } = await findOneArtistEarnings(periodStart, periodEnd, ctx.profile.id) 16 | 17 | ctx.body = { 18 | status: 'ok', 19 | data: report, 20 | stats: sums 21 | } 22 | } catch (err) { 23 | console.error(err) 24 | ctx.throw(ctx.status, err.message) 25 | } 26 | 27 | await next() 28 | } 29 | 30 | POST.apiDoc = { 31 | operationId: 'generateEarningsReport', 32 | description: 'Generate an earnings report for user', 33 | tags: ['user'], 34 | parameters: [ 35 | { 36 | in: 'body', 37 | name: 'date', 38 | schema: { 39 | type: 'object', 40 | properties: { 41 | from: { 42 | type: 'string', 43 | format: 'date' 44 | }, 45 | to: { 46 | type: 'string', 47 | format: 'date' 48 | } 49 | } 50 | } 51 | }, { 52 | in: 'body', 53 | name: 'creatorId', 54 | schema: { 55 | type: 'string', 56 | format: 'uuid' 57 | } 58 | } 59 | ], 60 | responses: { 61 | 200: { 62 | description: 'The requested earning report for the user', 63 | schema: { 64 | type: 'object' 65 | } 66 | }, 67 | 404: { 68 | description: 'No results were found.' 69 | } 70 | } 71 | } 72 | 73 | return operations 74 | } 75 | -------------------------------------------------------------------------------- /src/controllers/user/favorites/routes/resolve.js: -------------------------------------------------------------------------------- 1 | const { Favorite, Resonate: sequelize } = require('../../../../db/models') 2 | const { Op } = require('sequelize') 3 | const { authenticate } = require('../../authenticate') 4 | 5 | module.exports = function () { 6 | const operations = { 7 | GET: [authenticate, GET] 8 | } 9 | 10 | async function GET (ctx, next) { 11 | const query = ctx.request.query 12 | 13 | try { 14 | const result = await Favorite.findAll({ 15 | attributes: ['trackId'], 16 | where: { 17 | type: true, 18 | userId: ctx.profile.id, 19 | trackId: { 20 | [Op.in]: query.ids 21 | } 22 | }, 23 | group: [ 24 | sequelize.col('trackId') 25 | ], 26 | raw: true 27 | }) 28 | 29 | ctx.body = { 30 | data: result 31 | } 32 | } catch (err) { 33 | console.error('err', err) 34 | ctx.status = err.status || 500 35 | ctx.throw(ctx.status, err.message) 36 | } 37 | } 38 | 39 | GET.apiDoc = { 40 | operationId: 'resolveFavorites', 41 | description: 'Determine which tracks are favorited from supplied track IDs', 42 | tags: ['favorites'], 43 | parameters: [ 44 | { 45 | in: 'query', 46 | name: 'ids', 47 | required: true, 48 | description: 'IDs to check for whether they\'re favorites', 49 | type: 'array', 50 | items: { 51 | type: 'string', 52 | format: 'uuid' 53 | } 54 | } 55 | ], 56 | responses: { 57 | 200: { 58 | description: 'The favorites status', 59 | schema: { 60 | type: 'object' 61 | } 62 | }, 63 | 404: { 64 | description: 'No favorites found.' 65 | }, 66 | default: { 67 | description: 'Unexpected error', 68 | schema: { 69 | $ref: '#/definitions/Error' 70 | } 71 | } 72 | } 73 | } 74 | 75 | return operations 76 | } 77 | -------------------------------------------------------------------------------- /src/controllers/user/files/index.mjs: -------------------------------------------------------------------------------- 1 | import { authenticate } from '../authenticate.js' 2 | import db from '../../../db/models/index.js' 3 | const { File } = db 4 | 5 | export default () => { 6 | const operations = { 7 | GET: [authenticate, GET] 8 | } 9 | 10 | // FIXME: documentation 11 | async function GET (ctx, next) { 12 | const { limit = 100, page = 1 } = ctx.request.query 13 | 14 | try { 15 | const { rows: result, count } = await File.findAndCountAll({ 16 | limit, 17 | where: { 18 | ownerId: ctx.profile.id 19 | }, 20 | offset: page > 1 ? page * limit : 0 21 | }) 22 | 23 | ctx.body = { 24 | data: result, 25 | count: count, 26 | numberOfPages: Math.ceil(count / limit), 27 | status: 'ok' 28 | } 29 | } catch (err) { 30 | ctx.throw(ctx.status, err.message) 31 | } 32 | 33 | await next() 34 | } 35 | 36 | GET.apiDoc = { 37 | operationId: 'getFiles', 38 | description: 'Returns files', 39 | summary: 'Find files', 40 | tags: ['admin'], 41 | produces: [ 42 | 'application/json' 43 | ], 44 | responses: { 45 | 400: { 46 | description: 'Bad request', 47 | schema: { 48 | $ref: '#/responses/BadRequest' 49 | } 50 | }, 51 | 404: { 52 | description: 'Not found', 53 | schema: { 54 | $ref: '#/responses/NotFound' 55 | } 56 | }, 57 | default: { 58 | description: 'error payload', 59 | schema: { 60 | $ref: '#/definitions/Error' 61 | } 62 | } 63 | }, 64 | parameters: [ 65 | { 66 | description: 'The maximum number of results to return', 67 | in: 'query', 68 | name: 'limit', 69 | type: 'integer', 70 | maximum: 100 71 | }, 72 | { 73 | type: 'integer', 74 | description: 'The current page', 75 | in: 'query', 76 | name: 'page', 77 | minimum: 1 78 | } 79 | ] 80 | } 81 | 82 | return operations 83 | } 84 | -------------------------------------------------------------------------------- /src/controllers/user/files/{id}.mjs: -------------------------------------------------------------------------------- 1 | import { authenticate } from '../authenticate.js' 2 | import db from '../../../db/models/index.js' 3 | const { File } = db 4 | 5 | export default () => { 6 | const operations = { 7 | PUT: [authenticate, PUT] 8 | } 9 | 10 | async function PUT (ctx, next) { 11 | const body = ctx.request.body 12 | 13 | try { 14 | const [, file] = await File.update(body, { 15 | where: { 16 | id: ctx.params.id, 17 | ownerId: ctx.profile.id 18 | }, 19 | returning: true, 20 | plain: true 21 | }) 22 | 23 | ctx.status = 201 24 | ctx.body = { 25 | data: file, 26 | status: 'ok' 27 | } 28 | } catch (err) { 29 | ctx.throw(ctx.status, err.message) 30 | } 31 | 32 | await next() 33 | } 34 | 35 | PUT.apiDoc = { 36 | operationId: 'updateFile', 37 | description: 'Update a file', 38 | tags: ['admin'], 39 | parameters: [ 40 | { 41 | name: 'id', 42 | in: 'path', 43 | type: 'string', 44 | required: true, 45 | description: 'file uuid', 46 | format: 'uuid' 47 | }, 48 | { 49 | in: 'body', 50 | name: 'file', 51 | schema: { 52 | type: 'object', 53 | additionalProperties: false, 54 | // required: [], 55 | properties: { 56 | filename: { 57 | type: 'string' 58 | } 59 | } 60 | } 61 | } 62 | ], 63 | responses: { 64 | 200: { 65 | description: 'The updated playlist', 66 | schema: { 67 | type: 'object' 68 | } 69 | }, 70 | 404: { 71 | description: 'No playlist found.' 72 | }, 73 | default: { 74 | description: 'Unexpected error', 75 | schema: { 76 | $ref: '#/definitions/Error' 77 | } 78 | } 79 | } 80 | } 81 | 82 | return operations 83 | } 84 | -------------------------------------------------------------------------------- /src/controllers/user/playlists/routes/{id}/cover.js: -------------------------------------------------------------------------------- 1 | 2 | const { Playlist } = require('../../../../../db/models') 3 | const { processFile } = require('../../../../../util/process-file') 4 | const { authenticate } = require('../../../authenticate') 5 | 6 | module.exports = function () { 7 | const operations = { PUT: [authenticate, PUT] } 8 | 9 | async function PUT (ctx, next) { 10 | try { 11 | const file = ctx.request.files.file 12 | // TODO: Remove prior files 13 | const data = Array.isArray(file) 14 | ? await Promise.all(file.map(processFile(ctx))) 15 | : await processFile(ctx)(file) 16 | 17 | const trackgroup = await Playlist.findOne({ where: { id: ctx.request.params.id } }) 18 | trackgroup.set('cover', data.filename) 19 | await trackgroup.save() 20 | await trackgroup.reload() 21 | ctx.body = { 22 | data: trackgroup, 23 | status: 'ok' 24 | } 25 | await next() 26 | } catch (err) { 27 | ctx.status = 500 28 | ctx.throw(ctx.status, err.message) 29 | } 30 | } 31 | 32 | PUT.apiDoc = { 33 | operationId: 'updatePlaylistCover', 34 | description: 'Add cover to playlist', 35 | summary: '', 36 | tags: ['playlists'], 37 | // parameters: [ 38 | // { 39 | // name: 'id', 40 | // in: 'path', 41 | // type: 'string', 42 | // required: true, 43 | // description: 'Track uuid', 44 | // format: 'uuid' 45 | // } 46 | // ], 47 | responses: { 48 | 200: { 49 | description: 'The updated playlist', 50 | schema: { 51 | type: 'object' 52 | } 53 | }, 54 | 404: { 55 | description: 'No playlist found.' 56 | }, 57 | default: { 58 | description: 'Unexpected error', 59 | schema: { 60 | $ref: '#/definitions/Error' 61 | } 62 | } 63 | } 64 | } 65 | 66 | return operations 67 | } 68 | -------------------------------------------------------------------------------- /src/controllers/user/plays/routes/history/artists.mjs: -------------------------------------------------------------------------------- 1 | import models from '../../../../../db/models/index.js' 2 | import { authenticate } from '../../../authenticate.js' 3 | const { UserGroup, Resonate: sequelize } = models 4 | 5 | export default function () { 6 | const operations = { 7 | GET: [authenticate, GET] 8 | } 9 | 10 | async function GET (ctx, next) { 11 | const { 12 | limit = 10 13 | } = ctx.request.query 14 | 15 | try { 16 | const result = await sequelize.query(` 17 | SELECT ug.id as "creatorId", ug.display_name as "displayName", count(*) 18 | FROM user_groups ug 19 | LEFT JOIN tracks ON tracks.creator_id = ug.id 20 | LEFT JOIN plays ON plays.track_id = tracks.id 21 | WHERE plays.user_id = :userId 22 | GROUP BY ug.id 23 | LIMIT :limit 24 | `, { 25 | type: sequelize.QueryTypes.SELECT, 26 | mapToModel: true, 27 | replacements: { 28 | userId: ctx.profile.id, 29 | limit 30 | }, 31 | model: UserGroup, 32 | raw: true 33 | }) 34 | 35 | ctx.body = { 36 | data: result 37 | } 38 | } catch (err) { 39 | console.error('endpoint error') 40 | ctx.status = err.status || 500 41 | ctx.throw(ctx.status, err.message) 42 | } 43 | 44 | await next() 45 | } 46 | 47 | GET.apiDoc = { 48 | operationId: 'getLatestPlayedArtists', 49 | description: 'Returns latest played artists', 50 | tags: ['plays'], 51 | parameters: [ 52 | { 53 | description: 'The maximum number of results to return', 54 | in: 'query', 55 | name: 'limit', 56 | type: 'integer' 57 | } 58 | ], 59 | responses: { 60 | 200: { 61 | description: 'The requested search results.', 62 | schema: { 63 | type: 'object' 64 | } 65 | }, 66 | 404: { 67 | description: 'No results were found.' 68 | } 69 | } 70 | } 71 | 72 | return operations 73 | } 74 | -------------------------------------------------------------------------------- /src/controllers/user/products/routes/cancel.js: -------------------------------------------------------------------------------- 1 | // const { User, Role, OauthUser /* Resonate: sequelize */ } = require('../../../../db/models') 2 | // const { Op } = require('sequelize') 3 | 4 | // const stripe = require('stripe')(process.env.STRIPE_KEY) 5 | 6 | module.exports = function () { 7 | const operations = { 8 | GET: [GET] 9 | } 10 | 11 | async function GET (ctx, next) { 12 | try { 13 | ctx.body = { 14 | data: {} 15 | } 16 | } catch (err) { 17 | console.error('err', err) 18 | ctx.status = err.status 19 | ctx.throw(ctx.status, err.message) 20 | } 21 | 22 | await next() 23 | } 24 | 25 | return operations 26 | } 27 | -------------------------------------------------------------------------------- /src/controllers/user/products/routes/index.js: -------------------------------------------------------------------------------- 1 | const stripe = require('stripe')(process.env.STRIPE_KEY) 2 | const { authenticate } = require('../../authenticate') 3 | 4 | module.exports = function () { 5 | const operations = { 6 | GET: [authenticate, GET] 7 | } 8 | 9 | async function GET (ctx, next) { 10 | try { 11 | const products = await stripe.products.list({ limit: 50 }) 12 | 13 | ctx.body = { 14 | data: products.data, 15 | status: 'ok' 16 | } 17 | } catch (err) { 18 | console.error('err', err) 19 | ctx.status = err.status 20 | ctx.throw(ctx.status, err.message) 21 | } 22 | 23 | await next() 24 | } 25 | 26 | GET.apiDoc = { 27 | operationId: 'getProducts', 28 | description: 'Get products', 29 | summary: 'Get products', 30 | tags: ['products'], 31 | produces: [ 32 | 'application/json' 33 | ], 34 | responses: { 35 | 400: { 36 | description: 'Bad request', 37 | schema: { 38 | $ref: '#/responses/BadRequest' 39 | } 40 | }, 41 | 404: { 42 | description: 'Not found', 43 | schema: { 44 | $ref: '#/responses/NotFound' 45 | } 46 | }, 47 | default: { 48 | description: 'error payload', 49 | schema: { 50 | $ref: '#/definitions/Error' 51 | } 52 | } 53 | } 54 | 55 | } 56 | 57 | return operations 58 | } 59 | -------------------------------------------------------------------------------- /src/controllers/user/products/routes/success.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function () { 3 | const operations = { 4 | GET: [GET] 5 | } 6 | 7 | async function GET (ctx, next) { 8 | try { 9 | const response = ctx.request.query 10 | 11 | ctx.status = 303 12 | ctx.redirect(response.callbackURL) 13 | } catch (err) { 14 | console.error('err', err) 15 | ctx.status = err.status 16 | ctx.throw(ctx.status, err.message) 17 | } 18 | 19 | await next() 20 | } 21 | 22 | return operations 23 | } 24 | -------------------------------------------------------------------------------- /src/controllers/user/profileRedirect.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const Router = require('@koa/router') 3 | 4 | const profileRedirect = new Koa() 5 | const router = new Router() 6 | 7 | router.get('/', async (ctx, next) => { 8 | const redirectUrl = new URL('/profile', process.env.OAUTH_HOST) 9 | 10 | redirectUrl.search = new URLSearchParams({ 11 | client_id: process.env.OAUTH_CLIENT, 12 | response_type: 'code', 13 | redirect_uri: `https://${process.env.APP_HOST}/api/user/connect/resonate/callback`, 14 | scope: 'stream2own', 15 | state: ctx.session.grant.state 16 | }) 17 | 18 | ctx.redirect(redirectUrl.href) 19 | 20 | await next() 21 | }) 22 | 23 | profileRedirect 24 | .use(router.routes()) 25 | .use(router.allowedMethods({ 26 | throw: true 27 | })) 28 | 29 | module.exports = profileRedirect 30 | -------------------------------------------------------------------------------- /src/controllers/user/stream_legacy.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const path = require('path') 3 | const send = require('koa-send') 4 | const { promises: fs } = require('fs') 5 | const { File, Track } = require('../../db/models') 6 | const Router = require('@koa/router') 7 | const { isEnv } = require('../../util/dev') 8 | 9 | const BASE_DATA_DIR = process.env.BASE_DATA_DIR || '/' 10 | 11 | const stream = new Koa() 12 | const router = new Router() 13 | 14 | /** 15 | * WIP rewrite legacy stream endpoint 16 | */ 17 | 18 | router.get('/:id', isEnv(['development', 'test']), async (ctx, next) => { 19 | try { 20 | const track = await Track.findOne({ 21 | where: { 22 | id: ctx.params.id 23 | }, 24 | include: [ 25 | { 26 | required: true, // associated file is required 27 | model: File, 28 | attributes: ['id', 'size', 'owner_id'], 29 | as: 'audiofile' 30 | } 31 | ] 32 | }) 33 | 34 | if (!track) { 35 | ctx.status = 404 36 | ctx.throw(ctx.status, 'Not found') 37 | } 38 | 39 | // TODO check if listener can pay or owns the track (ctx.profile.id) 40 | 41 | const fileid = track.audiofile.id 42 | const filename = track.audiofile.filename || `${track.title} - ${track.artist}` 43 | const filesize = track.audiofile.size 44 | 45 | await fs.stat(path.join(BASE_DATA_DIR, '/data/media/audio', fileid + '.m4a')) 46 | 47 | ctx.set({ 48 | Pragma: 'no-cache', 49 | 'Content-Type': 'audio/mp4', 50 | 'Content-Length': filesize, 51 | 'Content-Disposition': `inline; filename=${filename}` 52 | }) 53 | 54 | await send(ctx, `/${fileid}.m4a`, { root: path.join(BASE_DATA_DIR, '/data/media/audio') }) 55 | } catch (err) { 56 | console.error(err) 57 | ctx.throw(ctx.status, err.message) 58 | } 59 | 60 | await next() 61 | }) 62 | 63 | stream 64 | .use(router.routes()) 65 | .use(router.allowedMethods({ 66 | throw: true 67 | })) 68 | 69 | module.exports = stream 70 | -------------------------------------------------------------------------------- /src/controllers/user/trackgroups/routes/{id}/cover.js: -------------------------------------------------------------------------------- 1 | 2 | const { TrackGroup } = require('../../../../../db/models') 3 | const { processFile } = require('../../../../../util/process-file') 4 | const { authenticate } = require('../../../authenticate') 5 | 6 | module.exports = function () { 7 | const operations = { 8 | PUT: [authenticate, PUT] 9 | } 10 | 11 | async function PUT (ctx, next) { 12 | try { 13 | const file = ctx.request.files.file 14 | // TODO: Remove prior files 15 | const data = Array.isArray(file) 16 | ? await Promise.all(file.map(processFile(ctx))) 17 | : await processFile(ctx)(file) 18 | 19 | const trackgroup = await TrackGroup.findOne({ where: { id: ctx.request.params.id } }) 20 | trackgroup.set('cover', data.filename) 21 | await trackgroup.save() 22 | await trackgroup.reload() 23 | ctx.body = { 24 | data: trackgroup, 25 | status: 'ok' 26 | } 27 | await next() 28 | } catch (err) { 29 | console.error(err) 30 | ctx.status = 500 31 | ctx.throw(ctx.status, err.message) 32 | } 33 | } 34 | 35 | PUT.apiDoc = { 36 | operationId: 'updateTrackGroupCover', 37 | description: 'Add cover to trackgroup', 38 | summary: '', 39 | tags: ['trackgroups'], 40 | // parameters: [ 41 | // { 42 | // name: 'id', 43 | // in: 'path', 44 | // type: 'string', 45 | // required: true, 46 | // description: 'Track uuid', 47 | // format: 'uuid' 48 | // } 49 | // ], 50 | responses: { 51 | 200: { 52 | description: 'The updated trackgroup', 53 | schema: { 54 | type: 'object' 55 | } 56 | }, 57 | 404: { 58 | description: 'No trackgroup found.' 59 | }, 60 | default: { 61 | description: 'Unexpected error', 62 | schema: { 63 | $ref: '#/definitions/Error' 64 | } 65 | } 66 | } 67 | } 68 | 69 | return operations 70 | } 71 | -------------------------------------------------------------------------------- /src/controllers/user/tracks/routes/{id}/file.js: -------------------------------------------------------------------------------- 1 | 2 | const { Track } = require('../../../../../db/models') 3 | const { processFile } = require('../../../../../util/process-file') 4 | const { authenticate } = require('../../../authenticate') 5 | 6 | module.exports = function () { 7 | const operations = { 8 | PUT: [authenticate, PUT] 9 | } 10 | 11 | async function PUT (ctx, next) { 12 | try { 13 | const files = ctx.request.files.files 14 | 15 | const data = Array.isArray(files) 16 | ? await Promise.all(files.map(processFile(ctx))) 17 | : await processFile(ctx)(files) 18 | 19 | const track = await Track.findOne({ where: { id: ctx.request.params.id } }) 20 | track.set('url', data.filename) 21 | track.set('hls', true) 22 | await track.save() 23 | await track.reload() 24 | ctx.body = { 25 | data: track, 26 | status: 'ok' 27 | } 28 | await next() 29 | } catch (e) { 30 | ctx.status = 500 31 | ctx.throw(ctx.status, 'Problem creating file') 32 | } 33 | } 34 | 35 | PUT.apiDoc = { 36 | operationId: 'updateTrackFile', 37 | description: 'Add a file to a track', 38 | summary: '', 39 | tags: ['tracks'], 40 | // FIXME: re-enable this 41 | // parameters: [ 42 | // { 43 | // name: 'id', 44 | // in: 'path', 45 | // type: 'string', 46 | // required: true, 47 | // description: 'Track uuid', 48 | // format: 'uuid' 49 | // } 50 | // ], 51 | responses: { 52 | 200: { 53 | description: 'The updated trackgroup', 54 | schema: { 55 | type: 'object' 56 | } 57 | }, 58 | 404: { 59 | description: 'No trackgroup found.' 60 | }, 61 | default: { 62 | description: 'Unexpected error', 63 | schema: { 64 | $ref: '#/definitions/Error' 65 | } 66 | } 67 | } 68 | } 69 | 70 | return operations 71 | } 72 | -------------------------------------------------------------------------------- /src/controllers/users/routes/{id}/index.js: -------------------------------------------------------------------------------- 1 | const { User } = require('../../../../db/models') 2 | 3 | module.exports = function () { 4 | const operations = { 5 | GET, 6 | parameters: [ 7 | { 8 | name: 'id', 9 | in: 'path', 10 | type: 'string', 11 | required: true, 12 | description: 'User id.', 13 | format: 'uuid' 14 | } 15 | ] 16 | } 17 | async function GET (ctx, next) { 18 | if (await ctx.cashed?.()) return 19 | 20 | try { 21 | const user = await User.findOne({ 22 | where: { 23 | id: ctx.params.id 24 | }, 25 | attributes: ['displayName'] 26 | }) 27 | 28 | ctx.body = { 29 | data: user.get() 30 | } 31 | } catch (err) { 32 | ctx.throw(ctx.status, err.message) 33 | } 34 | 35 | await next() 36 | } 37 | 38 | GET.apiDoc = { 39 | operationId: 'getUser', 40 | description: 'Returns a single user', 41 | tags: ['users'], 42 | parameters: [ 43 | { 44 | name: 'id', 45 | in: 'path', 46 | type: 'string', 47 | required: true, 48 | description: 'User id', 49 | format: 'uuid' 50 | } 51 | ], 52 | responses: { 53 | 200: { 54 | description: 'The requested user profile.', 55 | schema: { 56 | type: 'object' 57 | } 58 | }, 59 | 404: { 60 | description: 'No user found.' 61 | }, 62 | default: { 63 | description: 'Unexpected error', 64 | schema: { 65 | $ref: '#/definitions/Error' 66 | } 67 | } 68 | } 69 | } 70 | 71 | return operations 72 | } 73 | -------------------------------------------------------------------------------- /src/db/legacy/README.md: -------------------------------------------------------------------------------- 1 | # Legacy Migrations 2 | 3 | These files handle migrating data from the legacy databases to the current ones. 4 | 5 | > Note: for this to work you'll need SSH access to the `apiserver` and `resonate` servers. 6 | 7 | First, you'll need to make sure the database is seeded with the right roles and such. 8 | 9 | ``` 10 | docker exec -it resonate-api npx sequelize db:seed:all --config src/config/databases.js --seeders-path src/db/seeders 11 | ``` 12 | 13 | Then copy over user data from user-api 14 | 15 | ``` 16 | docker exec -it resonate-api node src/db/legacy/user-api-migration.js postgres 17 | ``` 18 | 19 | Then copy over old mysql data (from WordPress) 20 | 21 | ``` 22 | docker exec -it resonate-api node src/db/legacy/user-api-migration.js mysql 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /src/db/legacy/tunnel.js: -------------------------------------------------------------------------------- 1 | const { Client } = require('ssh2') 2 | const mysql = require('mysql2') 3 | 4 | const forwardConfig = { 5 | srcHost: '127.0.0.1', 6 | srcPort: 3306 7 | // dstHost: dbServer.host, 8 | // dstPort: dbServer.port 9 | } 10 | 11 | module.exports = (sshTunnelConfig, dbServer, destinationHost, destinationPort) => { 12 | const sshClient = new Client() 13 | 14 | return new Promise((resolve, reject) => { 15 | sshClient.on('ready', () => { 16 | console.log('ready', sshTunnelConfig) 17 | sshClient.forwardOut( 18 | forwardConfig.srcHost, 19 | forwardConfig.srcPort, 20 | destinationHost, 21 | destinationPort, 22 | (err, stream) => { 23 | console.log('err', err, stream) 24 | if (err) reject(err) 25 | const updatedDbServer = { 26 | ...dbServer, 27 | stream 28 | } 29 | console.log('updatedDB', updatedDbServer) 30 | const connection = mysql.createConnection(updatedDbServer) 31 | connection.connect((error) => { 32 | if (error) { 33 | console.log('error---', error) 34 | reject(error) 35 | } 36 | console.log('Connection Successful') 37 | resolve(connection) 38 | }) 39 | }) 40 | }).connect(sshTunnelConfig) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /src/db/legacy/user-api-migration.js: -------------------------------------------------------------------------------- 1 | const { Client: PGClient } = require('pg') 2 | const mysql = require('mysql') 3 | const tunnel = require('tunnel-ssh') 4 | const pgMigration = require('./pg-migrations') 5 | 6 | const { USER_API_DB, USER_API_SERVER, MYSQL_DB, WORDPRESS_SERVER } = require('./config.js') 7 | const mysqlMigrations = require('./mysql-migrations') 8 | 9 | const postgresProxyHost = '127.0.0.1' 10 | const postgresProxyPort = 9090 11 | const mysqlProxyPort = 9091 12 | const mysqlProxyHost = '127.0.0.1' 13 | 14 | if (process.argv[2] === 'mysql') { 15 | const server = tunnel({ 16 | ...WORDPRESS_SERVER, 17 | dstHost: MYSQL_DB.host, 18 | dstPort: MYSQL_DB.port, 19 | localHost: mysqlProxyHost, 20 | localPort: mysqlProxyPort 21 | }, async (err, server) => { 22 | if (err) { console.log(err); return } 23 | const conn = mysql.createConnection({ 24 | ...MYSQL_DB, 25 | host: mysqlProxyHost, 26 | port: mysqlProxyPort 27 | }) 28 | conn.connect() 29 | 30 | await mysqlMigrations(conn) 31 | 32 | conn.end() 33 | }) 34 | 35 | server.on('error', (err) => { 36 | console.log('error', err) 37 | }) 38 | } 39 | 40 | if (process.argv[2] === 'postgres') { 41 | const postgresServer = tunnel({ 42 | ...USER_API_SERVER, 43 | dstHost: USER_API_DB.host, 44 | dstPort: USER_API_DB.port, 45 | localHost: postgresProxyHost, 46 | localPort: postgresProxyPort 47 | }, async (err, server) => { 48 | if (err) { console.log(err); return } 49 | const pgClient = new PGClient({ ...USER_API_DB, host: postgresProxyHost, port: postgresProxyPort }) 50 | await pgClient.connect() 51 | 52 | const res = await pgClient.query('SELECT $1::text as connected', ['Connection to postgres successful!']) 53 | console.log(res.rows[0].connected) 54 | await pgMigration(pgClient) 55 | pgClient.end() 56 | }) 57 | 58 | postgresServer.on('error', (err) => { 59 | console.log('error', err) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /src/db/migrations/20191017210551-playlist.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('playlists', { 4 | id: { 5 | type: Sequelize.UUID, 6 | allowNull: false, 7 | defaultValue: Sequelize.UUIDV4, 8 | unique: true, 9 | field: 'id', 10 | primaryKey: true 11 | }, 12 | cover: { 13 | type: Sequelize.UUID, 14 | field: 'cover' 15 | }, 16 | title: { 17 | type: Sequelize.STRING, 18 | allowNull: false, 19 | field: 'title' 20 | }, 21 | about: { 22 | type: Sequelize.TEXT, 23 | field: 'about' 24 | }, 25 | private: { 26 | type: Sequelize.BOOLEAN, 27 | defaultValue: true, 28 | allowNull: false, 29 | field: 'private' 30 | }, 31 | creatorId: { 32 | type: Sequelize.UUID, 33 | field: 'creator_id' 34 | }, 35 | tags: { 36 | type: Sequelize.TEXT, 37 | field: 'tags' 38 | }, 39 | tracks: { 40 | type: Sequelize.ARRAY(Sequelize.UUID) 41 | }, 42 | featured: { 43 | type: Sequelize.BOOLEAN, 44 | defaultValue: false, 45 | field: 'featured' 46 | }, 47 | updatedAt: { 48 | field: 'updated_at', 49 | allowNull: false, 50 | type: Sequelize.DATE 51 | }, 52 | createdAt: { 53 | field: 'created_at', 54 | allowNull: false, 55 | type: Sequelize.DATE 56 | }, 57 | deletedAt: { 58 | field: 'deleted_at', 59 | type: Sequelize.DATE 60 | } 61 | }) 62 | }, 63 | 64 | down: (queryInterface, Sequelize) => { 65 | return queryInterface.dropTable('playlists') 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/db/migrations/20191017210557-playlist_item.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('playlist_items', { 4 | id: { 5 | type: Sequelize.UUID, 6 | allowNull: false, 7 | defaultValue: Sequelize.UUIDV4, 8 | unique: true, 9 | field: 'id', 10 | primaryKey: true 11 | }, 12 | index: { 13 | type: Sequelize.INTEGER, 14 | allowNull: false, 15 | defaultValue: 0, 16 | field: 'index' 17 | }, 18 | playlistId: { 19 | type: Sequelize.UUID, 20 | field: 'playlist_id' 21 | }, 22 | trackId: { 23 | type: Sequelize.UUID, 24 | field: 'track_id' 25 | }, 26 | updatedAt: { 27 | field: 'updated_at', 28 | allowNull: false, 29 | type: Sequelize.DATE 30 | }, 31 | createdAt: { 32 | field: 'created_at', 33 | allowNull: false, 34 | type: Sequelize.DATE 35 | }, 36 | deletedAt: { 37 | field: 'deleted_at', 38 | allowNull: true, 39 | type: Sequelize.DATE 40 | } 41 | }) 42 | }, 43 | 44 | down: (queryInterface, Sequelize) => { 45 | return queryInterface.dropTable('playlist_items') 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/db/migrations/20191017210557-track_group_item.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('track_group_items', { 4 | id: { 5 | type: Sequelize.UUID, 6 | allowNull: false, 7 | defaultValue: Sequelize.UUIDV4, 8 | unique: true, 9 | field: 'id', 10 | primaryKey: true 11 | }, 12 | index: { 13 | type: Sequelize.INTEGER, 14 | allowNull: false, 15 | defaultValue: 0, 16 | field: 'index' 17 | }, 18 | trackgroupId: { 19 | type: Sequelize.UUID, 20 | field: 'track_group_id' 21 | }, 22 | track_id: { 23 | type: Sequelize.UUID, 24 | field: 'track_id' 25 | }, 26 | track_performers: { 27 | type: Sequelize.STRING, 28 | field: 'track_performers' 29 | }, 30 | track_composers: { 31 | type: Sequelize.STRING, 32 | field: 'track_composers' 33 | }, 34 | updatedAt: { 35 | field: 'updated_at', 36 | allowNull: false, 37 | type: Sequelize.DATE 38 | }, 39 | createdAt: { 40 | field: 'created_at', 41 | allowNull: false, 42 | type: Sequelize.DATE 43 | }, 44 | deletedAt: { 45 | field: 'deleted_at', 46 | allowNull: true, 47 | type: Sequelize.DATE 48 | } 49 | }, 50 | { 51 | charset: 'utf8', 52 | collate: 'utf8_general_ci' 53 | }) 54 | }, 55 | 56 | down: (queryInterface, Sequelize) => { 57 | return queryInterface.dropTable('track_group_items') 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/db/migrations/20191023192727-file.js: -------------------------------------------------------------------------------- 1 | const { SUPPORTED_MEDIA_TYPES } = require('../../config/supported-media-types') 2 | const FILE_STATUS_LIST = require('../../config/file-status-list') 3 | 4 | module.exports = { 5 | up: (queryInterface, Sequelize) => { 6 | return queryInterface.createTable('files', { 7 | id: { 8 | type: Sequelize.UUID, 9 | allowNull: false, 10 | defaultValue: Sequelize.UUIDV4, 11 | unique: true, 12 | field: 'id', 13 | primaryKey: true 14 | }, 15 | filename: { 16 | type: Sequelize.STRING, 17 | field: 'filename' 18 | }, 19 | filename_prefix: { 20 | type: Sequelize.STRING, 21 | field: 'filename_prefix' 22 | }, 23 | ownerId: { 24 | type: Sequelize.UUID, 25 | allowNull: false, 26 | field: 'owner_id' 27 | }, 28 | description: { 29 | type: Sequelize.STRING, 30 | field: 'description' 31 | }, 32 | size: { 33 | type: Sequelize.INTEGER, 34 | field: 'size' 35 | }, 36 | hash: { 37 | type: Sequelize.STRING(64), 38 | field: 'hash' 39 | }, 40 | status: { 41 | type: Sequelize.ENUM, 42 | defaultValue: 'processing', 43 | allowNull: false, 44 | values: FILE_STATUS_LIST 45 | }, 46 | mime: { 47 | type: Sequelize.ENUM, 48 | values: SUPPORTED_MEDIA_TYPES, 49 | allowNull: false, 50 | field: 'mime' 51 | }, 52 | metadata: { 53 | type: Sequelize.TEXT, 54 | field: 'metadata' 55 | }, 56 | updatedAt: { 57 | field: 'updated_at', 58 | allowNull: false, 59 | type: Sequelize.DATE 60 | }, 61 | createdAt: { 62 | field: 'created_at', 63 | allowNull: false, 64 | type: Sequelize.DATE 65 | } 66 | }, 67 | { 68 | charset: 'utf8', 69 | collate: 'utf8_general_ci' 70 | }) 71 | }, 72 | 73 | down: (queryInterface, Sequelize) => { 74 | return queryInterface.dropTable('files') 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/db/migrations/20200214140232-play.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('plays', { 4 | id: { 5 | type: Sequelize.INTEGER, 6 | autoIncrement: true, 7 | field: 'id', 8 | primaryKey: true 9 | }, 10 | trackId: { 11 | type: Sequelize.UUID, 12 | field: 'track_id' 13 | }, 14 | userId: { 15 | type: Sequelize.UUID, 16 | field: 'user_id' 17 | }, 18 | type: { 19 | type: Sequelize.INTEGER, 20 | field: 'event' 21 | }, 22 | createdAt: { 23 | type: Sequelize.DATE(), 24 | defaultValue: Sequelize.fn('NOW'), 25 | field: 'created_at' 26 | } 27 | }, 28 | { 29 | charset: 'utf8', 30 | collate: 'utf8_general_ci' 31 | }) 32 | }, 33 | 34 | down: (queryInterface, Sequelize) => { 35 | return queryInterface.dropTable('plays') 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-address.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('addresses', { 4 | id: { 5 | primaryKey: true, 6 | type: DataTypes.UUID, 7 | defaultvalue: DataTypes.UUIDV4 8 | }, 9 | personalData: { 10 | type: DataTypes.BOOLEAN, 11 | defaultValue: true, 12 | field: 'personal_data' 13 | }, 14 | data: { 15 | type: DataTypes.ARRAY(DataTypes.STRING) 16 | } 17 | }) 18 | }, 19 | 20 | down: (queryInterface, Sequelize) => { 21 | return queryInterface.dropTable('addresses') 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-favorite.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('favorites', { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | field: 'id' 9 | }, 10 | userId: { 11 | type: DataTypes.UUID, 12 | field: 'user_id' 13 | }, 14 | trackid: { 15 | type: DataTypes.UUID, 16 | field: 'track_id' 17 | }, 18 | type: { 19 | type: DataTypes.BOOLEAN, 20 | field: 'type' 21 | } 22 | }) 23 | }, 24 | 25 | down: (queryInterface, Sequelize) => { 26 | return queryInterface.dropTable('favorites') 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-gf-form.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('rsntr_gf_entry', { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | field: 'id' 9 | }, 10 | form_id: { 11 | type: DataTypes.INTEGER, 12 | field: 'form_id' 13 | }, 14 | date_created: { 15 | type: DataTypes.DATE, 16 | field: 'date_created' 17 | }, 18 | date_updated: { 19 | type: DataTypes.DATE, 20 | field: 'date_updated' 21 | }, 22 | currency: { 23 | type: DataTypes.STRING(5), 24 | field: 'currency' 25 | }, 26 | ip: { 27 | type: DataTypes.STRING(39), 28 | allowNull: false, 29 | field: 'ip' 30 | }, 31 | user_agent: { 32 | type: DataTypes.STRING(250), 33 | field: 'user_agent' 34 | }, 35 | payment_status: { 36 | type: DataTypes.STRING(15), 37 | field: 'payment_status' 38 | }, 39 | payment_date: { 40 | type: DataTypes.DATE, 41 | field: 'payment_date' 42 | }, 43 | payment_amount: { 44 | type: DataTypes.DECIMAL(19, 2), 45 | field: 'payment_amount' 46 | }, 47 | payment_method: { 48 | type: DataTypes.STRING(30), 49 | field: 'payment_method' 50 | }, 51 | transaction_id: { 52 | type: DataTypes.STRING(50), 53 | field: 'transaction_id' 54 | }, 55 | is_fulfilled: { 56 | type: DataTypes.INTEGER, 57 | field: 'is_fulfilled' 58 | }, 59 | created_by: { 60 | type: DataTypes.BIGINT, 61 | field: 'created_by' 62 | }, 63 | transaction_type: { 64 | type: DataTypes.INTEGER, 65 | field: 'transaction_type' 66 | }, 67 | status: { 68 | type: DataTypes.STRING(20), 69 | field: 'status' 70 | } 71 | }) 72 | }, 73 | 74 | down: (queryInterface, Sequelize) => { 75 | return queryInterface.dropTable('rsntr_gf_entry') 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-image.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('images', { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | ownerId: { 11 | type: Sequelize.UUID, 12 | field: 'owner_id' 13 | }, 14 | uuid: { 15 | type: Sequelize.UUID, 16 | allowNull: false, 17 | defaultValue: Sequelize.UUIDV4, 18 | unique: true, 19 | field: 'name' 20 | } 21 | }) 22 | }, 23 | 24 | down: (queryInterface, Sequelize) => { 25 | return queryInterface.dropTable('images') 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-links.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('links', { 4 | id: { 5 | type: DataTypes.UUID, 6 | defaultValue: DataTypes.UUIDV4, 7 | primaryKey: true 8 | }, 9 | uri: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | }, 13 | type: { 14 | type: DataTypes.STRING 15 | }, 16 | platform: { 17 | type: DataTypes.STRING, 18 | allowNull: false 19 | }, 20 | personalData: { // This is in reference to Stripe codes 21 | type: DataTypes.STRING, 22 | allowNull: false, 23 | field: 'personal_data' 24 | } 25 | }) 26 | }, 27 | 28 | down: (queryInterface, Sequelize) => { 29 | return queryInterface.dropTable('links') 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-membership_class.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('membership_classes', { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true 8 | }, 9 | name: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | }, 13 | priceId: { 14 | type: DataTypes.STRING, 15 | allowNull: false, 16 | field: 'price_id' 17 | }, 18 | productId: { 19 | type: DataTypes.STRING, 20 | allowNull: false, 21 | field: 'product_id' 22 | } 23 | }) 24 | }, 25 | 26 | down: (queryInterface, Sequelize) => { 27 | return queryInterface.dropTable('membership_classes') 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-order.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('orders', { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | field: 'oid' 9 | }, 10 | user_id: { 11 | type: DataTypes.UUID, 12 | field: 'uid' 13 | }, 14 | amount: { 15 | type: DataTypes.DOUBLE, 16 | field: 'amount' 17 | }, 18 | currency: { 19 | type: DataTypes.STRING(10), 20 | field: 'currency', 21 | allowNull: true, 22 | enum: [ 23 | 'USD', 24 | 'EUR' 25 | ] 26 | }, 27 | transaction_id: { 28 | type: DataTypes.STRING, 29 | field: 'txid' 30 | }, 31 | vat: { 32 | type: DataTypes.INTEGER, 33 | defaultValue: 0, 34 | field: 'vat' 35 | }, 36 | details: { 37 | type: DataTypes.TEXT, 38 | field: 'details' 39 | }, 40 | payment_status: { 41 | type: DataTypes.INTEGER, 42 | field: 'pstatus' 43 | }, 44 | status: { 45 | type: DataTypes.INTEGER, 46 | field: 'ostatus' 47 | }, 48 | type: { 49 | type: DataTypes.INTEGER, 50 | field: 'type' 51 | }, 52 | createdAt: { 53 | type: DataTypes.INTEGER, 54 | field: 'date' 55 | } 56 | }) 57 | }, 58 | 59 | down: (queryInterface, Sequelize) => { 60 | return queryInterface.dropTable('orders') 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-role.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: async (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('roles', { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true 8 | }, 9 | name: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | }, 13 | description: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | }, 17 | isDefault: { 18 | type: DataTypes.BOOLEAN, 19 | defaultValue: false, 20 | field: 'is_default' 21 | } 22 | }) 23 | }, 24 | 25 | down: (queryInterface, Sequelize) => { 26 | return queryInterface.dropTable('roles') 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-share_transaction.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('share_transactions', { 4 | id: { 5 | type: DataTypes.UUID, 6 | allowNull: false, 7 | defaultValue: DataTypes.UUIDV4, 8 | primaryKey: true, 9 | validate: { 10 | isUUID: 4 11 | }, 12 | unique: true, 13 | field: 'id' 14 | }, 15 | userId: { 16 | type: DataTypes.UUID, 17 | allowNull: false, 18 | field: 'user_id' 19 | }, 20 | invoiceId: { 21 | type: DataTypes.STRING, 22 | allowNull: false, 23 | unique: true, 24 | field: 'invoice_id' 25 | }, 26 | quantity: { 27 | type: DataTypes.INTEGER, 28 | allowNull: false 29 | }, 30 | updatedAt: { 31 | field: 'updated_at', 32 | allowNull: false, 33 | type: DataTypes.DATE 34 | }, 35 | createdAt: { 36 | field: 'created_at', 37 | allowNull: false, 38 | type: DataTypes.DATE 39 | }, 40 | deletedAt: { 41 | field: 'deleted_at', 42 | type: DataTypes.DATE 43 | } 44 | }) 45 | }, 46 | 47 | down: (queryInterface, Sequelize) => { 48 | return queryInterface.dropTable('share_transactions') 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-tag.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('tags', { 4 | id: { 5 | primaryKey: true, 6 | type: DataTypes.UUID, 7 | defaultValue: DataTypes.UUIDV4 8 | }, 9 | type: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | }, 13 | name: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | } 17 | }) 18 | }, 19 | 20 | down: (queryInterface, Sequelize) => { 21 | return queryInterface.dropTable('tags') 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-userMeta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('rsntr_usermeta', { 4 | id: { 5 | type: DataTypes.BIGINT, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | field: 'umeta_id' 9 | }, 10 | userId: { 11 | type: DataTypes.UUID, 12 | field: 'user_id' 13 | }, 14 | meta_key: { 15 | type: DataTypes.STRING, 16 | field: 'meta_key' 17 | }, 18 | meta_value: { 19 | type: DataTypes.TEXT, 20 | field: 'meta_value' 21 | } 22 | }, { 23 | timestamps: false, 24 | modelName: 'UserMeta', 25 | tableName: 'rsntr_usermeta' 26 | }) 27 | }, 28 | 29 | down: (queryInterface, Sequelize) => { 30 | return queryInterface.dropTable('rsntr_usermeta') 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-user_group.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('user_groups', { 4 | id: { 5 | type: DataTypes.UUID, 6 | allowNull: false, 7 | defaultValue: DataTypes.UUIDV4, 8 | primaryKey: true, 9 | validate: { 10 | isUUID: 4 11 | }, 12 | unique: true, 13 | field: 'id' 14 | }, 15 | ownerId: { 16 | type: DataTypes.UUID, 17 | validate: { 18 | isUUID: 4 19 | }, 20 | field: 'owner_id', 21 | allowNull: false 22 | }, 23 | typeId: { 24 | type: DataTypes.INTEGER, 25 | allowNull: false, 26 | field: 'type_id' 27 | }, 28 | displayName: { 29 | type: DataTypes.STRING, 30 | field: 'display_name' 31 | }, 32 | description: { 33 | type: DataTypes.TEXT, 34 | field: 'description' 35 | }, 36 | shortBio: { 37 | type: DataTypes.TEXT, 38 | field: 'short_bio' 39 | }, 40 | email: { 41 | type: DataTypes.STRING 42 | }, 43 | addressId: { 44 | type: DataTypes.UUID, 45 | field: 'address_id' 46 | }, 47 | avatar: { 48 | type: DataTypes.UUID 49 | }, 50 | banner: { 51 | type: DataTypes.UUID 52 | }, 53 | tags: { 54 | type: DataTypes.ARRAY(DataTypes.UUID) 55 | }, 56 | updatedAt: { 57 | field: 'updated_at', 58 | allowNull: false, 59 | type: DataTypes.DATE 60 | }, 61 | createdAt: { 62 | field: 'created_at', 63 | allowNull: false, 64 | type: DataTypes.DATE 65 | }, 66 | deletedAt: { 67 | field: 'deleted_at', 68 | type: DataTypes.DATE 69 | } 70 | }) 71 | }, 72 | 73 | down: (queryInterface, Sequelize) => { 74 | return queryInterface.dropTable('user_groups') 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-user_group_link.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('user_group_links', { 4 | id: { 5 | type: DataTypes.UUID, 6 | defaultValue: DataTypes.UUIDV4, 7 | primaryKey: true 8 | }, 9 | ownerId: { 10 | type: DataTypes.UUID, 11 | field: 'owner_id' 12 | }, 13 | uri: { 14 | type: DataTypes.TEXT, 15 | allowNull: false 16 | }, 17 | type: { 18 | type: DataTypes.STRING 19 | }, 20 | platform: { 21 | type: DataTypes.STRING, 22 | allowNull: false 23 | } 24 | }) 25 | }, 26 | 27 | down: (queryInterface, Sequelize) => { 28 | return queryInterface.dropTable('user_group_links') 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-user_group_member.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('user_group_members', { 4 | member_id: { 5 | type: DataTypes.UUID, 6 | field: 'member_id', 7 | primaryKey: true 8 | }, 9 | belongsToId: { 10 | type: DataTypes.UUID, 11 | field: 'belongs_to_id', 12 | primaryKey: true 13 | } 14 | }) 15 | }, 16 | 17 | down: (queryInterface, Sequelize) => { 18 | return queryInterface.dropTable('user_group_members') 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-user_group_types.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('user_group_types', { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true 8 | }, 9 | name: { 10 | type: DataTypes.STRING(250), 11 | allowNull: false, 12 | unique: true 13 | }, 14 | description: { 15 | type: DataTypes.TEXT, 16 | allowNull: false 17 | } 18 | }) 19 | }, 20 | 21 | down: (queryInterface, Sequelize) => { 22 | return queryInterface.dropTable('user_group_types') 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/db/migrations/202207140053-user_membership.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('user_memberships', { 4 | id: { 5 | type: DataTypes.UUID, 6 | allowNull: false, 7 | defaultValue: DataTypes.UUIDV4, 8 | primaryKey: true, 9 | validate: { 10 | isUUID: 4 11 | }, 12 | unique: true, 13 | field: 'id' 14 | }, 15 | userId: { 16 | type: DataTypes.UUID, 17 | field: 'user_id' 18 | }, 19 | membershipClassId: { 20 | type: DataTypes.INTEGER, 21 | field: 'membership_class_id', 22 | allowNull: false 23 | }, 24 | subscriptionId: { // This is in reference to Stripe codes 25 | type: DataTypes.STRING, 26 | field: 'subscription_id', 27 | allowNull: false 28 | }, 29 | start: { 30 | type: DataTypes.TIME 31 | }, 32 | end: { 33 | type: DataTypes.TIME 34 | }, 35 | updated_at: { 36 | allowNull: false, 37 | type: DataTypes.DATE 38 | }, 39 | created_at: { 40 | allowNull: false, 41 | type: DataTypes.DATE 42 | }, 43 | deleted_at: { 44 | type: DataTypes.DATE 45 | } 46 | }) 47 | }, 48 | 49 | down: (queryInterface, Sequelize) => { 50 | return queryInterface.dropTable('user_memberships') 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/db/migrations/2022071410053-credit.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('credits', { 4 | id: { 5 | type: DataTypes.UUID, 6 | allowNull: false, 7 | defaultValue: DataTypes.UUIDV4, 8 | primaryKey: true, 9 | validate: { 10 | isUUID: 4 11 | }, 12 | unique: true, 13 | field: 'id' 14 | }, 15 | user_id: { 16 | type: DataTypes.UUID, 17 | allowNull: false, 18 | defaultValue: DataTypes.UUIDV4, 19 | validate: { 20 | isUUID: 4 21 | }, 22 | field: 'user_id' 23 | }, 24 | total: { 25 | type: DataTypes.INTEGER, 26 | defaultValue: 0, 27 | field: 'total' 28 | }, 29 | updatedAt: { 30 | field: 'updated_at', 31 | allowNull: false, 32 | type: DataTypes.DATE 33 | }, 34 | createdAt: { 35 | field: 'created_at', 36 | allowNull: false, 37 | type: DataTypes.DATE 38 | }, 39 | deletedAt: { 40 | field: 'deleted_at', 41 | type: DataTypes.DATE 42 | } 43 | }) 44 | }, 45 | 46 | down: (queryInterface, Sequelize) => { 47 | return queryInterface.dropTable('credits') 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/db/migrations/20221107-drop-usermeta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface) => { 3 | return queryInterface.dropTable('rsntr_usermeta') 4 | }, 5 | 6 | down: (queryInterface, DataTypes) => { 7 | return queryInterface.createTable('rsntr_usermeta', { 8 | id: { 9 | type: DataTypes.BIGINT, 10 | primaryKey: true, 11 | autoIncrement: true, 12 | field: 'umeta_id' 13 | }, 14 | userId: { 15 | type: DataTypes.UUID, 16 | field: 'user_id' 17 | }, 18 | meta_key: { 19 | type: DataTypes.STRING, 20 | field: 'meta_key' 21 | }, 22 | meta_value: { 23 | type: DataTypes.TEXT, 24 | field: 'meta_value' 25 | } 26 | }, { 27 | timestamps: false, 28 | modelName: 'UserMeta', 29 | tableName: 'rsntr_usermeta' 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/db/migrations/20221107-gf-form.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.dropTable('rsntr_gf_entry') 4 | }, 5 | 6 | down: (queryInterface, DataTypes) => { 7 | return queryInterface.createTable('rsntr_gf_entry', { 8 | id: { 9 | type: DataTypes.INTEGER, 10 | primaryKey: true, 11 | autoIncrement: true, 12 | field: 'id' 13 | }, 14 | form_id: { 15 | type: DataTypes.INTEGER, 16 | field: 'form_id' 17 | }, 18 | date_created: { 19 | type: DataTypes.DATE, 20 | field: 'date_created' 21 | }, 22 | date_updated: { 23 | type: DataTypes.DATE, 24 | field: 'date_updated' 25 | }, 26 | currency: { 27 | type: DataTypes.STRING(5), 28 | field: 'currency' 29 | }, 30 | ip: { 31 | type: DataTypes.STRING(39), 32 | allowNull: false, 33 | field: 'ip' 34 | }, 35 | user_agent: { 36 | type: DataTypes.STRING(250), 37 | field: 'user_agent' 38 | }, 39 | payment_status: { 40 | type: DataTypes.STRING(15), 41 | field: 'payment_status' 42 | }, 43 | payment_date: { 44 | type: DataTypes.DATE, 45 | field: 'payment_date' 46 | }, 47 | payment_amount: { 48 | type: DataTypes.DECIMAL(19, 2), 49 | field: 'payment_amount' 50 | }, 51 | payment_method: { 52 | type: DataTypes.STRING(30), 53 | field: 'payment_method' 54 | }, 55 | transaction_id: { 56 | type: DataTypes.STRING(50), 57 | field: 'transaction_id' 58 | }, 59 | is_fulfilled: { 60 | type: DataTypes.INTEGER, 61 | field: 'is_fulfilled' 62 | }, 63 | created_by: { 64 | type: DataTypes.BIGINT, 65 | field: 'created_by' 66 | }, 67 | transaction_type: { 68 | type: DataTypes.INTEGER, 69 | field: 'transaction_type' 70 | }, 71 | status: { 72 | type: DataTypes.STRING(20), 73 | field: 'status' 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/db/migrations/20221107-share_transaction_legacyId.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: async (queryInterface, DataTypes) => { 3 | return queryInterface.addColumn('share_transactions', 'legacy_source', DataTypes.STRING) 4 | }, 5 | 6 | down: async (queryInterface, Sequelize) => { 7 | return queryInterface.removeColumn('share_transactions', 'legacy_source') 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/db/migrations/20221107-user_membership_legacyId.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: async (queryInterface, DataTypes) => { 3 | return queryInterface.addColumn('user_memberships', 'legacy_source', DataTypes.STRING) 4 | }, 5 | 6 | down: async (queryInterface, Sequelize) => { 7 | return queryInterface.removeColumn('user_memberships', 'legacy_source') 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/db/migrations/20221108-add-email-confirmation-expiration.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: async (queryInterface, DataTypes) => { 3 | return queryInterface.addColumn('users', 'email_confirmation_expiration', DataTypes.DATE) 4 | }, 5 | 6 | down: async (queryInterface, Sequelize) => { 7 | return queryInterface.removeColumn('users', 'email_confirmation_expiration') 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/db/migrations/20221226-add-track-hls.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: async (queryInterface, DataTypes) => { 3 | return queryInterface.addColumn('tracks', 'hls', DataTypes.BOOLEAN) 4 | }, 5 | 6 | down: async (queryInterface, Sequelize) => { 7 | return queryInterface.removeColumn('tracks', 'hls') 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/db/migrations/20221228-create-table-user-ledger-entry.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('user_ledger_entries', { 4 | id: { 5 | type: DataTypes.UUID, 6 | allowNull: false, 7 | defaultValue: DataTypes.UUIDV4, 8 | primaryKey: true, 9 | validate: { 10 | isUUID: 4 11 | }, 12 | unique: true 13 | }, 14 | userId: { 15 | type: DataTypes.UUID, 16 | field: 'user_id' 17 | }, 18 | type: { 19 | type: DataTypes.ENUM, 20 | values: ['credit', 'debit'], 21 | allowNull: false 22 | }, 23 | amount: { 24 | type: DataTypes.DECIMAL(10, 2), 25 | allowNull: false 26 | }, 27 | extra: { 28 | type: DataTypes.JSONB, 29 | field: 'extra' 30 | }, 31 | updatedAt: { 32 | field: 'updated_at', 33 | allowNull: false, 34 | type: DataTypes.DATE 35 | }, 36 | createdAt: { 37 | field: 'created_at', 38 | allowNull: false, 39 | type: DataTypes.DATE 40 | }, 41 | deletedAt: { 42 | field: 'deleted_at', 43 | type: DataTypes.DATE 44 | } 45 | }) 46 | }, 47 | 48 | down: (queryInterface, Sequelize) => { 49 | return queryInterface.dropTable('user_ledger_entries') 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/db/migrations/20221231-create-table-user-track-purchase.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('user_track_purchase', { 4 | id: { 5 | type: DataTypes.UUID, 6 | allowNull: false, 7 | defaultValue: DataTypes.UUIDV4, 8 | primaryKey: true, 9 | validate: { 10 | isUUID: 4 11 | }, 12 | unique: true 13 | }, 14 | userId: { 15 | type: DataTypes.UUID, 16 | field: 'user_id' 17 | }, 18 | trackId: { 19 | type: DataTypes.UUID, 20 | allowNull: false, 21 | field: 'track_id' 22 | }, 23 | type: { 24 | type: DataTypes.ENUM, 25 | allowNull: false, 26 | values: ['purchase', 'plays'] 27 | }, 28 | updatedAt: { 29 | allowNull: false, 30 | type: DataTypes.DATE, 31 | field: 'updated_at' 32 | }, 33 | createdAt: { 34 | allowNull: false, 35 | type: DataTypes.DATE, 36 | field: 'created_at' 37 | } 38 | }) 39 | }, 40 | 41 | down: (queryInterface, Sequelize) => { 42 | return queryInterface.dropTable('user_track_purchase') 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/db/migrations/20221231-create-table-user-trackgroup-purchase.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, DataTypes) => { 3 | return queryInterface.createTable('user_track_group_purchase', { 4 | id: { 5 | type: DataTypes.UUID, 6 | allowNull: false, 7 | defaultValue: DataTypes.UUIDV4, 8 | primaryKey: true, 9 | validate: { 10 | isUUID: 4 11 | }, 12 | unique: true 13 | }, 14 | userId: { 15 | type: DataTypes.UUID, 16 | field: 'user_id' 17 | }, 18 | trackGroupId: { 19 | type: DataTypes.UUID, 20 | allowNull: false, 21 | field: 'track_group_id' 22 | }, 23 | type: { 24 | type: DataTypes.ENUM, 25 | allowNull: false, 26 | values: ['purchase'] 27 | }, 28 | updatedAt: { 29 | allowNull: false, 30 | type: DataTypes.DATE, 31 | field: 'updated_at' 32 | }, 33 | createdAt: { 34 | allowNull: false, 35 | type: DataTypes.DATE, 36 | field: 'created_at' 37 | } 38 | }) 39 | }, 40 | 41 | down: (queryInterface, Sequelize) => { 42 | return queryInterface.dropTable('user_track_group_purchase') 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/db/models/resonate/address.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Address = sequelize.define('Address', { 3 | id: { 4 | primaryKey: true, 5 | type: DataTypes.UUID, 6 | defaultValue: DataTypes.UUIDV4 7 | }, 8 | personalData: { 9 | type: DataTypes.BOOLEAN, 10 | defaultValue: true, 11 | field: 'personal_data' 12 | }, 13 | data: { 14 | type: DataTypes.ARRAY(DataTypes.STRING) 15 | } 16 | }, { 17 | timestamps: false, 18 | modelName: 'Address', 19 | tableName: 'addresses' 20 | }) 21 | 22 | Address.associate = function (models) { 23 | Address.hasMany(models.UserGroup, { as: 'user_group', sourceKey: 'id' }) 24 | } 25 | 26 | return Address 27 | } 28 | -------------------------------------------------------------------------------- /src/db/models/resonate/client.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Client = sequelize.define('Client', { 3 | id: { 4 | type: DataTypes.INTEGER, 5 | allowNull: false, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | field: 'id' 9 | }, 10 | key: { 11 | type: DataTypes.UUID, 12 | allowNull: false, 13 | defaultValue: DataTypes.UUIDV4, 14 | validate: { 15 | isUUID: 4 16 | }, 17 | unique: true, 18 | field: 'key' 19 | }, 20 | secret: { 21 | type: DataTypes.STRING, 22 | allowNull: false, 23 | field: 'secret' 24 | }, 25 | grantTypes: { 26 | type: DataTypes.ARRAY(DataTypes.STRING), 27 | allowNull: false, 28 | field: 'grant_types' 29 | }, 30 | responseTypes: { 31 | type: DataTypes.ARRAY(DataTypes.STRING), 32 | allowNull: false, 33 | field: 'response_types' 34 | }, 35 | redirectURIs: { 36 | type: DataTypes.ARRAY(DataTypes.STRING), 37 | defaultValue: 0, 38 | field: 'redirect_uris' 39 | }, 40 | applicationName: { 41 | type: DataTypes.STRING, 42 | allowNull: false, 43 | field: 'application_name' 44 | }, 45 | applicationURL: { 46 | type: DataTypes.STRING, 47 | allowNull: false, 48 | field: 'application_url' 49 | }, 50 | metaData: { 51 | type: DataTypes.JSONB 52 | }, 53 | updatedAt: { 54 | field: 'updated_at', 55 | allowNull: false, 56 | type: DataTypes.DATE 57 | }, 58 | createdAt: { 59 | field: 'created_at', 60 | allowNull: false, 61 | type: DataTypes.DATE 62 | }, 63 | deletedAt: { 64 | field: 'deleted_at', 65 | type: DataTypes.DATE 66 | } 67 | }, { 68 | sequelize, 69 | underscored: true, 70 | paranoid: true, 71 | modelName: 'Client', 72 | tableName: 'clients' 73 | }) 74 | 75 | return Client 76 | } 77 | -------------------------------------------------------------------------------- /src/db/models/resonate/credit.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Credit = sequelize.define('Credit', { 3 | id: { 4 | type: DataTypes.UUID, 5 | allowNull: false, 6 | defaultValue: DataTypes.UUIDV4, 7 | primaryKey: true, 8 | validate: { 9 | isUUID: 4 10 | }, 11 | unique: true, 12 | field: 'id' 13 | }, 14 | userId: { 15 | type: DataTypes.UUID, 16 | allowNull: false, 17 | defaultValue: DataTypes.UUIDV4, 18 | validate: { 19 | isUUID: 4 20 | }, 21 | field: 'user_id' 22 | }, 23 | total: { 24 | type: DataTypes.INTEGER, 25 | defaultValue: 0, 26 | field: 'total' 27 | }, 28 | updatedAt: { 29 | field: 'updated_at', 30 | allowNull: false, 31 | type: DataTypes.DATE 32 | }, 33 | createdAt: { 34 | field: 'created_at', 35 | allowNull: false, 36 | type: DataTypes.DATE 37 | }, 38 | deletedAt: { 39 | field: 'deleted_at', 40 | type: DataTypes.DATE 41 | } 42 | }, { 43 | sequelize, 44 | underscored: true, 45 | paranoid: true, 46 | modelName: 'Credit', 47 | tableName: 'credits' 48 | }) 49 | 50 | return Credit 51 | } 52 | -------------------------------------------------------------------------------- /src/db/models/resonate/favorite.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Favorite = sequelize.define('Favorite', { 3 | id: { 4 | type: DataTypes.INTEGER, 5 | primaryKey: true, 6 | autoIncrement: true 7 | }, 8 | userId: { 9 | type: DataTypes.UUID 10 | }, 11 | trackId: { 12 | type: DataTypes.UUID 13 | }, 14 | type: { 15 | type: DataTypes.BOOLEAN 16 | } 17 | }, { 18 | timestamps: false, 19 | underscored: true, 20 | modelName: 'Favorite', 21 | tableName: 'favorites' 22 | }) 23 | 24 | Favorite.associate = function (models) { 25 | Favorite.belongsTo(models.Track, { as: 'track', foreignKey: 'trackId' }) 26 | Favorite.belongsTo(models.User, { as: 'user', foreignKey: 'userId' }) 27 | } 28 | 29 | return Favorite 30 | } 31 | -------------------------------------------------------------------------------- /src/db/models/resonate/image.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Image = sequelize.define('Image', { 3 | id: { 4 | type: DataTypes.INTEGER, 5 | primaryKey: true, 6 | autoIncrement: true 7 | }, 8 | ownerId: { 9 | type: DataTypes.UUID 10 | }, 11 | uuid: { 12 | type: DataTypes.UUID, 13 | allowNull: false, 14 | defaultValue: DataTypes.UUIDV4, 15 | unique: true, 16 | field: 'name' 17 | } 18 | }, { 19 | timestamps: false, 20 | underscored: true, 21 | modelName: 'Image', 22 | tableName: 'images' 23 | }) 24 | 25 | return Image 26 | } 27 | -------------------------------------------------------------------------------- /src/db/models/resonate/link.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Link = sequelize.define('Link', { 3 | id: { 4 | type: DataTypes.UUID, 5 | defaultValue: DataTypes.UUIDV4, 6 | primaryKey: true, 7 | autoIncrement: true 8 | }, 9 | uri: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | }, 13 | type: { 14 | type: DataTypes.STRING 15 | }, 16 | platform: { 17 | type: DataTypes.STRING, 18 | allowNull: false 19 | }, 20 | personalData: { // This is in reference to Stripe codes 21 | type: DataTypes.STRING, 22 | allowNull: false 23 | } 24 | }, { 25 | underscored: true, 26 | modelName: 'Link', 27 | tableName: 'links' 28 | }) 29 | 30 | return Link 31 | } 32 | -------------------------------------------------------------------------------- /src/db/models/resonate/membership_class.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const MembershipClass = sequelize.define('MembershipClass', { 3 | id: { 4 | type: DataTypes.INTEGER, 5 | primaryKey: true, 6 | autoIncrement: true 7 | }, 8 | name: { 9 | type: DataTypes.STRING, 10 | allowNull: false 11 | }, 12 | priceId: { 13 | type: DataTypes.STRING, 14 | allowNull: false 15 | }, 16 | productId: { 17 | type: DataTypes.STRING, 18 | allowNull: false 19 | } 20 | }, { 21 | timestamps: false, 22 | underscored: true, 23 | modelName: 'MembershipClass', 24 | tableName: 'membership_classes' 25 | }) 26 | 27 | return MembershipClass 28 | } 29 | -------------------------------------------------------------------------------- /src/db/models/resonate/order.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Order = sequelize.define('Order', { 3 | id: { 4 | type: DataTypes.INTEGER, 5 | primaryKey: true, 6 | autoIncrement: true, 7 | field: 'oid' 8 | }, 9 | user_id: { 10 | type: DataTypes.UUID, 11 | field: 'uid' 12 | }, 13 | amount: { 14 | type: DataTypes.DOUBLE, 15 | field: 'amount' 16 | }, 17 | currency: { 18 | type: DataTypes.STRING(10), 19 | field: 'currency', 20 | allowNull: true, 21 | enum: [ 22 | 'USD', 23 | 'EUR' 24 | ] 25 | }, 26 | transaction_id: { 27 | type: DataTypes.STRING, 28 | field: 'txid' 29 | }, 30 | vat: { 31 | type: DataTypes.INTEGER, 32 | defaultValue: 0, 33 | field: 'vat' 34 | }, 35 | details: { 36 | type: DataTypes.TEXT, 37 | field: 'details', 38 | set (details) { 39 | this.setDataValue('details', JSON.stringify(details)) 40 | }, 41 | get () { 42 | const details = this.getDataValue('details') 43 | if (details) return JSON.parse(details) 44 | return details 45 | } 46 | }, 47 | payment_status: { 48 | type: DataTypes.INTEGER, 49 | field: 'pstatus' 50 | }, 51 | status: { 52 | type: DataTypes.INTEGER, 53 | field: 'ostatus' 54 | }, 55 | type: { 56 | type: DataTypes.INTEGER, 57 | field: 'type' 58 | }, 59 | createdAt: { 60 | type: DataTypes.INTEGER, 61 | field: 'date' 62 | } 63 | }, { 64 | timestamps: false, 65 | modelName: 'Order', 66 | tableName: 'orders' 67 | }) 68 | 69 | return Order 70 | } 71 | -------------------------------------------------------------------------------- /src/db/models/resonate/play.js: -------------------------------------------------------------------------------- 1 | const types = ['free', 'paid'] 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const Play = sequelize.define('Play', { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | trackId: { 11 | type: DataTypes.UUID, 12 | allowNull: false 13 | }, 14 | userId: { 15 | type: DataTypes.UUID, 16 | allowNull: false 17 | }, 18 | type: { 19 | type: DataTypes.INTEGER, 20 | set (type) { 21 | this.setDataValue('type', types.indexOf(type)) 22 | }, 23 | get () { 24 | const type = this.getDataValue('type') 25 | return types[type] 26 | }, 27 | validate: { 28 | min: 0, 29 | max: 1 30 | }, 31 | defaultValue: 0, 32 | field: 'event' 33 | }, 34 | createdAt: { 35 | type: DataTypes.DATE, 36 | field: 'created_at' 37 | } 38 | }, { 39 | timestamps: false, 40 | underscored: true, 41 | modelName: 'Play', 42 | tableName: 'plays' 43 | }) 44 | 45 | Play.associate = function (models) { 46 | Play.belongsTo(models.Track, { foreignKey: 'trackId', as: 'track' }) 47 | Play.belongsTo(models.User, { foreignKey: 'userId', as: 'user' }) 48 | } 49 | 50 | return Play 51 | } 52 | -------------------------------------------------------------------------------- /src/db/models/resonate/playlist_item.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const PlaylistItem = sequelize.define('PlaylistItem', { 3 | id: { 4 | type: DataTypes.UUID, 5 | allowNull: false, 6 | defaultValue: DataTypes.UUIDV4, 7 | primaryKey: true, 8 | validate: { 9 | isUUID: 4 10 | }, 11 | unique: true, 12 | field: 'id' 13 | }, 14 | index: { 15 | type: DataTypes.INTEGER, 16 | allowNull: false, 17 | defaultValue: 0, 18 | field: 'index' 19 | }, 20 | playlistId: { 21 | type: DataTypes.UUID, 22 | allowNull: false, 23 | validate: { 24 | isUUID: 4 25 | }, 26 | field: 'playlist_id' 27 | }, 28 | trackId: { 29 | type: DataTypes.UUID, 30 | allowNull: false, 31 | field: 'track_id' 32 | }, 33 | updatedAt: { 34 | field: 'updated_at', 35 | allowNull: false, 36 | type: DataTypes.DATE 37 | }, 38 | createdAt: { 39 | field: 'created_at', 40 | allowNull: false, 41 | type: DataTypes.DATE 42 | }, 43 | deletedAt: { 44 | field: 'deleted_at', 45 | type: DataTypes.DATE 46 | } 47 | }, { 48 | modelName: 'PlaylistItem', 49 | paranoid: true, 50 | underscored: true, 51 | tableName: 'playlist_items' 52 | }) 53 | 54 | PlaylistItem.associate = function (models) { 55 | PlaylistItem.hasOne(models.Track, { as: 'track', sourceKey: 'trackId', foreignKey: 'id' }) 56 | PlaylistItem.belongsTo(models.Playlist, { foreignKey: 'playlistId' }) 57 | } 58 | 59 | return PlaylistItem 60 | } 61 | -------------------------------------------------------------------------------- /src/db/models/resonate/role.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Roles are distinct from groups because roles define what a user can do in the system. 3 | */ 4 | 5 | module.exports = (sequelize, DataTypes) => { 6 | const Role = sequelize.define('Role', { 7 | id: { 8 | type: DataTypes.INTEGER, 9 | allowNull: false, 10 | primaryKey: true, 11 | autoIncrement: true 12 | }, 13 | name: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | }, 17 | description: { 18 | type: DataTypes.STRING, 19 | allowNull: false 20 | }, 21 | isDefault: { 22 | type: DataTypes.BOOLEAN, 23 | defaultValue: false 24 | } 25 | }, { 26 | sequelize, 27 | underscored: true, 28 | timestamps: false, 29 | modelName: 'Role', 30 | tableName: 'roles' 31 | }) 32 | 33 | return Role 34 | } 35 | -------------------------------------------------------------------------------- /src/db/models/resonate/share_transaction.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const ShareTransaction = sequelize.define('ShareTransaction', { 3 | id: { 4 | type: DataTypes.UUID, 5 | allowNull: false, 6 | defaultValue: DataTypes.UUIDV4, 7 | primaryKey: true, 8 | validate: { 9 | isUUID: 4 10 | }, 11 | unique: true, 12 | field: 'id' 13 | }, 14 | legacySource: { 15 | type: DataTypes.STRING, 16 | field: 'legacy_source' 17 | }, 18 | userId: { 19 | type: DataTypes.UUID, 20 | allowNull: false 21 | }, 22 | invoiceId: { 23 | type: DataTypes.STRING, 24 | allowNull: false, 25 | unique: true 26 | }, 27 | quantity: { 28 | type: DataTypes.INTEGER, 29 | allowNull: false 30 | }, 31 | updatedAt: { 32 | allowNull: false, 33 | type: DataTypes.DATE 34 | }, 35 | createdAt: { 36 | allowNull: false, 37 | type: DataTypes.DATE 38 | }, 39 | deletedAt: { 40 | type: DataTypes.DATE 41 | } 42 | }, { 43 | underscored: true, 44 | modelName: 'ShareTransaction', 45 | tableName: 'share_transactions' 46 | }) 47 | 48 | return ShareTransaction 49 | } 50 | -------------------------------------------------------------------------------- /src/db/models/resonate/tag.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Tag = sequelize.define('Tag', { 3 | id: { 4 | primaryKey: true, 5 | type: DataTypes.UUID, 6 | defaultValue: DataTypes.UUIDV4 7 | }, 8 | type: { 9 | type: DataTypes.INTEGER, 10 | allowNull: false 11 | }, 12 | name: { 13 | type: DataTypes.STRING, 14 | allowNull: false 15 | } 16 | }, { 17 | timestamps: false, 18 | modelName: 'Tag', 19 | tableName: 'tags' 20 | }) 21 | 22 | return Tag 23 | } 24 | -------------------------------------------------------------------------------- /src/db/models/resonate/track_group_item.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const TrackGroupItem = sequelize.define('TrackGroupItem', { 3 | id: { 4 | type: DataTypes.UUID, 5 | allowNull: false, 6 | defaultValue: DataTypes.UUIDV4, 7 | primaryKey: true, 8 | validate: { 9 | isUUID: 4 10 | }, 11 | unique: true, 12 | field: 'id' 13 | }, 14 | index: { 15 | type: DataTypes.INTEGER, 16 | allowNull: false, 17 | defaultValue: 0, 18 | field: 'index' 19 | }, 20 | trackgroupId: { 21 | type: DataTypes.UUID, 22 | validate: { 23 | isUUID: 4 24 | }, 25 | field: 'track_group_id' 26 | }, 27 | trackId: { 28 | type: DataTypes.UUID, 29 | field: 'track_id' 30 | }, 31 | track_performers: { 32 | type: DataTypes.STRING, 33 | field: 'track_performers' 34 | }, 35 | track_composers: { 36 | type: DataTypes.STRING, 37 | field: 'track_composers' 38 | }, 39 | updatedAt: { 40 | field: 'updated_at', 41 | allowNull: false, 42 | type: DataTypes.DATE 43 | }, 44 | createdAt: { 45 | field: 'created_at', 46 | allowNull: false, 47 | type: DataTypes.DATE 48 | }, 49 | deletedAt: { 50 | field: 'deleted_at', 51 | type: DataTypes.DATE 52 | } 53 | }, { 54 | modelName: 'TrackGroupItem', 55 | paranoid: true, 56 | underscored: true, 57 | tableName: 'track_group_items' 58 | }) 59 | 60 | TrackGroupItem.associate = function (models) { 61 | TrackGroupItem.hasOne(models.Track, { as: 'track', sourceKey: 'trackId', foreignKey: 'id' }) 62 | TrackGroupItem.belongsTo(models.TrackGroup, { as: 'trackGroup', foreignKey: 'trackgroupId' }) 63 | } 64 | 65 | return TrackGroupItem 66 | } 67 | -------------------------------------------------------------------------------- /src/db/models/resonate/user_group_link.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const UserGroupLink = sequelize.define('UserGroupLink', { 3 | id: { 4 | type: DataTypes.UUID, 5 | defaultValue: DataTypes.UUIDV4, 6 | primaryKey: true 7 | }, 8 | ownerId: { 9 | type: DataTypes.UUID 10 | }, 11 | uri: { 12 | type: DataTypes.TEXT, 13 | allowNull: false 14 | }, 15 | type: { 16 | type: DataTypes.STRING 17 | }, 18 | platform: { 19 | type: DataTypes.STRING, 20 | allowNull: false 21 | } 22 | }, { 23 | underscored: true, 24 | timestamps: false, 25 | modelName: 'UserGroupLink', 26 | tableName: 'user_group_links' 27 | }) 28 | 29 | UserGroupLink.associate = function (models) { 30 | UserGroupLink.belongsTo(models.UserGroup, { foreignKey: 'ownerId' }) 31 | } 32 | 33 | return UserGroupLink 34 | } 35 | -------------------------------------------------------------------------------- /src/db/models/resonate/user_group_member.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const UserGroupMember = sequelize.define('UserGroupMember', { 3 | memberId: { 4 | type: DataTypes.UUID, 5 | validate: { 6 | isUUID: 4 7 | }, 8 | primaryKey: true 9 | }, 10 | belongsToId: { 11 | type: DataTypes.UUID, 12 | validate: { 13 | isUUID: 4 14 | }, 15 | primaryKey: true 16 | } 17 | }, { 18 | sequelize, 19 | underscored: true, 20 | paranoid: true, 21 | timestamps: false, 22 | modelName: 'UserGroupMember', 23 | tableName: 'user_group_members' 24 | }) 25 | 26 | UserGroupMember.associate = function (models) { 27 | // UserGroupMember.belongsTo(models.UserGroup, { as: 'members', targetKey: 'id', foreignKey: 'memberId' }) 28 | // UserGroupMember.hasMany(models.UserGroup, { as: 'parents', targetKey: 'id', foreignKey: 'belongsToId' }) 29 | } 30 | 31 | return UserGroupMember 32 | } 33 | -------------------------------------------------------------------------------- /src/db/models/resonate/user_group_type.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const UserGroupType = sequelize.define('UserGroupType', { 3 | id: { 4 | type: DataTypes.INTEGER, 5 | primaryKey: true, 6 | autoIncrement: true 7 | }, 8 | name: { 9 | type: DataTypes.STRING(250), 10 | allowNull: false, 11 | unique: true 12 | }, 13 | description: { 14 | type: DataTypes.TEXT, 15 | allowNull: false 16 | } 17 | }, { 18 | sequelize, 19 | timestamps: false, 20 | modelName: 'UserGroupType', 21 | tableName: 'user_group_types' 22 | }) 23 | 24 | UserGroupType.associate = function (models) { 25 | UserGroupType.hasMany(models.UserGroup, { as: 'groups', targetKey: 'id', foreignKey: 'typeId' }) 26 | } 27 | 28 | return UserGroupType 29 | } 30 | -------------------------------------------------------------------------------- /src/db/models/resonate/user_ledger_entry.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const UserLedgerEntry = sequelize.define('UserLedgerEntry', { 3 | id: { 4 | type: DataTypes.UUID, 5 | allowNull: false, 6 | defaultValue: DataTypes.UUIDV4, 7 | primaryKey: true, 8 | validate: { 9 | isUUID: 4 10 | }, 11 | unique: true 12 | }, 13 | userId: { 14 | type: DataTypes.UUID, 15 | field: 'user_id' 16 | }, 17 | type: { 18 | type: DataTypes.ENUM, 19 | values: ['credit', 'debit'], 20 | allowNull: false 21 | }, 22 | amount: { 23 | type: DataTypes.DECIMAL(10, 2), 24 | allowNull: false 25 | }, 26 | extra: { 27 | type: DataTypes.JSONB, 28 | field: 'extra' 29 | }, 30 | updatedAt: { 31 | allowNull: false, 32 | type: DataTypes.DATE 33 | }, 34 | createdAt: { 35 | allowNull: false, 36 | type: DataTypes.DATE 37 | }, 38 | deletedAt: { 39 | type: DataTypes.DATE 40 | } 41 | }, { 42 | sequelize, 43 | timestamps: true, 44 | paranoid: true, 45 | underscored: true, 46 | modelName: 'UserLedgerEntry', 47 | tableName: 'user_ledger_entries' 48 | }) 49 | 50 | UserLedgerEntry.associate = (models) => { 51 | UserLedgerEntry.belongsTo(models.User, { as: 'user', sourceKey: 'userId', foreignKey: 'id' }) 52 | } 53 | 54 | return UserLedgerEntry 55 | } 56 | -------------------------------------------------------------------------------- /src/db/models/resonate/user_membership.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const UserMembership = sequelize.define('UserMembership', { 3 | id: { 4 | type: DataTypes.UUID, 5 | allowNull: false, 6 | defaultValue: DataTypes.UUIDV4, 7 | primaryKey: true, 8 | validate: { 9 | isUUID: 4 10 | }, 11 | unique: true 12 | }, 13 | userId: { 14 | type: DataTypes.UUID 15 | }, 16 | membershipClassId: { 17 | type: DataTypes.INTEGER, 18 | allowNull: false 19 | }, 20 | subscriptionId: { // This is in reference to Stripe codes 21 | type: DataTypes.STRING, 22 | allowNull: false 23 | }, 24 | legacySource: { 25 | type: DataTypes.STRING, 26 | field: 'legacy_source' 27 | }, 28 | start: { 29 | type: DataTypes.TIME 30 | }, 31 | end: { 32 | type: DataTypes.TIME 33 | }, 34 | updatedAt: { 35 | allowNull: false, 36 | type: DataTypes.DATE 37 | }, 38 | createdAt: { 39 | allowNull: false, 40 | type: DataTypes.DATE 41 | }, 42 | deletedAt: { 43 | type: DataTypes.DATE 44 | } 45 | }, { 46 | // timestamps: false, 47 | underscored: true, 48 | modelName: 'UserMembership', 49 | tableName: 'user_memberships' 50 | }) 51 | 52 | UserMembership.associate = (models) => { 53 | UserMembership.hasOne(models.MembershipClass, { as: 'class', sourceKey: 'membershipClassId', foreignKey: 'id' }) 54 | } 55 | 56 | return UserMembership 57 | } 58 | -------------------------------------------------------------------------------- /src/db/models/resonate/user_track_group_purchase.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const UserTrackGroupPurchase = sequelize.define('UserTrackGroupPurchase', { 3 | id: { 4 | type: DataTypes.UUID, 5 | allowNull: false, 6 | defaultValue: DataTypes.UUIDV4, 7 | primaryKey: true, 8 | validate: { 9 | isUUID: 4 10 | }, 11 | unique: true 12 | }, 13 | userId: { 14 | type: DataTypes.UUID, 15 | field: 'user_id', 16 | primaryKey: true 17 | }, 18 | trackGroupId: { 19 | type: DataTypes.UUID, 20 | allowNull: false, 21 | field: 'track_group_id', 22 | primaryKey: true 23 | }, 24 | type: { 25 | type: DataTypes.ENUM, 26 | allowNull: false, 27 | values: ['purchase'] 28 | }, 29 | updatedAt: { 30 | allowNull: false, 31 | type: DataTypes.DATE 32 | }, 33 | createdAt: { 34 | allowNull: false, 35 | type: DataTypes.DATE 36 | } 37 | }, { 38 | sequelize, 39 | timestamps: true, 40 | paranoid: true, 41 | underscored: true, 42 | modelName: 'UserTrackGroupPurchase', 43 | tableName: 'user_track_group_purchase' 44 | }) 45 | 46 | UserTrackGroupPurchase.associate = (models) => { 47 | UserTrackGroupPurchase.belongsTo(models.TrackGroup, { as: 'track_group', sourceKey: 'trackGroupId', foreignKey: 'id' }) 48 | UserTrackGroupPurchase.belongsTo(models.User, { as: 'user', sourceKey: 'userId', foreignKey: 'id' }) 49 | } 50 | 51 | return UserTrackGroupPurchase 52 | } 53 | -------------------------------------------------------------------------------- /src/db/models/resonate/user_track_purchase.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const UserTrackPurchase = sequelize.define('UserTrackPurchase', { 3 | id: { 4 | type: DataTypes.UUID, 5 | allowNull: false, 6 | defaultValue: DataTypes.UUIDV4, 7 | primaryKey: true, 8 | validate: { 9 | isUUID: 4 10 | }, 11 | unique: true 12 | }, 13 | userId: { 14 | type: DataTypes.UUID, 15 | field: 'user_id', 16 | primaryKey: true 17 | }, 18 | trackId: { 19 | type: DataTypes.UUID, 20 | allowNull: false, 21 | field: 'track_id', 22 | primaryKey: true 23 | }, 24 | type: { 25 | type: DataTypes.ENUM, 26 | allowNull: false, 27 | values: ['purchase', 'plays'] 28 | }, 29 | updatedAt: { 30 | allowNull: false, 31 | type: DataTypes.DATE 32 | }, 33 | createdAt: { 34 | allowNull: false, 35 | type: DataTypes.DATE 36 | } 37 | }, { 38 | sequelize, 39 | timestamps: true, 40 | underscored: true, 41 | modelName: 'UserTrackPurchase', 42 | tableName: 'user_track_purchase' 43 | }) 44 | 45 | UserTrackPurchase.associate = (models) => { 46 | UserTrackPurchase.belongsTo(models.Track, { as: 'track', sourceKey: 'trackId', foreignKey: 'id' }) 47 | UserTrackPurchase.belongsTo(models.User, { as: 'user', sourceKey: 'userId', foreignKey: 'id' }) 48 | } 49 | 50 | return UserTrackPurchase 51 | } 52 | -------------------------------------------------------------------------------- /src/db/seeders/clients-seeder.js: -------------------------------------------------------------------------------- 1 | const { faker } = require('@faker-js/faker') 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | // Seeds something for a local client 6 | return queryInterface.bulkInsert('clients', [{ 7 | key: '81b4671a-f40a-4479-a449-c87499904ddf', // faker.datatype.uuid(), 8 | secret: 'matron-fling-raging-send-herself-ninth', 9 | grant_types: ['authorization_code'], 10 | response_types: ['code'], 11 | redirect_uris: ['http://localhost:8080'], 12 | application_name: 'Test', 13 | meta_data: { 'allowed-cors-origins': ['http://localhost:8080'] }, 14 | application_url: 'http://test.test', 15 | created_at: faker.date.past(1), 16 | updated_at: faker.date.past(1) 17 | }], {}, { meta_data: { type: new Sequelize.JSON() } }) 18 | }, 19 | 20 | down: (queryInterface, Sequelize) => { 21 | return queryInterface.bulkDelete('clients', null, {}) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/db/seeders/immutables-seeder.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | // Seeds standard roles 5 | await queryInterface.bulkInsert('roles', [{ 6 | name: 'superadmin', 7 | description: 'SuperAdminRole has all permissions and can assign admins', 8 | is_default: false 9 | }, { 10 | name: 'admin', 11 | description: 'AdminRole has Admin permissions across all tenants, except the ability to assign other Admins', 12 | is_default: false 13 | }, { 14 | description: 'TenantAdmin has Admin permissions over other users in their tenant', 15 | name: 'tenantadmin', 16 | is_default: false 17 | }, { 18 | name: 'artist', 19 | description: 'An artist', 20 | is_default: false 21 | }, { 22 | name: 'label', 23 | description: 'A label', 24 | is_default: false 25 | }, { 26 | name: 'user', 27 | description: 'A basic user', 28 | is_default: true 29 | }]) 30 | 31 | await queryInterface.bulkInsert('membership_classes', [{ 32 | name: 'Listener', 33 | price_id: 'price_', 34 | product_id: 'prod_' 35 | }, { 36 | name: 'Artist', 37 | price_id: 'price_', 38 | product_id: 'prod_' 39 | }, { 40 | name: 'Label', 41 | price_id: 'price_', 42 | product_id: 'prod_' 43 | }]) 44 | 45 | await queryInterface.bulkInsert('user_group_types', [{ 46 | name: 'artist', 47 | description: 'Artist' 48 | }, { 49 | name: 'band', 50 | description: 'Band' 51 | }, { 52 | name: 'label', 53 | description: 'Label' 54 | }, { 55 | name: 'distributor', 56 | description: 'Distributor' 57 | }]) 58 | }, 59 | 60 | down: async (queryInterface, Sequelize) => { 61 | await queryInterface.bulkDelete('roles', null, {}) 62 | await queryInterface.bulkDelete('user_group_types', null, {}) 63 | return queryInterface.bulkDelete('membership_classes', null, {}) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/db/seeders/test/01-clients-seeder.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | // Seeds something for a local client 5 | return queryInterface.sequelize.query( 6 | `INSERT INTO public.clients (id, key, secret, grant_types, response_types, redirect_uris, application_name, application_url, meta_data, updated_at, created_at, deleted_at) 7 | VALUES(1, '81b4671a-f40a-4479-a449-c87499904ddf', 'matron-fling-raging-send-herself-ninth', '{authorization_code}', '{code}', '{http://localhost:8080}', 'Test', 'http://test.test', '{"allowed-cors-origins": ["http://localhost:8080"]}' ,'2022-09-09 16:39:02.39+00', '2022-06-09 09:58:55.29+00', NULL);` 8 | ).catch(console.error) 9 | }, 10 | 11 | down: (queryInterface, Sequelize) => { 12 | return queryInterface.bulkDelete('clients', null, {}) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/db/seeders/test/02-users-seeder.js: -------------------------------------------------------------------------------- 1 | const { Credit } = require('../../models') 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await Credit.create({ 6 | userId: '251c01f6-7293-45f6-b8cd-242bdd76cd0d', 7 | total: 10000 8 | }) 9 | return queryInterface.sequelize.query( 10 | ` 11 | INSERT INTO public.users (id, legacy_id, password, email, email_confirmed, display_name, country, full_name, member, newsletter_notification, last_login, last_password_change, role_id, updated_at, created_at, deleted_at) 12 | VALUES ('1c88dea6-0519-4b61-a279-4006954c5d4c', NULL, '$2a$04$fJ6Du2KKdTG4B2FpvM.diOYSSD6kSBhvwWkeCnejj/s2lublLZIQi', 'artist@admin.com', true, 'artist', NULL, NULL, NULL, NULL, NULL, NULL, 4, '2022-01-30 08:13:41.884+00', '2022-05-15 05:44:49.078+00', NULL); 13 | INSERT INTO public.users (id, legacy_id, password, email, email_confirmed, display_name, country, full_name, member, newsletter_notification, last_login, last_password_change, role_id, updated_at, created_at, deleted_at) 14 | VALUES ('251c01f6-7293-45f6-b8cd-242bdd76cd0d', NULL, '$2a$04$2kRrLFrEW/XL6X4TY2pzTO18N7Y5wd5f32HzxK.4.rSLakFIhh.Qq', 'listener@admin.com', true, 'listener', NULL, NULL, NULL, NULL, NULL, NULL, 4, '2022-05-25 11:16:07.828+00', '2021-11-11 05:55:47.116+00', NULL); 15 | INSERT INTO public.users (id, legacy_id, password, email, email_confirmed, display_name, country, full_name, member, newsletter_notification, last_login, last_password_change, role_id, updated_at, created_at, deleted_at) 16 | VALUES ('71175a23-9256-41c9-b8c1-cd2170aa6591', NULL, '$2a$04$XPeruRJNIrnewWfkhtZ3v.5AZxgRgGUh3T/k57040AFXKUlzDvva.', 'admin@admin.com', true, 'admin', NULL, NULL, NULL, NULL, NULL, NULL, 1, '2021-11-02 23:32:46.074+00', '2021-11-23 02:12:17.516+00', NULL); 17 | ` 18 | ).catch(console.error) 19 | }, 20 | down: (queryInterface, Sequelize) => { 21 | return queryInterface.bulkDelete('users', null, {}) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/db/seeders/users-seeder.js: -------------------------------------------------------------------------------- 1 | const { faker } = require('@faker-js/faker') 2 | const { User, Role } = require('../models') 3 | 4 | module.exports = { 5 | up: async (queryInterface, Sequelize) => { 6 | // Seeds something for a local client 7 | const role = await Role.findOne({ 8 | where: { 9 | name: 'superadmin' 10 | } 11 | }) 12 | 13 | await queryInterface.bulkInsert('users', [{ 14 | id: faker.datatype.uuid(), 15 | email: 'admin@admin.com', 16 | password: await User.hashPassword({ password: 'test1234' }), 17 | email_confirmed: true, 18 | role_id: role.id, 19 | display_name: 'admin', 20 | created_at: faker.date.past(1), 21 | updated_at: faker.date.past(1) 22 | }]) 23 | }, 24 | 25 | down: (queryInterface, Sequelize) => { 26 | return queryInterface.bulkDelete('users', null, {}) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/jobs/audio-duration.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const getAudioDuration = require('../util/get-audio-duration') 3 | 4 | const BASE_DATA_DIR = process.env.BASE_DATA_DIR || '/' 5 | 6 | module.exports = async job => { 7 | const { filename } = job.data 8 | try { 9 | // fallback for file with no headers? 10 | // see: https://github.com/Borewit/music-metadata/issues/543 11 | // https://github.com/Borewit/music-metadata/pull/584 partially addressed? 12 | const duration = await getAudioDuration(path.join(BASE_DATA_DIR, `/data/media/incoming/${filename}`)) 13 | 14 | return Promise.resolve(duration) 15 | } catch (err) { 16 | return Promise.reject(err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/jobs/cleanup.js: -------------------------------------------------------------------------------- 1 | const { promises: fs } = require('fs') 2 | const winston = require('winston') 3 | const path = require('path') 4 | 5 | const BASE_DATA_DIR = process.env.BASE_DATA_DIR || '/' 6 | 7 | const logger = winston.createLogger({ 8 | level: 'info', 9 | format: winston.format.json(), 10 | defaultMeta: { service: 'cleanup' }, 11 | transports: [ 12 | new winston.transports.Console({ 13 | level: 'debug', 14 | format: winston.format.simple() 15 | }), 16 | new winston.transports.File({ 17 | filename: 'error.log', 18 | level: 'error' 19 | }) 20 | ] 21 | }) 22 | 23 | /** 24 | * Cleanup incoming folder and more (later) 25 | */ 26 | 27 | const cleanupJob = async (job) => { 28 | try { 29 | await fs.unlink(path.join(BASE_DATA_DIR, `/data/media/incoming/${job.data.filename}`)) 30 | 31 | logger.info('file removed') 32 | 33 | return Promise.resolve() 34 | } catch (err) { 35 | return Promise.reject(err) 36 | } 37 | } 38 | 39 | export default cleanupJob 40 | -------------------------------------------------------------------------------- /src/jobs/prepare-download.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a zip archive of requested file formats for a given track 3 | * includes high res artworks if applicable 4 | */ 5 | 6 | export const zipForDownload = async (job) => { 7 | try { 8 | return Promise.resolve() 9 | } catch (err) { 10 | return Promise.reject(err) 11 | } 12 | } 13 | 14 | export default zipForDownload 15 | -------------------------------------------------------------------------------- /src/jobs/send-mail.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston') 2 | const path = require('path') 3 | const nodemailer = require('nodemailer') 4 | const sendgrid = require('nodemailer-sendgrid') 5 | const Email = require('email-templates') 6 | 7 | const logger = winston.createLogger({ 8 | level: 'info', 9 | format: winston.format.json(), 10 | defaultMeta: { service: 'cleanup' }, 11 | transports: [ 12 | new winston.transports.Console({ 13 | level: 'debug', 14 | format: winston.format.simple() 15 | }), 16 | new winston.transports.File({ 17 | filename: 'error.log', 18 | level: 'error' 19 | }) 20 | ] 21 | }) 22 | 23 | const viewsDir = path.join(__dirname, '../../emails') 24 | 25 | /** 26 | * Cleanup incoming folder and more (later) 27 | */ 28 | 29 | const sendMail = async (job) => { 30 | try { 31 | const email = new Email({ 32 | message: { 33 | from: `"Resonate" <${process.env.SENDGRID_SENDER ?? 'members@resonate.coop'}>` 34 | }, 35 | juice: true, 36 | send: true, 37 | juiceResources: { 38 | preserveImportant: true, 39 | webResources: { 40 | relativeTo: path.resolve(viewsDir) 41 | } 42 | }, 43 | transport: nodemailer.createTransport( 44 | sendgrid({ 45 | apiKey: process.env.SENDGRID_API_KEY 46 | }) 47 | ) 48 | }) 49 | 50 | if (process.env.NODE_ENV === 'production') { 51 | await email.send({ 52 | template: job.data.template, 53 | message: job.data.message, 54 | locals: job.data.locals 55 | }) 56 | } else { 57 | email.render(job.data.template + '/html', job.data.locals) 58 | .then(logger.info) 59 | } 60 | 61 | logger.info('Email sent') 62 | 63 | return Promise.resolve() 64 | } catch (err) { 65 | return Promise.reject(err) 66 | } 67 | } 68 | 69 | module.exports = sendMail 70 | -------------------------------------------------------------------------------- /src/schemas/tracks.js: -------------------------------------------------------------------------------- 1 | const AJV = require('ajv') 2 | const ajvKeywords = require('ajv-keywords') 3 | const ajvFormats = require('ajv-formats') 4 | 5 | const ajv = new AJV({ 6 | allErrors: true, 7 | removeAdditional: true 8 | }) 9 | 10 | ajvKeywords(ajv) 11 | ajvFormats(ajv) 12 | 13 | const validate = ajv.compile({ 14 | type: 'object', 15 | additionalProperties: false, 16 | properties: { 17 | title: { 18 | type: 'string' 19 | }, 20 | artist: { 21 | type: 'string' 22 | }, 23 | album_artist: { 24 | type: 'string' 25 | }, 26 | album: { 27 | type: 'string' 28 | }, 29 | composer: { 30 | type: 'string' 31 | }, 32 | status: { 33 | type: 'string', 34 | enum: ['paid', 'free', 'hidden'], 35 | default: 'paid' 36 | }, 37 | year: { 38 | type: 'number', 39 | minimum: 1900, 40 | maximum: new Date().getFullYear() + 1 41 | } 42 | } 43 | }) 44 | 45 | module.exports.validate = validate 46 | -------------------------------------------------------------------------------- /src/scripts/README.md: -------------------------------------------------------------------------------- 1 | # Reports and scripts 2 | -------------------------------------------------------------------------------- /src/util/cover-src.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_STATIC_BASE_PATH = process.env.STATIC_MEDIA_HOST 2 | 3 | const coverSrc = (uuid, size, ext, fallback = false) => { 4 | if (!uuid) return 5 | 6 | const pathname = `/images/${uuid}-x${size}${ext}` 7 | const url = new URL(pathname, DEFAULT_STATIC_BASE_PATH) 8 | 9 | // if (fallback) { 10 | // pathname = `/track-artwork/${size}x${size}/${uuid}` 11 | // url = new URL(pathname, FALLBACK_STATIC_BASE_PATH) 12 | // } 13 | 14 | return url.href 15 | } 16 | 17 | module.exports = coverSrc 18 | -------------------------------------------------------------------------------- /src/util/dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prevent access to non dev env 3 | */ 4 | 5 | module.exports.isDev = () => { 6 | return async (ctx, next) => { 7 | if (process.env.NODE_ENV !== 'development') { 8 | ctx.status = 403 9 | ctx.throw(ctx.status) 10 | } 11 | await next() 12 | } 13 | } 14 | 15 | module.exports.isEnv = (allowed = ['development', 'test']) => { 16 | return async (ctx, next) => { 17 | if (!allowed.includes(process.env.NODE_ENV)) { 18 | ctx.status = 403 19 | ctx.throw(ctx.status) 20 | } 21 | await next() 22 | } 23 | } 24 | 25 | // module.exports = module.exports.isDev 26 | -------------------------------------------------------------------------------- /src/util/ffprobe-metadata.js: -------------------------------------------------------------------------------- 1 | const ffmpeg = require('fluent-ffmpeg') 2 | const winston = require('winston') 3 | 4 | const logger = winston.createLogger({ 5 | level: 'info', 6 | format: winston.format.json(), 7 | defaultMeta: { service: 'ffprobe-metadata' }, 8 | transports: [ 9 | new winston.transports.Console({ 10 | level: 'debug', 11 | format: winston.format.simple() 12 | }), 13 | new winston.transports.File({ 14 | filename: 'error.log', 15 | level: 'error' 16 | }) 17 | ] 18 | }) 19 | 20 | function ffprobe (pathname) { 21 | return new Promise((resolve, reject) => { 22 | const profiler = logger.startTimer() 23 | 24 | return ffmpeg.ffprobe(pathname, (err, metadata) => { 25 | if (err) { 26 | return reject(err) 27 | } 28 | 29 | profiler.done({ message: 'Done parsing metadata' }) 30 | 31 | let data = { 32 | duration: metadata.format.duration 33 | } 34 | 35 | if ('tags' in metadata.format) { 36 | // make sure to normalize object keys 37 | // TODO normalize case to snake case ? 38 | const tags = Object.fromEntries(Object.entries(metadata.format.tags).map(([k, v]) => { 39 | return [k.toLowerCase(), v] 40 | })) 41 | 42 | const year = tags.date || tags.year 43 | 44 | if (year) { 45 | data.year = new Date(tags.date || tags.year).getFullYear() 46 | } 47 | 48 | if (tags.track) { 49 | data.number = tags.track 50 | } 51 | 52 | data = Object.assign(data, { 53 | title: tags.title, 54 | genre: tags.genre, 55 | artist: tags.artist, 56 | album: tags.album, 57 | album_artist: tags.album_artist 58 | }) 59 | } 60 | 61 | return resolve(data) 62 | }) 63 | }) 64 | } 65 | 66 | module.exports = ffprobe 67 | -------------------------------------------------------------------------------- /src/util/get-audio-duration.js: -------------------------------------------------------------------------------- 1 | const ffmpeg = require('fluent-ffmpeg') 2 | 3 | // Try this with some flac with no headers 4 | function getAudioDurationInSeconds (filepath) { 5 | return new Promise((resolve, reject) => { 6 | ffmpeg.ffprobe(filepath, (err, metadata) => { 7 | if (err) return reject(err) 8 | 9 | return resolve(metadata.format.duration) 10 | }) 11 | }) 12 | } 13 | 14 | module.exports = getAudioDurationInSeconds 15 | -------------------------------------------------------------------------------- /src/util/links.js: -------------------------------------------------------------------------------- 1 | const decodeUriComponent = require('decode-uri-component') 2 | const normalizeUrl = require('normalize-url') 3 | 4 | const base = { 5 | facebook: 'https://www.facebook.com', 6 | instagram: 'https://www.instagram.com', 7 | twitter: 'https://twitter.com', 8 | vimeo: 'https://vimeo.com', 9 | youtube: 'https://www.youtube.com/user' 10 | } 11 | 12 | module.exports = (...links) => { 13 | return links 14 | .filter(([platform, value]) => { 15 | if (!value) return false 16 | return true 17 | }) 18 | .map(([platform, value]) => { 19 | const isWebsite = platform === 'website' 20 | 21 | if (!isWebsite) { 22 | const parts = value.split('/') 23 | 24 | if (parts.length === 1) { 25 | // assume it's only an username 26 | value = base[platform] + '/' + value 27 | return { 28 | href: value, 29 | text: normalizeUrl(value, { stripWWW: true, stripProtocol: true }) 30 | } 31 | } 32 | } 33 | 34 | let removeQueryParameters = [/^\w+/i] // remove all query params 35 | 36 | if (platform === 'youtube' && value.includes('watch?')) { 37 | removeQueryParameters = [/^utm_\w+/i] // default 38 | } 39 | 40 | return { 41 | href: decodeUriComponent(normalizeUrl(value, { forceHttps: !isWebsite, stripWWW: false, removeQueryParameters })), 42 | text: decodeUriComponent(normalizeUrl(value, { stripWWW: true, stripProtocol: true, removeQueryParameters })) 43 | } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/util/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston') 2 | 3 | /** 4 | * @description Create a winston logger instance 5 | * @param {Object} options 6 | */ 7 | 8 | const createLogger = (options) => { 9 | const { 10 | level = { 11 | production: 'error', 12 | development: 'info', 13 | test: 'debug' 14 | }[process.env.NODE_ENV], 15 | format = winston.format, 16 | service = 'resonate-logger' 17 | } = options 18 | 19 | const filename = { 20 | test: 'error.test.log' 21 | }[process.env.NODE_ENV] || 'error.log' 22 | 23 | const logger = winston.createLogger({ 24 | level: level, 25 | format: format, 26 | defaultMeta: { service: service }, 27 | transports: [ 28 | new winston.transports.File({ 29 | filename: filename, 30 | level: 'error' 31 | }) 32 | ] 33 | }) 34 | 35 | if (process.env.NODE_ENV !== 'production') { 36 | logger.add(new winston.transports.Console({ 37 | level: level, 38 | format: winston.format.combine( 39 | winston.format.colorize(), 40 | winston.format.simple() 41 | ) 42 | })) 43 | } 44 | 45 | return logger 46 | } 47 | 48 | module.exports = { createLogger, winston } 49 | -------------------------------------------------------------------------------- /src/util/query.js: -------------------------------------------------------------------------------- 1 | const queryBuilder = (sequelize) => { 2 | return (query, values) => { 3 | return sequelize.query(query, { 4 | type: sequelize.QueryTypes.SELECT, 5 | replacements: values 6 | }) 7 | } 8 | } 9 | 10 | module.exports = queryBuilder 11 | -------------------------------------------------------------------------------- /test/-- best album ever.pgsql: -------------------------------------------------------------------------------- 1 | -- best album ever 2 | 3 | -- get track group info for best album ever 4 | -- returns track group id= '84322e4f-0247-427f-8bed-e7617c3df5ad'; 5 | select * 6 | from track_groups 7 | where title = 'Best album ever'; 8 | 9 | -- get track group items 10 | -- select id, track_id 11 | select * 12 | from track_group_items 13 | where track_group_id = '84322e4f-0247-427f-8bed-e7617c3df5ad'; 14 | -- -- -- where track_group_id = '8e9c188c-0f1f-4c99-ac89-0709970345bd' 15 | -- -- where track_group_id = '58991f22-b172-48e4-8b27-e0a4c946f9b2' 16 | -- order by index; 17 | 18 | -- get tracks by a track group 19 | -- track_group_items.track_id = tracks.id 20 | select * 21 | from tracks t 22 | where t.id in ( 23 | select tgi.track_id 24 | from track_group_items tgi 25 | where tgi.track_group_id = '84322e4f-0247-427f-8bed-e7617c3df5ad' 26 | order by index 27 | ); 28 | 29 | -- get a track 30 | -- Ergonomic interactive concept / Laurie Yost 31 | select * 32 | from tracks 33 | where id = '44a28752-1101-4e0d-8c40-2c36dc82d035'; 34 | -- where id = '706cff12-ba44-49f7-8982-98b3996a2919'; 35 | 36 | -- get files for tracks 37 | -- tracks.track_url looks like it keys to file.id, but not certain about this. 38 | -- delete from files; 39 | 40 | select * 41 | from files; 42 | -------------------------------------------------------------------------------- /test/HowTheTestDataWasMade.md: -------------------------------------------------------------------------------- 1 | 2 | # How the test data was made 3 | 4 | TL;DR: Add new test data by creating new SQL INSERT INTO statements in a new test data seeder in the `api/src/db/seeders/test` folder. Create a new seeder in order to not disrupt the existing test data. You will need to re-seed the test database, as if you were starting the test container for the first time. 5 | 6 | You should never replace the existing test data set. If you do, you will have to rewrite every assertion in every test. 7 | 8 | Please DO NOT change the existing SQL statements / test seeders. You can add new test data to the test data set by creating new test seeders in the `api/src/db/seeders/test` directory and adding the appropriate SQL INSERT INTO statements into the new seeders. 9 | 10 | What follows is an overview of the process that created the original test data set. It might help you figure out how to create additional, new test data: 11 | 12 | * The development Docker container was started up 13 | * The pgsql service in this Docker container was accessed externally using pgAdmin 14 | * A database backup was created, using pgAdmin 'Backup Server...' 15 | * On the 'General' tab 16 | * Use 'Plain' format 17 | * Enter a filename / location 18 | * Encoding: UTF-8 19 | * Role name: resonate 20 | * On the 'Data/Objects' tab 21 | * 'Type of Objects' 22 | * Only data 23 | * On the 'Options' tab 24 | * 'Queries' 25 | * 'Use Column Inserts' set to true 26 | * 'Use Insert Commands' set to true 27 | * Click 'Backup' 28 | * Now there is a plain text/sql file at the name/location you saved 29 | * This file should contain a lot of INSERT INTO statements. 30 | * These statements go into api/src/db/seeders/test files. 31 | * You will need to work out which statements belong in which file. It is probably best to create new seeder files, so that the original, base data is not disrupted. 32 | 33 | * Don't forget to re-seed the database after you enter all of the INSERT INTO statements. -------------------------------------------------------------------------------- /test/MockAccessToken.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | // require this in tests that need accessToken-based authentication. 4 | // const MockAccessToken = require('../MockAccessToken') 5 | 6 | // In the test's primary 'describe' block, put 7 | // MockAccessToken(some.user.id) 8 | // as the first line inside of the describe block 9 | // Look at test/auth/AccessTokenExample.test.js for more infos 10 | 11 | const { TestRedisAdapter, testAccessToken } = require('./testConfig') 12 | 13 | const MockAccessToken = (userId) => { 14 | // get a Redis 15 | const adapter = new TestRedisAdapter('AccessToken') 16 | 17 | // Give a test access token to a Redis. Then Redis will believe there is a valid login. 18 | before('send access token to Redis', async () => { 19 | await adapter.upsert(testAccessToken, { 20 | accountId: userId 21 | }) 22 | }) 23 | // Remove a test access token from a Redis. 24 | after('remove access token from Redis', async () => { 25 | await adapter.destroy(testAccessToken) 26 | }) 27 | } 28 | 29 | module.exports = MockAccessToken 30 | -------------------------------------------------------------------------------- /test/ResetDB.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const Umzug = require('umzug') 4 | 5 | const { Resonate: sequelize } = require('../src/db/models') 6 | 7 | const seedsConfig = { 8 | storage: 'sequelize', 9 | storageOptions: { 10 | sequelize: sequelize, 11 | modelName: 'SequelizeData' // Or whatever you want to name the seeder storage table 12 | }, 13 | migrations: { 14 | params: [ 15 | sequelize.getQueryInterface(), 16 | sequelize.constructor 17 | ], 18 | path: 'src/db/seeders/test', // path to folder containing seeds 19 | pattern: /\.js$/ 20 | } 21 | } 22 | 23 | const seeder = new Umzug(seedsConfig) 24 | 25 | const ResetDB = () => { 26 | before('reset the test database to test seed data', async () => { 27 | await seeder.down({ to: 0 }) 28 | await seeder.up() 29 | console.log(' >>> reset the db! >>>') 30 | }) 31 | } 32 | 33 | module.exports = ResetDB 34 | -------------------------------------------------------------------------------- /test/Template.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* eslint-env mocha */ 3 | 4 | // EDIT THIS TO SUIT THE REQUIREMENTS OF THE TEST/S YOU WRITE 5 | const { request, expect } = require('./testConfig') // change this to require('../../testConfig') after you copy this template 6 | // uncomment MockAccessToken if you need to test protected routes. 7 | // look at auth/AccessTokenExample.test.js for infos. 8 | // const MockAccessToken = require('../MockAccessToken') 9 | 10 | describe('TEMPLATE endpoint test', () => { 11 | // If you are writing tests for endpoints that require authentication, 12 | // uncomment the next line. Otherwise you can remove these three lines. 13 | // MockAccessToken(some.test.user.id) 14 | 15 | let response = null 16 | 17 | it('should do something', async () => { 18 | response = await request.get('/') 19 | 20 | expect(response.status).to.eql(404) 21 | 22 | // Uncomment these lines to start testing the response from the endpoint 23 | // const attributes = response.body 24 | // expect(attributes).to.be.an('object') 25 | // expect(attributes).to.include.keys('') 26 | 27 | // expect(attributes.data).to.be.an('array') 28 | // expect(attributes.data.length).to.eql(3) 29 | 30 | // const theData = attributes.data[0] 31 | // expect(theData).to.include.keys('') 32 | // expect(theData.xxx).to.eql() 33 | 34 | // expect(attributes.count).to.eql(1) 35 | // expect(attributes.numberOfPages).to.eql(1) 36 | // expect(attributes.status).to.eql('ok') 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/baseline/auth/AccessTokenExample.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* eslint-env mocha */ 3 | 4 | // Examaple for how to use the MockAccessToken.js file 5 | 6 | const { expect, testAccessToken, testUserId } = require('../../testConfig') 7 | const MockAccessToken = require('../../MockAccessToken') 8 | 9 | describe('Access token example test', () => { 10 | // Provides before() and after(). sets dummy accessToken, in order to test protected routes. 11 | MockAccessToken(testUserId) 12 | 13 | // FIXME: should actually get the token from Redis then display it, in order to confirm 14 | // that is was set correctly. 15 | 16 | it('should have correct test access token', async () => { 17 | expect(testAccessToken).to.eql('test-!@#$-test-%^&*') 18 | }) 19 | it('should have correct test user id', async () => { 20 | expect(testUserId).to.eql('251c01f6-7293-45f6-b8cd-242bdd76cd0d') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/baseline/auth/Auth.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* eslint-env mocha */ 3 | 4 | const baseURL = `${process.env.APP_HOST}` 5 | const request = require('supertest')(baseURL) 6 | const { expect } = require('../../testConfig') 7 | const { User } = require('../../../src/db/models') 8 | 9 | const ResetDB = require('../../ResetDB') 10 | 11 | const { faker } = require('@faker-js/faker') 12 | 13 | describe('Auth endpoint test', () => { 14 | ResetDB() 15 | let response = null 16 | 17 | it('should handle new user registration', async () => { 18 | response = await request.post('/register') 19 | .send({ 20 | email: faker.internet.email(), 21 | password: faker.internet.password() 22 | }) 23 | .type('form') 24 | 25 | expect(response.status).to.eql(200) 26 | }) 27 | 28 | it('should handle password reset', async () => { 29 | const user = await User.create({ 30 | email: faker.internet.email(), 31 | password: 'blabla', 32 | roleId: 1 33 | }) 34 | response = await request.post('/password-reset') 35 | .send({ 36 | email: user.email 37 | }) 38 | .type('form') 39 | 40 | expect(response.status).to.eql(200) 41 | expect(response.text).to.include('Password reset email sent') 42 | 43 | await user.reload() 44 | expect(user.emailConfirmationToken).not.to.be.null 45 | expect(user.emailConfirmationExpiration).not.to.be.null 46 | 47 | await user.destroy({ force: true }) 48 | }) 49 | 50 | it('should handle password reset with non-existent password', async () => { 51 | response = await request.post('/password-reset') 52 | .send({ 53 | email: faker.internet.email() 54 | }) 55 | .type('form') 56 | 57 | expect(response.status).to.eql(200) 58 | expect(response.text).to.include('No user with this email exists') 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/baseline/search/Search.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* eslint-env mocha */ 3 | 4 | const { request, expect, testArtistUserId } = require('../../testConfig') 5 | const { faker } = require('@faker-js/faker') 6 | const { UserGroup, UserGroupType } = require('../../../src/db/models') 7 | const ResetDB = require('../../ResetDB') 8 | 9 | describe('baseline/search endpoint test', () => { 10 | ResetDB() 11 | 12 | it('should handle no provided search term/s', async () => { 13 | const response = await request.get('/search') 14 | 15 | expect(response.status).to.eql(400) 16 | }) 17 | 18 | it('should return empty results for a search term', async () => { 19 | const searchTerm = 'asdf' 20 | 21 | const response = await request.get(`/search?q=${searchTerm}`) 22 | expect(response.status).to.eql(200) 23 | expect(response.body.data).to.include.keys('artists', 'labels', 'tracks', 'trackgroups', 'bands') 24 | }) 25 | 26 | it('should GET /search?q=', async () => { 27 | const displayName = faker.name.fullName() 28 | const type = await UserGroupType.findOne({ where: { name: 'artist' } }) 29 | 30 | await UserGroup.create({ 31 | displayName: displayName, 32 | ownerId: testArtistUserId, 33 | typeId: type.id 34 | }) 35 | 36 | const response = await request.get(`/search?q=${displayName}`) 37 | console.log('response', response.body) 38 | expect(response.status).to.eql(200) 39 | expect(response.body.data.artists[0].displayName).to.eql(displayName) 40 | 41 | await UserGroup.destroy({ 42 | where: { 43 | displayName, 44 | typeId: type.id 45 | }, 46 | force: true 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /test/baseline/user/Products.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* eslint-env mocha */ 3 | 4 | const { request, expect, testAdminUserId, testAccessToken, testInvalidAccessToken } = require('../../testConfig') 5 | const MockAccessToken = require('../../MockAccessToken') 6 | 7 | describe('User.ts/products endpoint test', () => { 8 | MockAccessToken(testAdminUserId) 9 | 10 | let response = null 11 | 12 | it('should handle no authentication / accessToken', async () => { 13 | response = await request.get('/user/products') 14 | 15 | expect(response.status).to.eql(401) 16 | }) 17 | it('should handle an invalid access token', async () => { 18 | response = await request.get('/user/products').set('Authorization', `Bearer ${testInvalidAccessToken}`) 19 | 20 | expect(response.status).to.eql(401) 21 | }) 22 | 23 | // FIXME: 20221010 Skip. This test currently throws a StripeAuthenticationError 24 | // 'Invalid API Key provided: test' 25 | it.skip('should get user products', async () => { 26 | response = await request.get('/user/products').set('Authorization', `Bearer ${testAccessToken}`) 27 | 28 | console.log('user products RESPONSE: ', response.text) 29 | 30 | expect(response.status).to.eql(200) 31 | 32 | // const attributes = response.body 33 | // expect(attributes).to.be.an('object') 34 | // expect(attributes).to.include.keys("data", "count", "numberOfPages", "status") 35 | 36 | // expect(attributes.data).to.be.an('array') 37 | // expect(attributes.data.length).to.eql(3) 38 | 39 | // const theData = attributes.data[0] 40 | // expect(theData).to.include.keys("") 41 | // expect(theData.xxx).to.eql() 42 | 43 | // expect(attributes.count).to.eql(1) 44 | // expect(attributes.numberOfPages).to.eql(1) 45 | // expect(attributes.status).to.eql('ok') 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/baseline/users/users.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* eslint-env mocha */ 3 | const { request, expect, testArtistId } = require('../../testConfig') 4 | const { User, Playlist } = require('../../../src/db/models') 5 | const { faker } = require('@faker-js/faker') 6 | const ResetDB = require('../../ResetDB') 7 | 8 | describe('/users endpoint tests', () => { 9 | ResetDB() 10 | let response = null 11 | 12 | it('should GET /users/:id', async () => { 13 | const displayName = faker.animal.rabbit() 14 | const user = await User.create({ 15 | displayName, 16 | creatorId: testArtistId, 17 | status: 'paid', 18 | password: 'test', 19 | 20 | roleId: 5, 21 | email: 'email@email.com' 22 | }) 23 | 24 | response = await request.get('/users/' + user.id) 25 | 26 | expect(response.status).to.eql(200) 27 | expect(response.body.data.displayName).to.eql(displayName) 28 | 29 | await user.destroy({ force: true }) 30 | }) 31 | 32 | it('should GET /users/:id/playlists', async () => { 33 | const displayName = faker.animal.rabbit() 34 | const user = await User.create({ 35 | displayName, 36 | creatorId: testArtistId, 37 | status: 'paid', 38 | password: 'test', 39 | 40 | roleId: 5, 41 | email: 'email@email.com' 42 | }) 43 | 44 | const privatePlaylist = await Playlist.create({ 45 | title: faker.music.songName(), 46 | private: true, 47 | creatorId: user.id 48 | }) 49 | 50 | const publicPlaylist = await Playlist.create({ 51 | title: faker.music.songName(), 52 | private: false, 53 | creatorId: user.id 54 | }) 55 | 56 | response = await request.get('/users/' + user.id + '/playlists') 57 | 58 | expect(response.status).to.eql(200) 59 | expect(response.body.data.length).to.eql(1) 60 | expect(response.body.data[0].id).to.eql(publicPlaylist.id) 61 | 62 | await user.destroy({ force: true }) 63 | await privatePlaylist.destroy({ force: true }) 64 | await publicPlaylist.destroy({ force: true }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /test/media/audio/112e3c00-7727-4e7b-8e60-be0751d56a77.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/112e3c00-7727-4e7b-8e60-be0751d56a77.m4a -------------------------------------------------------------------------------- /test/media/audio/23d45613-5b56-4ceb-9a2c-efa266beaeeb.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/23d45613-5b56-4ceb-9a2c-efa266beaeeb.m4a -------------------------------------------------------------------------------- /test/media/audio/26de83c3-537a-4a09-8942-2deb1eb42a04.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/26de83c3-537a-4a09-8942-2deb1eb42a04.m4a -------------------------------------------------------------------------------- /test/media/audio/3d141f46-de2f-4b0f-af6e-34cf6b987805.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/3d141f46-de2f-4b0f-af6e-34cf6b987805.m4a -------------------------------------------------------------------------------- /test/media/audio/4ef71341-1de3-4b19-b224-46878619f7a4.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/4ef71341-1de3-4b19-b224-46878619f7a4.m4a -------------------------------------------------------------------------------- /test/media/audio/57200189-5eb7-434e-a023-57baabea9eda.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/57200189-5eb7-434e-a023-57baabea9eda.m4a -------------------------------------------------------------------------------- /test/media/audio/57f0476b-c5cf-43c9-8aad-ede6bde080c4.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/57f0476b-c5cf-43c9-8aad-ede6bde080c4.m4a -------------------------------------------------------------------------------- /test/media/audio/692a9e72-8278-4ae1-b9e3-e17c8773db77.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/692a9e72-8278-4ae1-b9e3-e17c8773db77.m4a -------------------------------------------------------------------------------- /test/media/audio/69560f28-2953-4ed9-9a5d-a1a4cc7db047.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/69560f28-2953-4ed9-9a5d-a1a4cc7db047.m4a -------------------------------------------------------------------------------- /test/media/audio/6fe8b335-d3ae-42e2-b2ac-a26866402520.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/6fe8b335-d3ae-42e2-b2ac-a26866402520.m4a -------------------------------------------------------------------------------- /test/media/audio/7516dc73-e304-43a7-9f00-0ba387acac9b.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/7516dc73-e304-43a7-9f00-0ba387acac9b.m4a -------------------------------------------------------------------------------- /test/media/audio/7d9df3f3-b85d-4f3f-8fbe-bf48e6f1cb51.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/7d9df3f3-b85d-4f3f-8fbe-bf48e6f1cb51.m4a -------------------------------------------------------------------------------- /test/media/audio/843eb984-da5b-4244-8ed7-2678948cca19.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/843eb984-da5b-4244-8ed7-2678948cca19.m4a -------------------------------------------------------------------------------- /test/media/audio/8a45c5c4-4986-4140-bcf2-ac55f072ee93.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/8a45c5c4-4986-4140-bcf2-ac55f072ee93.m4a -------------------------------------------------------------------------------- /test/media/audio/8b30f3c5-37e5-4cba-b99c-f3cb7b5c15d2.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/8b30f3c5-37e5-4cba-b99c-f3cb7b5c15d2.m4a -------------------------------------------------------------------------------- /test/media/audio/9013c592-1576-4fa4-b2ea-9953bf8a21b0.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/9013c592-1576-4fa4-b2ea-9953bf8a21b0.m4a -------------------------------------------------------------------------------- /test/media/audio/911ba504-d74b-4cd6-83f6-33937e03cd46.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/911ba504-d74b-4cd6-83f6-33937e03cd46.m4a -------------------------------------------------------------------------------- /test/media/audio/9a99d504-9270-450f-b64f-06daed70fd5a.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/9a99d504-9270-450f-b64f-06daed70fd5a.m4a -------------------------------------------------------------------------------- /test/media/audio/9ba71ad7-eb4f-4a56-b8da-e3f395f590ea.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/9ba71ad7-eb4f-4a56-b8da-e3f395f590ea.m4a -------------------------------------------------------------------------------- /test/media/audio/a6cb36e6-77ff-4a37-ba83-0d828b254183.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/a6cb36e6-77ff-4a37-ba83-0d828b254183.m4a -------------------------------------------------------------------------------- /test/media/audio/ac4833b7-5aa5-4205-8d4a-c8c9575a2bc0.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/ac4833b7-5aa5-4205-8d4a-c8c9575a2bc0.m4a -------------------------------------------------------------------------------- /test/media/audio/b60f1759-6405-4457-9910-6da1ccd5f40f.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/b60f1759-6405-4457-9910-6da1ccd5f40f.m4a -------------------------------------------------------------------------------- /test/media/audio/bf8f778a-793e-4e66-bf19-f10ede501ea3.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/bf8f778a-793e-4e66-bf19-f10ede501ea3.m4a -------------------------------------------------------------------------------- /test/media/audio/c1a1aea3-25d5-4608-93e5-7616a7d25387.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/c1a1aea3-25d5-4608-93e5-7616a7d25387.m4a -------------------------------------------------------------------------------- /test/media/audio/d2cbc2b4-36a6-4854-85f9-ed0aa0b46711.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/d2cbc2b4-36a6-4854-85f9-ed0aa0b46711.m4a -------------------------------------------------------------------------------- /test/media/audio/dba9e81b-d32c-4541-a99a-c81cb8fb142b.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/dba9e81b-d32c-4541-a99a-c81cb8fb142b.m4a -------------------------------------------------------------------------------- /test/media/audio/de7dfe91-1122-4a64-a757-20d7817251a4.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/de7dfe91-1122-4a64-a757-20d7817251a4.m4a -------------------------------------------------------------------------------- /test/media/audio/f27f9c60-ed1c-436c-9382-72a26abf644d.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/f27f9c60-ed1c-436c-9382-72a26abf644d.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-112e3c00-7727-4e7b-8e60-be0751d56a77.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-112e3c00-7727-4e7b-8e60-be0751d56a77.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-1a20b4b0-b853-4e75-8815-f5148af2b64a.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-1a20b4b0-b853-4e75-8815-f5148af2b64a.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-1b41b2ec-36d8-4385-8f15-bd25b55005db.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-1b41b2ec-36d8-4385-8f15-bd25b55005db.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-23d45613-5b56-4ceb-9a2c-efa266beaeeb.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-23d45613-5b56-4ceb-9a2c-efa266beaeeb.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-26de83c3-537a-4a09-8942-2deb1eb42a04.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-26de83c3-537a-4a09-8942-2deb1eb42a04.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-3d141f46-de2f-4b0f-af6e-34cf6b987805.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-3d141f46-de2f-4b0f-af6e-34cf6b987805.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-4ef71341-1de3-4b19-b224-46878619f7a4.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-4ef71341-1de3-4b19-b224-46878619f7a4.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-57200189-5eb7-434e-a023-57baabea9eda.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-57200189-5eb7-434e-a023-57baabea9eda.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-57f0476b-c5cf-43c9-8aad-ede6bde080c4.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-57f0476b-c5cf-43c9-8aad-ede6bde080c4.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-692a9e72-8278-4ae1-b9e3-e17c8773db77.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-692a9e72-8278-4ae1-b9e3-e17c8773db77.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-69560f28-2953-4ed9-9a5d-a1a4cc7db047.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-69560f28-2953-4ed9-9a5d-a1a4cc7db047.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-6fe8b335-d3ae-42e2-b2ac-a26866402520.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-6fe8b335-d3ae-42e2-b2ac-a26866402520.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-7516dc73-e304-43a7-9f00-0ba387acac9b.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-7516dc73-e304-43a7-9f00-0ba387acac9b.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-7d9df3f3-b85d-4f3f-8fbe-bf48e6f1cb51.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-7d9df3f3-b85d-4f3f-8fbe-bf48e6f1cb51.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-843eb984-da5b-4244-8ed7-2678948cca19.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-843eb984-da5b-4244-8ed7-2678948cca19.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-8a45c5c4-4986-4140-bcf2-ac55f072ee93.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-8a45c5c4-4986-4140-bcf2-ac55f072ee93.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-8b30f3c5-37e5-4cba-b99c-f3cb7b5c15d2.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-8b30f3c5-37e5-4cba-b99c-f3cb7b5c15d2.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-9013c592-1576-4fa4-b2ea-9953bf8a21b0.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-9013c592-1576-4fa4-b2ea-9953bf8a21b0.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-911ba504-d74b-4cd6-83f6-33937e03cd46.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-911ba504-d74b-4cd6-83f6-33937e03cd46.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-9a99d504-9270-450f-b64f-06daed70fd5a.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-9a99d504-9270-450f-b64f-06daed70fd5a.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-9ba71ad7-eb4f-4a56-b8da-e3f395f590ea.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-9ba71ad7-eb4f-4a56-b8da-e3f395f590ea.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-a6cb36e6-77ff-4a37-ba83-0d828b254183.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-a6cb36e6-77ff-4a37-ba83-0d828b254183.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-ac4833b7-5aa5-4205-8d4a-c8c9575a2bc0.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-ac4833b7-5aa5-4205-8d4a-c8c9575a2bc0.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-b60f1759-6405-4457-9910-6da1ccd5f40f.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-b60f1759-6405-4457-9910-6da1ccd5f40f.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-bf8f778a-793e-4e66-bf19-f10ede501ea3.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-bf8f778a-793e-4e66-bf19-f10ede501ea3.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-c1a1aea3-25d5-4608-93e5-7616a7d25387.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-c1a1aea3-25d5-4608-93e5-7616a7d25387.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-d2cbc2b4-36a6-4854-85f9-ed0aa0b46711.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-d2cbc2b4-36a6-4854-85f9-ed0aa0b46711.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-dba9e81b-d32c-4541-a99a-c81cb8fb142b.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-dba9e81b-d32c-4541-a99a-c81cb8fb142b.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-de7dfe91-1122-4a64-a757-20d7817251a4.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-de7dfe91-1122-4a64-a757-20d7817251a4.m4a -------------------------------------------------------------------------------- /test/media/audio/trim-f27f9c60-ed1c-436c-9382-72a26abf644d.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/trim-f27f9c60-ed1c-436c-9382-72a26abf644d.m4a -------------------------------------------------------------------------------- /test/media/audio/whiteNoise50s1.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/whiteNoise50s1.m4a -------------------------------------------------------------------------------- /test/media/audio/whiteNoise50s2.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/whiteNoise50s2.m4a -------------------------------------------------------------------------------- /test/media/audio/whiteNoise50s3.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/whiteNoise50s3.m4a -------------------------------------------------------------------------------- /test/media/audio/whiteNoise50s4.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/whiteNoise50s4.m4a -------------------------------------------------------------------------------- /test/media/audio/whiteNoise50s5.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/whiteNoise50s5.m4a -------------------------------------------------------------------------------- /test/media/audio/whiteNoise50s6.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/whiteNoise50s6.m4a -------------------------------------------------------------------------------- /test/media/audio/whiteNoise5s1.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/whiteNoise5s1.m4a -------------------------------------------------------------------------------- /test/media/audio/whiteNoise5s2.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/whiteNoise5s2.m4a -------------------------------------------------------------------------------- /test/media/audio/whiteNoise5s3.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/whiteNoise5s3.m4a -------------------------------------------------------------------------------- /test/media/audio/whiteNoise5s4.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/whiteNoise5s4.m4a -------------------------------------------------------------------------------- /test/media/audio/whiteNoise5s5.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/whiteNoise5s5.m4a -------------------------------------------------------------------------------- /test/media/audio/whiteNoise5s6.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/audio/whiteNoise5s6.m4a -------------------------------------------------------------------------------- /test/media/image/album_cover_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/image/album_cover_01.png -------------------------------------------------------------------------------- /test/media/image/avatar_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/image/avatar_01.png -------------------------------------------------------------------------------- /test/media/image/banner_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/api/f1fc5ee2358594cd6b2e92825e832be0aaa0c171/test/media/image/banner_01.png -------------------------------------------------------------------------------- /test/testConfig.js: -------------------------------------------------------------------------------- 1 | 2 | const { apiRoot } = require('../src/constants') 3 | const expect = require('chai').expect 4 | 5 | // We're referencing the URL _inside_ of docker. 6 | const baseURL = `http://api:4000${apiRoot}` 7 | 8 | const request = require('supertest')(baseURL) 9 | 10 | const TestRedisAdapter = require('../src/auth/redis-adapter') 11 | 12 | // generic user 13 | const testUserId = '251c01f6-7293-45f6-b8cd-242bdd76cd0d' 14 | // artist user from table 'users' 15 | const testArtistUserId = '1c88dea6-0519-4b61-a279-4006954c5d4c' 16 | // admin user from table 'users' 17 | const testAdminUserId = '71175a23-9256-41c9-b8c1-cd2170aa6591' 18 | // listerner user from table 'users' 19 | const testListenerUserId = '251c01f6-7293-45f6-b8cd-242bdd76cd0d' 20 | const testTrackGroupId = '84322e4f-0247-427f-8bed-e7617c3df5ad' 21 | const testTagId = 'asdf' 22 | const testArtistId = '49d2ac44-7f20-4a47-9cf5-3ea5d6ef78f6' 23 | const testTrackId = 'b6d160d1-be16-48a4-8c4f-0c0574c4c6aa' 24 | 25 | const testAccessToken = 'test-!@#$-test-%^&*' 26 | const testInvalidAccessToken = 'invalid-invalid-invalid-invalid' 27 | 28 | module.exports = { 29 | baseURL, 30 | request, 31 | expect, 32 | testUserId, 33 | testAdminUserId, 34 | testArtistUserId, 35 | testListenerUserId, 36 | testTrackGroupId, 37 | testTagId, 38 | testArtistId, 39 | testTrackId, 40 | testAccessToken, 41 | TestRedisAdapter, 42 | testInvalidAccessToken 43 | } 44 | --------------------------------------------------------------------------------