├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── Procfile ├── README.md ├── backend ├── .env.sample ├── .graphqlconfig.yml ├── .sequelizerc ├── bin │ ├── console │ └── console.ts ├── config │ └── database.js ├── db │ ├── dev.sql │ └── migrations │ │ ├── 20200125062114-InitDatabase.js │ │ ├── 20200125070937-AddLoginShares.js │ │ ├── 20200203092851-AddAvatar.js │ │ ├── 20200204105026-ChangeToUsername.js │ │ ├── 20200205015220-AddNoAvatarDefault.js │ │ ├── 20200205021340-AddInvites.js │ │ ├── 20200208013511-AddWaitlist.js │ │ ├── 20200208220544-AddUnwrapKey.js │ │ ├── 20200209012216-AddDismissOnboardingFlag.js │ │ ├── 20200209030003-AddEmailVerifications.js │ │ ├── 20200212004351-AlterPhoneDomain.js │ │ ├── 20200214231801-LimitUsernameLength.js │ │ ├── 20200215050035-SimplifySharingCrypto.js │ │ ├── 20200215203236-AddLoginPreviews.js │ │ ├── 20200219215217-UniqueFriendRequests.js │ │ ├── 20200220035158-RemoveVaultMetadata.js │ │ ├── 20200220052923-InviteWithoutAutofriend.js │ │ ├── 20200225043057-AddPbkdf2SaltToUser.js │ │ ├── 20200225052320-NewLoginsDataModel.js │ │ ├── 20200302013737-AddMasterKeySalt.js │ │ ├── 20200305222217-AddKeyAndNicknameToInvites.js │ │ ├── 20200306003039-AddInviteLoginAccess.js │ │ ├── 20200312205449-AddBetaOnboardingFlag.js │ │ ├── 20200312210436-AddEmailToInvites.js │ │ ├── 20200314003000-AddOnDeleteCascadeToHandshakes.js │ │ ├── 20200314003633-AddBetaOnboardFieldsToWaitlist.js │ │ ├── 20200314030523-AddMoreOnDeleteCascade.js │ │ ├── 20200314203239-AddEmailSentAtToInvites.js │ │ ├── 20200319003259-AddLastUpdatedAtToUser.js │ │ ├── 20200401193012-AlterNonemptyString.js │ │ ├── 20200406210451-AddToWaitlistEntries.js │ │ ├── 20200410213416-AddUnsubscribedAtToWaitlist.js │ │ ├── 20200424005357-AddSharePreviewsToLoginData.js │ │ ├── 20200520003241-AddSchemaAndTypeToLoginTable.js │ │ └── 20201006183850-AddInternalPasswordResetFlagToUser.js ├── emails │ ├── beta_invite.mjml │ ├── friend_invite.mjml │ ├── friend_request.mjml │ ├── friend_request_accepted.mjml │ ├── invite_accepted.mjml │ ├── login_access_requested.mjml │ ├── login_request_approved.mjml │ ├── new_login_previews.mjml │ ├── share_login.mjml │ ├── verify_email.mjml │ └── verify_waitlist_email.mjml ├── graphql-codegen.yml ├── jest.config.js ├── package.json ├── public │ └── landing │ │ ├── css │ │ ├── main.css │ │ └── main.css.map │ │ ├── favicons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-384x384.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-150x150.png │ │ ├── safari-pinned-tab.svg │ │ └── site.webmanifest │ │ ├── img │ │ ├── 2x │ │ │ ├── confetti-1.png │ │ │ ├── confetti-2.png │ │ │ ├── confetti-3.png │ │ │ ├── confetti-4.png │ │ │ ├── confetti-5.png │ │ │ ├── confetti-6.png │ │ │ ├── confetti-7.png │ │ │ ├── distressed-dots-bg.png │ │ │ ├── gradient-bright.jpg │ │ │ ├── gradient.jpg │ │ │ ├── hypno-eye-bg.png │ │ │ ├── large-button-squiggles.png │ │ │ ├── scroll-arrow.png │ │ │ └── sticky-header-blob.png │ │ ├── a-animation │ │ │ ├── 2x │ │ │ │ └── static.png │ │ │ └── static.png │ │ ├── confetti-1.png │ │ ├── confetti-2.png │ │ ├── confetti-3.png │ │ ├── confetti-4.png │ │ ├── confetti-5.png │ │ ├── confetti-6.png │ │ ├── confetti-7.png │ │ ├── dashboard-animation │ │ │ ├── 2x │ │ │ │ ├── dude-mask.png │ │ │ │ ├── eyes-normal.png │ │ │ │ ├── girl-mask.png │ │ │ │ ├── guide.png │ │ │ │ ├── pop-box-1.png │ │ │ │ ├── pop-box-2.png │ │ │ │ ├── purple-note-1.png │ │ │ │ ├── purple-note-2.png │ │ │ │ ├── static.png │ │ │ │ ├── yellow-note-1.png │ │ │ │ └── yellow-note-2.png │ │ │ ├── dude-mask.png │ │ │ ├── eyes-normal.png │ │ │ ├── girl-mask.png │ │ │ ├── guide.png │ │ │ ├── pop-box-1.png │ │ │ ├── pop-box-2.png │ │ │ ├── purple-note-1.png │ │ │ ├── purple-note-2.png │ │ │ ├── static.png │ │ │ ├── yellow-note-1.png │ │ │ └── yellow-note-2.png │ │ ├── distressed-dots-bg.png │ │ ├── footer-animation │ │ │ ├── 2x │ │ │ │ ├── arrow.png │ │ │ │ └── squiggles.png │ │ │ ├── arrow.png │ │ │ ├── guide@2x.png │ │ │ └── squiggles.png │ │ ├── gradient-bright.jpg │ │ ├── gradient.jpg │ │ ├── hero-animation │ │ │ ├── 2x │ │ │ │ ├── distressed-dots.png │ │ │ │ ├── dude-eyes-normal.png │ │ │ │ ├── dude-mask.png │ │ │ │ ├── girl-eyes-normal.png │ │ │ │ ├── guide.png │ │ │ │ ├── heart-1.png │ │ │ │ ├── heart-2.png │ │ │ │ ├── music-note-1.png │ │ │ │ ├── music-note-2.png │ │ │ │ ├── music-note-3.png │ │ │ │ ├── music-note-4.png │ │ │ │ ├── pop-box-1.png │ │ │ │ ├── pop-box-2.png │ │ │ │ ├── radiant-circles.png │ │ │ │ ├── squiggles.png │ │ │ │ ├── static.png │ │ │ │ └── video.png │ │ │ ├── distressed-dots.png │ │ │ ├── dude-eyes-normal.png │ │ │ ├── dude-mask.png │ │ │ ├── girl-eyes-normal.png │ │ │ ├── guide.png │ │ │ ├── heart-1.png │ │ │ ├── heart-2.png │ │ │ ├── music-note-1.png │ │ │ ├── music-note-2.png │ │ │ ├── music-note-3.png │ │ │ ├── music-note-4.png │ │ │ ├── pop-box-1.png │ │ │ ├── pop-box-2.png │ │ │ ├── radiant-circles.png │ │ │ ├── squiggles.png │ │ │ ├── static.png │ │ │ ├── video-player-2.png │ │ │ └── video.png │ │ ├── hypno-eye-bg.png │ │ ├── j-animation │ │ │ ├── 2x │ │ │ │ └── static.png │ │ │ └── static.png │ │ ├── jam-logo.svg │ │ ├── jam.svg │ │ ├── large-button-squiggles.png │ │ ├── large-button-top.svg │ │ ├── og-image.jpg │ │ ├── scroll-arrow.png │ │ ├── share-animation │ │ │ ├── 2x │ │ │ │ ├── address-bar.png │ │ │ │ ├── dude-finger.png │ │ │ │ ├── guide.png │ │ │ │ ├── pop-box-1.png │ │ │ │ ├── pop-box-2.png │ │ │ │ ├── share-animation.png │ │ │ │ └── static.png │ │ │ ├── address-bar.png │ │ │ ├── dude-finger.png │ │ │ ├── pop-box-1.png │ │ │ ├── pop-box-2.png │ │ │ └── static.png │ │ ├── split-cost-animation-mobile │ │ │ ├── 2x │ │ │ │ ├── dude-mask.png │ │ │ │ ├── guide.png │ │ │ │ └── static.png │ │ │ ├── dude-mask.png │ │ │ ├── guide.png │ │ │ ├── split-cost-animation-mobile.png │ │ │ └── static.png │ │ ├── split-cost-animation │ │ │ ├── 2x │ │ │ │ ├── dude-eyes-normal.png │ │ │ │ ├── girl-1-eyes-normal.png │ │ │ │ ├── girl-1-mask.png │ │ │ │ ├── girl-2-eyes-normal.png │ │ │ │ ├── girl-2-mask.png │ │ │ │ ├── guide.png │ │ │ │ ├── light-blue.png │ │ │ │ ├── light-yellow.png │ │ │ │ ├── machine-person-1.png │ │ │ │ ├── machine-person-2.png │ │ │ │ ├── machine-person-3.png │ │ │ │ └── static.png │ │ │ ├── dude-eyes-normal.png │ │ │ ├── girl-1-eyes-normal.png │ │ │ ├── girl-1-mask.png │ │ │ ├── girl-2-eyes-normal.png │ │ │ ├── girl-2-mask.png │ │ │ ├── guide.png │ │ │ ├── light-blue.png │ │ │ ├── light-yellow.png │ │ │ ├── machine-person-1.png │ │ │ ├── machine-person-2.png │ │ │ ├── machine-person-3.png │ │ │ └── static.png │ │ └── sticky-header-blob.png │ │ ├── index.html │ │ └── js │ │ └── main.js ├── server │ ├── __snapshots__ │ │ └── emails.test.ts.snap │ ├── addToBugsnag.ts │ ├── authentication.ts │ ├── db │ │ └── loginMembers.ts │ ├── emails.test.ts │ ├── emails.tsx │ ├── env.ts │ ├── error.ts │ ├── index.ts │ ├── logGraphqlError.ts │ ├── models │ │ ├── EmailVerification.ts │ │ ├── Friend.ts │ │ ├── FriendRequest.ts │ │ ├── Invite.ts │ │ ├── InviteLoginAccess.ts │ │ ├── Login.ts │ │ ├── LoginAccess.ts │ │ ├── LoginData.ts │ │ ├── SRPHandshake.ts │ │ ├── User.ts │ │ ├── WaitlistEntry.ts │ │ ├── index.ts │ │ └── types.ts │ ├── pulse.ts │ ├── queries │ │ └── loginPreviewTodo.ts │ ├── queue.ts │ ├── resolvers.ts │ ├── resolvers │ │ ├── Friend.ts │ │ ├── Invite.ts │ │ ├── MyLogin.ts │ │ ├── PendingOutboundFriendRequest.ts │ │ └── SharedLogin.ts │ ├── sendEmail.ts │ ├── server.graphql │ ├── server.ts │ ├── utils.ts │ └── worker.ts ├── tsconfig.json └── tsconfig.test.json ├── bin ├── build-prod-extension ├── compile-quicktype ├── console ├── dev ├── dev-server ├── dev-static ├── dev-worker ├── dump-dev-sql ├── graphql-codegen ├── heroku-postbuild ├── integration-test ├── migrate ├── prettier ├── react-storybook ├── reload-dev-sql ├── shared │ └── ts-node-dev-instance.sh └── strict-mode.sh ├── extension ├── .gitignore ├── README.md ├── bin │ └── build-prod-extension ├── client ├── package.json ├── src │ ├── background.html │ ├── img │ │ ├── icon-128-dev.png │ │ ├── icon-128.png │ │ ├── icon-16-dev.png │ │ ├── icon-16.png │ │ ├── icon-34-dev.png │ │ ├── icon-34.png │ │ ├── icon-48-dev.png │ │ ├── icon-48.png │ │ ├── logo-192.png │ │ └── logo-512.png │ ├── js │ │ ├── background.ts │ │ └── popup.tsx │ ├── manifest.json │ └── popup.html ├── tsconfig.json ├── typings │ ├── index.d.ts │ └── rebass__forms ├── utils │ ├── build.js │ ├── env.js │ └── webserver.js ├── webpack.config.js └── yarn.lock ├── package.json ├── react-app ├── .env.sample ├── .storybook │ ├── main.js │ ├── manager.js │ ├── preview-body.html │ ├── preview-head.html │ ├── preview.js │ └── theme.js ├── __tests__ │ └── server.test.ts ├── graphql-codegen.yml ├── jest.config.js ├── package.json ├── public │ ├── account-share-email-icon.png │ ├── email-header.png │ ├── favicon.ico │ ├── favicon.png │ ├── img │ │ ├── 2x │ │ │ └── no-avatar.png │ │ └── no-avatar.png │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── respond-to-friend-request-button.png │ ├── robots.txt │ └── social-card.png ├── src │ ├── AcceptInvite.tsx │ ├── AddFriend.tsx │ ├── App.tsx │ ├── AppContainer.tsx │ ├── AppHistory.tsx │ ├── AuthRequired.tsx │ ├── Avatar.tsx │ ├── BrowserEnv.tsx │ ├── CreateAccount.tsx │ ├── CurrentUser.ts │ ├── FAQ │ │ ├── faq.json │ │ └── index.tsx │ ├── FindFriends.tsx │ ├── Flash.tsx │ ├── Home.tsx │ ├── LandingPage.tsx │ ├── LoginsViewer.tsx │ ├── NewLogin.tsx │ ├── PrivacyPolicy.tsx │ ├── Signin.tsx │ ├── TermsOfService.tsx │ ├── UI.tsx │ ├── VerifyEmail.tsx │ ├── ViewLogin.tsx │ ├── ViewLoginPreview.tsx │ ├── api.graphql │ ├── api.ts │ ├── api │ │ └── graphql.ts │ ├── assertNever.ts │ ├── browserSession.ts │ ├── bugsnagUser.ts │ ├── clientEnv.ts │ ├── colors.ts │ ├── components │ │ ├── AccountDropdown.tsx │ │ ├── ActionBtns.tsx │ │ ├── ActionCard.tsx │ │ ├── AddFriendRow.tsx │ │ ├── AvatarUploadInput.tsx │ │ ├── BetaOnboarding.tsx │ │ ├── BoldedListSentence.tsx │ │ ├── Button.tsx │ │ ├── Card.tsx │ │ ├── ChangePassword.tsx │ │ ├── ConfirmDelete.tsx │ │ ├── CropImg.tsx │ │ ├── CroppableImageUpload.tsx │ │ ├── EditLogin.tsx │ │ ├── EditProfile.tsx │ │ ├── Emoji.tsx │ │ ├── EmojiList.tsx │ │ ├── ExtensionRequired.tsx │ │ ├── FloatingIcon.tsx │ │ ├── FormError.tsx │ │ ├── Icon.tsx │ │ ├── ImageUpload.tsx │ │ ├── Input.tsx │ │ ├── InviteModal.tsx │ │ ├── InviteVia.tsx │ │ ├── LegalCheckbox.tsx │ │ ├── LegalDoc.tsx │ │ ├── Loading.tsx │ │ ├── LoadingAnim.tsx │ │ ├── LocalStorageCheck.tsx │ │ ├── LoginBar.tsx │ │ ├── LoginDescription.tsx │ │ ├── LoginDetail.tsx │ │ ├── LoginIcon.tsx │ │ ├── LoginMember.tsx │ │ ├── MagicLinkCard.tsx │ │ ├── MagicLinkGuide.tsx │ │ ├── Modal.tsx │ │ ├── NoImagePlaceholder.tsx │ │ ├── NotFound.tsx │ │ ├── Notification.tsx │ │ ├── Notifications.tsx │ │ ├── OnboardingCard.tsx │ │ ├── Page.tsx │ │ ├── SearchBar.tsx │ │ ├── SelectNewLoginType.tsx │ │ ├── SelectUsers.tsx │ │ ├── SimpleMessageCard.tsx │ │ ├── SubmitBtn.tsx │ │ ├── Switch.tsx │ │ ├── ValidatedForm.tsx │ │ ├── ViewFriendship.tsx │ │ ├── ViewNotifications.tsx │ │ ├── YourFriends.tsx │ │ └── glow.tsx │ ├── crypto.ts │ ├── friendRequests.ts │ ├── generics.ts │ ├── graphqlApi.ts │ ├── icons.ts │ ├── img │ │ ├── addFriendsCardBg.png │ │ ├── addFriendsCardVectors.png │ │ ├── addLoginCardBg.png │ │ ├── addLoginCardVector.png │ │ ├── approved-white.svg │ │ ├── approved.svg │ │ ├── back-arrow.svg │ │ ├── back-button.svg │ │ ├── bell.svg │ │ ├── broken-heart.svg │ │ ├── camera-icon-hover.svg │ │ ├── camera-icon.svg │ │ ├── checkmark.svg │ │ ├── chrome-icon.png │ │ ├── close-button-white.svg │ │ ├── close-button.svg │ │ ├── copy-done-button.svg │ │ ├── copy-icon.svg │ │ ├── copy-link-button.svg │ │ ├── create-arrow.svg │ │ ├── domain-icon.png │ │ ├── edit-pencil.svg │ │ ├── email-icon.svg │ │ ├── external-link-white.svg │ │ ├── griflan │ │ ├── hamburger.svg │ │ ├── heart-icon.png │ │ ├── home-icon.svg │ │ ├── invalid.svg │ │ ├── invite-friend.svg │ │ ├── invite-present-icon.svg │ │ ├── invite-via-email.svg │ │ ├── invite-via-link.svg │ │ ├── invite-via-sms.svg │ │ ├── login-complete-checkmark.svg │ │ ├── magic-link-with-bg.svg │ │ ├── magic-link.svg │ │ ├── magnifyingGlass.svg │ │ ├── new-account.png │ │ ├── next-icon.svg │ │ ├── password-icon.png │ │ ├── save-icon.svg │ │ ├── secure-credentials-icon-with-bg.svg │ │ ├── selfie-icon.svg │ │ ├── send-arrow.png │ │ ├── spinner-black.gif │ │ ├── spinner.gif │ │ ├── trash-can.svg │ │ └── username-icon.png │ ├── index.css │ ├── index.tsx │ ├── loginSchemaMapper.ts │ ├── logo.svg │ ├── pages │ │ ├── BetaInvitePage.tsx │ │ ├── ChangePasswordPage.tsx │ │ ├── ConfirmWaitlistPage.tsx │ │ ├── EditProfilePage.tsx │ │ ├── NewMagicLinkPage.tsx │ │ └── UnsubscribeWaitlistPage.tsx │ ├── react-app-env.d.ts │ ├── routing.tsx │ ├── serviceWorker.ts │ ├── setupTests.ts │ ├── srp.ts │ ├── sync.ts │ ├── theme.ts │ ├── types │ │ ├── index.ts │ │ ├── schema.ts │ │ └── serializer.ts │ ├── useKeyPress.ts │ └── utils.ts ├── tsconfig.json └── typings │ └── rebass__forms │ └── index.d.ts ├── typings └── mailgun-js │ └── index.d.ts └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | backend/server/resolvers.ts linguist-generated=true 2 | react-app/src/api/graphql.ts linguist-generated=true 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: "12.x" 14 | 15 | - name: Install dependencies 16 | run: yarn 17 | 18 | - name: Install client dependencies 19 | run: cd react-app && yarn 20 | 21 | - name: Assert prettier is in sync 22 | run: bin/prettier && git diff --exit-code 23 | 24 | - name: Typecheck server 25 | run: cd backend && ./node_modules/.bin/tsc --version && ./node_modules/.bin/tsc --project tsconfig.json --noEmit 26 | 27 | - name: Typecheck client 28 | run: | 29 | cd react-app 30 | ./node_modules/.bin/tsc --version && ./node_modules/.bin/tsc --project tsconfig.json --noEmit 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env 26 | 27 | react-app/node_modules 28 | react-app/build 29 | 30 | backend/node_modules 31 | 32 | tmp/ 33 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # The landing page assets are minified 2 | backend/public/landing 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: yarn start 2 | release: echo noop 3 | worker: yarn worker 4 | -------------------------------------------------------------------------------- /backend/.env.sample: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | AWS_REGION=us-east-2 3 | AWS_ACCESS_KEY=XXX 4 | AWS_SECRET_KEY=XXX 5 | S3_AVATARS_BUCKET=dev--avatars 6 | MAILGUN_DOMAIN=sandbox11111111111111111111111111111111.mailgun.org 7 | MAILGUN_API_KEY=cafecafecafecafecafecafecafecafe-cafecafe-cafecafe 8 | APP_DOMAIN=http://localhost:3000/ 9 | BUGSNAG_KEY=cafecafecafecafecafecafecafecafe 10 | REDIS_URL=redis://127.0.0.1:6379 11 | REACT_APP_INVITE_ONLY=false 12 | REACT_APP_BUGSNAG_KEY=cafecafecafecafecafecafecafecafe 13 | REACT_APP_HEAP_ANALYTICS_ID=1111111111 14 | ADMIN_PATH=admin 15 | ADMIN_USERNAME=john 16 | ADMIN_PASSWORD=letmein 17 | SLACK_PULSE_WEBHOOK_URL=https://hooks.slack.com/services/XXXXXXXXX/yyyyyyyyyyy/zzzzzzzzzzzzzzzzzzzzzzzz 18 | REACT_APP_STAGING=false 19 | APP_MODE=web 20 | BETA_INVITES_EMAIL_CRON="*/15 16-23,0-1 * * *" 21 | BETA_INVITES_BATCH_SIZE=1 22 | EXTEND_ESLINT=true 23 | COOKIE_SECRET=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef 24 | -------------------------------------------------------------------------------- /backend/.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | frontend: 3 | schemaPath: react-app/src/api.graphql 4 | includes: ["**/*.graphql"] 5 | extensions: 6 | endpoints: 7 | default: http://localhost:5000 8 | db: 9 | schemaPath: server/server.graphql 10 | includes: ["**/*.graphql"] 11 | -------------------------------------------------------------------------------- /backend/.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | config: path.resolve('config', 'database.js'), 5 | 'models-path': path.resolve('server', 'models'), 6 | 'migrations-path': 'db/migrations', 7 | } 8 | -------------------------------------------------------------------------------- /backend/bin/console: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function ts-node-dev() { 4 | # --rs false configures ts-node-dev to not bind to stdin 5 | # see: https://www.karltarvas.com/2020/05/31/ts-node-dev-binds-to-stdin-via-readline-by-default.html 6 | NODE_OPTIONS=--experimental-repl-await ./node_modules/.bin/ts-node-dev --rs false "$@" 7 | } 8 | 9 | ts-node-dev \ 10 | -P tsconfig.json \ 11 | -O '{"module":"commonjs", "target":"es6"}' \ 12 | -r tsconfig-paths/register ./bin/console.ts 13 | -------------------------------------------------------------------------------- /backend/bin/console.ts: -------------------------------------------------------------------------------- 1 | // Support TS requires using our server tsconfig. Otherwise, models don't work 2 | import * as models from "./../server/models"; 3 | import * as queues from "./../server/queue"; 4 | 5 | const repl = require("repl"); 6 | 7 | /** 8 | * Inject models into REPL context 9 | */ 10 | 11 | // Add values to this object if you want them to be available in the console 12 | const customReplContext: { [k: string]: any } = {}; 13 | 14 | // Merge all model exports in via Object.assign so we don't have to update this file when we add new models 15 | 16 | Object.assign(customReplContext, models); 17 | Object.assign(customReplContext, queues); 18 | 19 | // List of injected variables for the welcome message 20 | const injectedValuesList = Object.keys(customReplContext) 21 | .map((key) => ` - ${key}`) 22 | .join("\n"); 23 | 24 | console.log(`Values injected into REPL:\n`); 25 | console.log(injectedValuesList); 26 | console.log(); 27 | 28 | /** 29 | * Initialize REPLs 30 | */ 31 | 32 | const replSession = repl.start({ 33 | prompt: `jam (${process.env.NODE_ENV})> `, 34 | }); 35 | 36 | Object.keys(customReplContext).forEach((key: string) => { 37 | // replSession.context already has things in it like `console` so you can do `console.log` 38 | // As a precaution, we should throw an error if we accidentally clobber 39 | if (key in replSession.context) { 40 | throw new Error( 41 | `${key} is already set in replSession.context! You'll need to give it another name` 42 | ); 43 | } 44 | 45 | replSession.context[key] = (customReplContext as any)[key]; 46 | }); 47 | -------------------------------------------------------------------------------- /backend/config/database.js: -------------------------------------------------------------------------------- 1 | require("babel-register"); 2 | 3 | module.exports = { 4 | development: { 5 | username: null, 6 | password: null, 7 | database: "jam-dev", 8 | host: "localhost", 9 | dialect: "postgres", 10 | // Make sure tables and columns are underscored in DB and not camel 11 | // @see https://git.io/Jvvj6 12 | // Also, for models @see https://sequelize.org/master/manual/models-definition.html#configuration 13 | define: { 14 | underscored: true, 15 | underscoredAll: true, 16 | createdAt: "created_at", 17 | updatedAt: "updated_at", 18 | }, 19 | }, 20 | test: { 21 | username: null, 22 | password: null, 23 | database: "jam-test", 24 | host: "localhost", 25 | dialect: "postgres", 26 | // Make sure tables and columns are underscored in DB and not camel 27 | // @see https://git.io/Jvvj6 28 | // Also, for models @see https://sequelize.org/master/manual/models-definition.html#configuration 29 | define: { 30 | underscored: true, 31 | underscoredAll: true, 32 | createdAt: "created_at", 33 | updatedAt: "updated_at", 34 | }, 35 | }, 36 | production: { 37 | use_env_variable: "DATABASE_URL", 38 | dialect: "postgres", 39 | ssl: { 40 | require: true, 41 | rejectUnauthorized: false, 42 | }, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /backend/db/migrations/20200125070937-AddLoginShares.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | CREATE TABLE login_shares ( 9 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 10 | created_at timestamp without time zone DEFAULT now() NOT NULL, 11 | updated_at timestamp without time zone DEFAULT now() NOT NULL, 12 | login_id uuid NOT NULL REFERENCES logins(id), 13 | from_id uuid NOT NULL REFERENCES users(id), 14 | to_id uuid NOT NULL REFERENCES users(id), 15 | preview t_aes_encrypted_blob NOT NULL, 16 | credentials t_aes_encrypted_blob NOT NULL 17 | ); 18 | 19 | ALTER TABLE logins ADD COLUMN parent_id uuid REFERENCES logins(id); 20 | 21 | COMMIT; 22 | `); 23 | }, 24 | 25 | down: async (queryInterface, _) => { 26 | await queryInterface.sequelize.query(` 27 | BEGIN TRANSACTION; 28 | 29 | ALTER TABLE logins DROP COLUMN parent_id; 30 | 31 | DROP TABLE login_shares; 32 | 33 | COMMIT; 34 | `); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /backend/db/migrations/20200203092851-AddAvatar.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE users ADD COLUMN avatar_url VARCHAR; 9 | 10 | COMMIT; 11 | `); 12 | }, 13 | 14 | down: async (queryInterface, _) => { 15 | await queryInterface.sequelize.query(` 16 | BEGIN TRANSACTION; 17 | 18 | ALTER TABLE users DROP COLUMN avatar_url; 19 | 20 | COMMIT; 21 | `); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /backend/db/migrations/20200204105026-ChangeToUsername.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | CREATE DOMAIN t_username citext CHECK (VALUE::text ~ '\\A[a-zA-Z0-9_]+\\Z'); 9 | 10 | ALTER TABLE users ADD COLUMN username t_username; 11 | UPDATE users SET username = display_name; 12 | ALTER TABLE users ALTER COLUMN username SET NOT NULL; 13 | ALTER TABLE users ADD CONSTRAINT unique_username UNIQUE (username); 14 | ALTER TABLE users DROP COLUMN display_name; 15 | 16 | DROP DOMAIN t_user_display_name; 17 | 18 | COMMIT; 19 | `); 20 | }, 21 | 22 | down: async (queryInterface, _) => { 23 | await queryInterface.sequelize.query(` 24 | BEGIN TRANSACTION; 25 | 26 | CREATE DOMAIN t_user_display_name AS character varying(50) 27 | CONSTRAINT t_user_display_name_check CHECK ((((VALUE)::text ~ '\\A[a-zA-Z0-9 _]+\\Z'::text) AND ((VALUE)::text ~ '\\A[a-zA-Z0-9]'::text) AND ((VALUE)::text ~ '[a-zA-Z0-9]\\Z'::text))); 28 | 29 | TRUNCATE TABLE users RESTART IDENTITY CASCADE; 30 | 31 | ALTER TABLE users DROP COLUMN username; 32 | ALTER TABLE users ADD COLUMN display_name t_username NOT NULL UNIQUE; 33 | 34 | DROP DOMAIN t_username; 35 | 36 | COMMIT; 37 | `); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /backend/db/migrations/20200205015220-AddNoAvatarDefault.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE users ALTER COLUMN avatar_url SET DEFAULT '/img/no-avatar.png'; 9 | ALTER TABLE users ALTER COLUMN avatar_url SET NOT NULL; 10 | 11 | COMMIT; 12 | `); 13 | }, 14 | 15 | down: async (queryInterface, _) => { 16 | await queryInterface.sequelize.query(` 17 | BEGIN TRANSACTION; 18 | 19 | ALTER TABLE users ALTER COLUMN avatar_url DROP DEFAULT; 20 | ALTER TABLE users ALTER COLUMN avatar_url DROP NOT NULL; 21 | 22 | COMMIT; 23 | `); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /backend/db/migrations/20200208013511-AddWaitlist.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | CREATE TABLE waitlist_entries ( 9 | email email_address PRIMARY KEY, 10 | created_at timestamp without time zone DEFAULT now() NOT NULL, 11 | updated_at timestamp without time zone DEFAULT now() NOT NULL 12 | ); 13 | 14 | COMMIT; 15 | `); 16 | }, 17 | 18 | down: async (queryInterface, _) => { 19 | await queryInterface.sequelize.query(` 20 | BEGIN TRANSACTION; 21 | 22 | DROP TABLE waitlist_entries; 23 | 24 | COMMIT; 25 | `); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /backend/db/migrations/20200208220544-AddUnwrapKey.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | DELETE FROM srp_handshakes; 9 | ALTER TABLE srp_handshakes ADD COLUMN session_wrapper varchar NOT NULL; 10 | 11 | COMMIT; 12 | `); 13 | }, 14 | 15 | down: async (queryInterface, _) => { 16 | await queryInterface.sequelize.query(` 17 | BEGIN TRANSACTION; 18 | 19 | ALTER TABLE srp_handshakes DROP COLUMN session_wrapper; 20 | 21 | COMMIT; 22 | `); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /backend/db/migrations/20200209012216-AddDismissOnboardingFlag.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE users ADD COLUMN show_onboarding_card boolean DEFAULT true NOT NULL; 9 | 10 | COMMIT; 11 | `); 12 | }, 13 | 14 | down: async (queryInterface, _) => { 15 | await queryInterface.sequelize.query(` 16 | BEGIN TRANSACTION; 17 | 18 | ALTER TABLE users DROP COLUMN show_onboarding_card; 19 | 20 | COMMIT; 21 | `); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /backend/db/migrations/20200209030003-AddEmailVerifications.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | CREATE TABLE email_verifications ( 9 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 10 | created_at timestamp without time zone DEFAULT now() NOT NULL, 11 | updated_at timestamp without time zone DEFAULT now() NOT NULL, 12 | user_id uuid NOT NULL REFERENCES users(id) 13 | ); 14 | 15 | ALTER TABLE users ADD COLUMN email_verified boolean DEFAULT false NOT NULL; 16 | 17 | COMMIT; 18 | `); 19 | }, 20 | 21 | down: async (queryInterface, _) => { 22 | await queryInterface.sequelize.query(` 23 | BEGIN TRANSACTION; 24 | 25 | DROP TABLE email_verifications; 26 | ALTER TABLE users DROP COLUMN email_verified; 27 | 28 | COMMIT; 29 | `); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /backend/db/migrations/20200212004351-AlterPhoneDomain.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER DOMAIN t_international_e164_number DROP NOT NULL; 9 | 10 | ALTER TABLE invites ALTER COLUMN phone DROP NOT NULL; 11 | 12 | COMMIT; 13 | `); 14 | }, 15 | 16 | down: async (queryInterface, _) => { 17 | await queryInterface.sequelize.query(` 18 | BEGIN TRANSACTION; 19 | 20 | ALTER DOMAIN t_international_e164_number SET NOT NULL; 21 | 22 | ALTER TABLE invites ALTER COLUMN phone SET NOT NULL; 23 | 24 | COMMIT; 25 | `); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /backend/db/migrations/20200214231801-LimitUsernameLength.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER DOMAIN t_username ADD CONSTRAINT username_length CHECK (char_length(VALUE) <= 15); 9 | 10 | COMMIT; 11 | `); 12 | }, 13 | 14 | down: async (queryInterface, _) => { 15 | await queryInterface.sequelize.query(` 16 | BEGIN TRANSACTION; 17 | 18 | ALTER DOMAIN t_international_e164_number SET NOT NULL; 19 | 20 | ALTER DOMAIN t_username DROP CONSTRAINT username_length; 21 | 22 | COMMIT; 23 | `); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /backend/db/migrations/20200215050035-SimplifySharingCrypto.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | TRUNCATE TABLE login_shares; 9 | ALTER TABLE login_shares ADD COLUMN encrypted_data_key t_rsa_encrypted_blob NOT NULL; 10 | ALTER TABLE friend_requests DROP COLUMN initiator_broadcast_key; 11 | ALTER TABLE friend_requests DROP COLUMN recipient_broadcast_key; 12 | ALTER TABLE friends DROP COLUMN encrypted_broadcast_key; 13 | 14 | COMMIT; 15 | `); 16 | }, 17 | 18 | down: async (queryInterface, _) => { 19 | throw new Error("Not reversible!"); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /backend/db/migrations/20200215203236-AddLoginPreviews.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | CREATE TYPE t_login_preview_status AS ENUM ( 9 | 'preview', 10 | 'requested', 11 | 'accepted', 12 | 'rejected', 13 | 'transferring', 14 | 'transferred' 15 | ); 16 | 17 | CREATE TABLE login_previews ( 18 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 19 | created_at timestamp without time zone DEFAULT now() NOT NULL, 20 | updated_at timestamp without time zone DEFAULT now() NOT NULL, 21 | login_id uuid NOT NULL REFERENCES logins(id), 22 | from_id uuid NOT NULL REFERENCES users(id), 23 | to_id uuid NOT NULL REFERENCES users(id), 24 | preview t_aes_encrypted_blob NOT NULL, 25 | preview_key t_rsa_encrypted_blob NOT NULL, 26 | status t_login_preview_status NOT NULL DEFAULT 'preview', 27 | credentials t_aes_encrypted_blob, 28 | credentials_key t_rsa_encrypted_blob 29 | ); 30 | 31 | ALTER TABLE login_previews ADD CONSTRAINT transferring_credentials CHECK ( 32 | ((status = 'transferring') AND (credentials IS NOT NULL) AND (credentials_key IS NOT NULL)) 33 | OR ((status <> 'transferring') AND (credentials IS NULL) AND (credentials_key IS NULL)) 34 | ); 35 | 36 | COMMIT; 37 | `); 38 | }, 39 | 40 | down: async (queryInterface, _) => { 41 | await queryInterface.sequelize.query(` 42 | BEGIN TRANSACTION; 43 | 44 | DROP TABLE login_previews; 45 | DROP TYPE t_login_preview_status; 46 | 47 | COMMIT; 48 | `); 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /backend/db/migrations/20200219215217-UniqueFriendRequests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE friend_requests ADD CONSTRAINT unique_requests UNIQUE (initiator_id, recipient_id); 9 | 10 | COMMIT; 11 | `); 12 | }, 13 | 14 | down: async (queryInterface, _) => { 15 | await queryInterface.sequelize.query(` 16 | BEGIN TRANSACTION; 17 | 18 | ALTER TABLE friend_requests DROP CONSTRAINT unique_requests; 19 | 20 | COMMIT; 21 | `); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /backend/db/migrations/20200220035158-RemoveVaultMetadata.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE users DROP COLUMN vault_metadata; 9 | ALTER TABLE users DROP COLUMN vault_metadata_last_updated_at; 10 | 11 | COMMIT; 12 | `); 13 | }, 14 | 15 | down: async (queryInterface, _) => { 16 | await queryInterface.sequelize.query(` 17 | BEGIN TRANSACTION; 18 | 19 | ALTER TABLE users ADD COLUMN vault_metadata t_aes_encrypted_blob NOT NULL; 20 | ALTER TABLE users ADD COLUMN vault_metadata_last_updated_at timestamp without time zone DEFAULT now() NOT NULL; 21 | 22 | COMMIT; 23 | `); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /backend/db/migrations/20200220052923-InviteWithoutAutofriend.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE invites ADD COLUMN autofriend boolean DEFAULT true NOT NULL; 9 | 10 | COMMIT; 11 | `); 12 | }, 13 | 14 | down: async (queryInterface, _) => { 15 | await queryInterface.sequelize.query(` 16 | BEGIN TRANSACTION; 17 | 18 | ALTER TABLE invites DROP COLUMN autofriend; 19 | 20 | COMMIT; 21 | `); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /backend/db/migrations/20200225043057-AddPbkdf2SaltToUser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE users ADD COLUMN pbkdf2_salt t_srp_salt; 9 | 10 | COMMIT; 11 | `); 12 | }, 13 | 14 | down: async (queryInterface, _) => { 15 | await queryInterface.sequelize.query(` 16 | BEGIN TRANSACTION; 17 | 18 | ALTER TABLE users DROP COLUMN; 19 | 20 | COMMIT; 21 | `); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /backend/db/migrations/20200302013737-AddMasterKeySalt.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | TRUNCATE TABLE users CASCADE; 9 | ALTER TABLE users ALTER COLUMN pbkdf2_salt SET NOT NULL; 10 | ALTER TABLE users RENAME COLUMN pbkdf2_salt TO srp_pbkdf2_salt; 11 | ALTER TABLE users ADD COLUMN master_key_pbkdf2_salt t_srp_salt NOT NULL; 12 | 13 | COMMIT;`); 14 | }, 15 | 16 | down: async (queryInterface, _) => { 17 | throw new Error("No going back lol"); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /backend/db/migrations/20200305222217-AddKeyAndNicknameToInvites.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | CREATE DOMAIN nonempty_string AS varchar(15) 9 | CONSTRAINT not_blank CHECK (VALUE::text ~ '\\A[[:alnum:]][[:alnum:] ]+\\Z' AND length(trim(VALUE)) > 0); 10 | 11 | ALTER TABLE invites ADD COLUMN key t_rsa_encrypted_blob NOT NULL DEFAULT '{"ciphertext":"hack","algorithm":"RSA-OAEP"}'::json; 12 | ALTER TABLE invites ADD COLUMN nickname nonempty_string NOT NULL DEFAULT 'friend'; 13 | ALTER TABLE invites ALTER COLUMN key DROP DEFAULT; 14 | ALTER TABLE invites ALTER COLUMN nickname DROP DEFAULT; 15 | 16 | COMMIT; 17 | `); 18 | }, 19 | 20 | down: async (queryInterface, _) => { 21 | await queryInterface.sequelize.query(` 22 | BEGIN TRANSACTION; 23 | 24 | ALTER TABLE invites DROP COLUMN nickname; 25 | ALTER TABLE invites DROP COLUMN key; 26 | DROP DOMAIN nonempty_string; 27 | 28 | COMMIT; 29 | `); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /backend/db/migrations/20200306003039-AddInviteLoginAccess.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | CREATE TABLE invite_login_access ( 9 | login_data_id uuid NOT NULL REFERENCES login_data(id) ON DELETE CASCADE, 10 | invite_id uuid NOT NULL REFERENCES invites(id), 11 | created_at timestamp without time zone DEFAULT now() NOT NULL, 12 | updated_at timestamp without time zone DEFAULT now() NOT NULL, 13 | preview_key t_aes_encrypted_blob NOT NULL, 14 | credentials_key t_aes_encrypted_blob, 15 | status t_login_access_status NOT NULL DEFAULT 'preview' CHECK (status = 'preview' OR status = 'offer/pending'), 16 | PRIMARY KEY (invite_id, login_data_id) 17 | ); 18 | 19 | COMMIT; 20 | `); 21 | }, 22 | 23 | down: async (queryInterface, _) => { 24 | await queryInterface.sequelize.query(` 25 | BEGIN TRANSACTION; 26 | 27 | DROP TABLE invite_login_access; 28 | 29 | COMMIT; 30 | `); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /backend/db/migrations/20200312205449-AddBetaOnboardingFlag.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE users ADD COLUMN is_in_beta_onboarding boolean DEFAULT false NOT NULL; 9 | 10 | COMMIT; 11 | `); 12 | }, 13 | 14 | down: async (queryInterface, _) => { 15 | await queryInterface.sequelize.query(` 16 | BEGIN TRANSACTION; 17 | 18 | ALTER TABLE users DROP COLUMN is_in_beta_onboarding; 19 | 20 | COMMIT; 21 | `); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /backend/db/migrations/20200312210436-AddEmailToInvites.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE invites ADD COLUMN email email_address; 9 | 10 | COMMIT; 11 | `); 12 | }, 13 | 14 | down: async (queryInterface, _) => { 15 | await queryInterface.sequelize.query(` 16 | BEGIN TRANSACTION; 17 | 18 | ALTER TABLE invites DROP COLUMN email; 19 | 20 | COMMIT; 21 | `); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /backend/db/migrations/20200314003000-AddOnDeleteCascadeToHandshakes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE srp_handshakes 9 | DROP CONSTRAINT "srp_handshakes_user_id_fkey", 10 | ADD CONSTRAINT "srp_handshakes_user_id_fkey" 11 | FOREIGN KEY (user_id) REFERENCES users(id) 12 | ON DELETE CASCADE; 13 | 14 | ALTER TABLE email_verifications 15 | DROP CONSTRAINT "email_verifications_user_id_fkey", 16 | ADD CONSTRAINT "email_verifications_user_id_fkey" 17 | FOREIGN KEY (user_id) REFERENCES users(id) 18 | ON DELETE CASCADE; 19 | 20 | COMMIT; 21 | `); 22 | }, 23 | 24 | down: async (queryInterface, _) => { 25 | await queryInterface.sequelize.query(` 26 | BEGIN TRANSACTION; 27 | 28 | ALTER TABLE srp_handshakes 29 | DROP CONSTRAINT "srp_handshakes_user_id_fkey", 30 | ADD CONSTRAINT "srp_handshakes_user_id_fkey" 31 | FOREIGN KEY (user_id) REFERENCES users(id); 32 | 33 | ALTER TABLE email_verifications 34 | DROP CONSTRAINT "email_verifications_user_id_fkey", 35 | ADD CONSTRAINT "email_verifications_user_id_fkey" 36 | FOREIGN KEY (user_id) REFERENCES users(id); 37 | 38 | COMMIT; 39 | `); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /backend/db/migrations/20200314003633-AddBetaOnboardFieldsToWaitlist.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | CREATE TYPE t_waitlist_status AS ENUM ( 9 | 'waiting', 10 | 'invited', 11 | 'accepted' 12 | ); 13 | 14 | ALTER TABLE waitlist_entries ADD COLUMN id uuid NOT NULL UNIQUE DEFAULT uuid_generate_v4(); 15 | ALTER TABLE waitlist_entries ADD COLUMN status t_waitlist_status NOT NULL DEFAULT 'waiting'; 16 | COMMIT; 17 | `); 18 | }, 19 | 20 | down: async (queryInterface, _) => { 21 | await queryInterface.sequelize.query(` 22 | BEGIN TRANSACTION; 23 | 24 | ALTER TABLE waitlist_entries DROP COLUMN id; 25 | ALTER TABLE waitlist_entries DROP COLUMN status; 26 | 27 | COMMIT; 28 | `); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /backend/db/migrations/20200314203239-AddEmailSentAtToInvites.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE waitlist_entries ADD COLUMN email_sent_at timestamp without time zone; 9 | 10 | COMMIT; 11 | `); 12 | }, 13 | 14 | down: async (queryInterface, _) => { 15 | await queryInterface.sequelize.query(` 16 | BEGIN TRANSACTION; 17 | 18 | ALTER TABLE waitlist_entries DROP COLUMN email_sent_at; 19 | 20 | COMMIT; 21 | `); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /backend/db/migrations/20200319003259-AddLastUpdatedAtToUser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE users ADD COLUMN last_active_at TIMESTAMP WITHOUT TIME ZONE; 9 | UPDATE users SET last_active_at = created_at; 10 | ALTER TABLE users ALTER COLUMN last_active_at SET NOT NULL; 11 | ALTER TABLE users ALTER COLUMN last_active_at SET DEFAULT NOW(); 12 | 13 | COMMIT;`); 14 | }, 15 | 16 | down: async (queryInterface, _) => { 17 | await queryInterface.sequelize.query(` 18 | BEGIN TRANSACTION; 19 | 20 | ALTER TABLE users DROP COLUMN last_active_at; 21 | 22 | COMMIT;`); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /backend/db/migrations/20200401193012-AlterNonemptyString.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER DOMAIN nonempty_string DROP CONSTRAINT not_blank; 9 | ALTER DOMAIN nonempty_string ADD CONSTRAINT not_blank CHECK (length(trim(VALUE)) > 0); 10 | 11 | COMMIT; 12 | `); 13 | }, 14 | 15 | down: async (queryInterface, _) => { 16 | await queryInterface.sequelize.query(` 17 | BEGIN TRANSACTION; 18 | 19 | ALTER DOMAIN nonempty_string DROP CONSTRAINT not_blank; 20 | ALTER DOMAIN nonempty_string ADD CONSTRAINT not_blank CHECK (VALUE::text ~ '\\A[[:alnum:]][[:alnum:] ]+\\Z' AND length(trim(VALUE)) > 0); 21 | 22 | COMMIT; 23 | `); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /backend/db/migrations/20200406210451-AddToWaitlistEntries.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query( 6 | ` 7 | BEGIN TRANSACTION; 8 | 9 | ALTER TABLE waitlist_entries 10 | ADD COLUMN email_confirmed boolean NOT NULL DEFAULT false; 11 | 12 | UPDATE waitlist_entries SET email_confirmed = 't'; 13 | 14 | ALTER TABLE waitlist_entries 15 | ADD COLUMN first_name nonempty_string; 16 | 17 | ALTER TABLE waitlist_entries 18 | ADD COLUMN verify_email_secret uuid DEFAULT uuid_generate_v4() NOT NULL; 19 | 20 | ALTER TABLE waitlist_entries 21 | ADD COLUMN verify_email_sent_at timestamp without time zone; 22 | 23 | ALTER TABLE waitlist_entries 24 | ADD CONSTRAINT first_name_required CHECK (created_at < :migrationDate OR first_name IS NOT NULL); 25 | 26 | COMMIT; 27 | `, 28 | { replacements: { migrationDate: new Date().toUTCString() } } 29 | ); 30 | }, 31 | 32 | down: async (queryInterface, _) => { 33 | await queryInterface.sequelize.query(` 34 | BEGIN TRANSACTION; 35 | 36 | ALTER TABLE waitlist_entries DROP COLUMN first_name; 37 | ALTER TABLE waitlist_entries DROP COLUMN email_confirmed; 38 | ALTER TABLE waitlist_entries DROP COLUMN verify_email_secret; 39 | ALTER TABLE waitlist_entries DROP COLUMN verify_email_sent_at; 40 | 41 | COMMIT; 42 | `); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /backend/db/migrations/20200410213416-AddUnsubscribedAtToWaitlist.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE waitlist_entries ADD COLUMN unsubscribed_at TIMESTAMP WITHOUT TIME ZONE; 9 | 10 | COMMIT; 11 | `); 12 | }, 13 | 14 | down: async (queryInterface, _) => { 15 | await queryInterface.sequelize.query(` 16 | BEGIN TRANSACTION; 17 | 18 | ALTER TABLE waitlist_entries DROP COLUMN unsubscribed_at; 19 | 20 | COMMIT; 21 | `); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /backend/db/migrations/20200424005357-AddSharePreviewsToLoginData.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE login_data ADD COLUMN share_previews boolean DEFAULT true NOT NULL; 9 | 10 | CREATE OR REPLACE VIEW logins AS SELECT 11 | login_data.id, 12 | login_data.created_at, 13 | login_data.updated_at, 14 | credentials, 15 | preview, 16 | login_data.user_id AS manager_id, 17 | preview_key, 18 | credentials_key, 19 | login_access.user_id AS member_id, 20 | login_access.status, 21 | login_data.share_previews 22 | FROM login_data 23 | JOIN login_access ON login_data.id = login_access.login_data_id; 24 | 25 | COMMIT; 26 | `); 27 | }, 28 | 29 | down: async (queryInterface, _) => { 30 | await queryInterface.sequelize.query(` 31 | BEGIN TRANSACTION; 32 | 33 | ALTER TABLE login_data DROP COLUMN share_previews; 34 | 35 | CREATE OR REPLACE VIEW logins AS SELECT 36 | login_data.id, 37 | login_data.created_at, 38 | login_data.updated_at, 39 | credentials, 40 | preview, 41 | login_data.user_id AS manager_id, 42 | preview_key, 43 | credentials_key, 44 | login_access.user_id AS member_id, 45 | login_access.status 46 | FROM login_data 47 | JOIN login_access ON login_data.id = login_access.login_data_id; 48 | 49 | COMMIT; 50 | `); 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /backend/db/migrations/20201006183850-AddInternalPasswordResetFlagToUser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, _) => { 5 | await queryInterface.sequelize.query(` 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE users ADD COLUMN forced_password_change_enabled boolean DEFAULT false NOT NULL; 9 | 10 | COMMIT; 11 | `); 12 | }, 13 | 14 | down: async (queryInterface, _) => { 15 | await queryInterface.sequelize.query(` 16 | BEGIN TRANSACTION; 17 | 18 | ALTER TABLE users DROP COLUMN forced_password_change_enabled; 19 | 20 | COMMIT; 21 | `); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /backend/emails/beta_invite.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{greeting}} I'm reaching out to share your Jam invite with you: 7 | {{beta_invite_url}} 9 | 10 | You joined Jam's waiting list {{time_since_waitlist}} ago, so, in 12 | case you forgot, Jam is a new service for sharing accounts with 13 | friends. 15 | 16 | Anyways, Jam is very new, so I'm thrilled you took an early interest. 18 | If you have any feedback, you can reach me at this email. 😊 20 | 21 | Thanks,
John
22 | 23 | P.S. If you definitely didn't add your email to the waiting list, 25 | click here. 28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /backend/emails/friend_request.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | New friend request! 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | @{{sender_username}} wants to be your friend 42 | 43 | 44 | 45 | 46 | 💌 Respond 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /backend/emails/friend_request_accepted.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 💕 You have a new friend! 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | @{{friend_username}} accepted your friend request. How cute! Go share 41 | something with @{{friend_username}} now: 43 | 44 | 45 | 46 | 47 | 🥰 Share with @{{friend_username}} 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /backend/emails/invite_accepted.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 🙌 {{invite_nickname}} accepted your invite! 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | {{invite_nickname}}'s username is @{{from_username}}. You two are now 41 | friends on Jam. You should share something with them, I bet it'll make 42 | them happy: 44 | 45 | 46 | 47 | 48 | 😊 Share with @{{from_username}} 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /backend/emails/login_access_requested.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 🙋 @{{from_username}} asked you to share an account on Jam! 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | @{{from_username}} requested access to one of your accounts on Jam. 41 | You don't have to say yes, but I bet it would make them happy if you 42 | did. 44 | 45 | 46 | 47 | 48 | Respond to @{{from_username}} 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /backend/emails/verify_email.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 👋 Ready to Jam? 19 | 20 | 21 | 22 | 23 | Hey @{{username}}, ready to Jam? Click here to verify your email: 25 | 26 | 27 | 28 | 29 | 30 | Verify me!! 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /backend/graphql-codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "http://localhost:5000/graphql" 3 | documents: 4 | - "./../react-app/src/api.graphql" 5 | - "server/server.graphql" 6 | generates: 7 | server/resolvers.ts: 8 | plugins: 9 | - "typescript" 10 | - "typescript-resolvers" 11 | - add: 12 | content: "import { DeepPartial } from 'utility-types';" 13 | config: 14 | defaultMapper: DeepPartial<{T}> 15 | # @see https://git.io/JU0xa 16 | useIndexSignature: true 17 | config: 18 | avoidOptionals: false 19 | contextType: ./server#GraphQLContext 20 | allowParentTypeOverride: true 21 | hooks: 22 | afterAllFileWrite: 23 | - ./../bin/prettier 24 | -------------------------------------------------------------------------------- /backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | }; 5 | -------------------------------------------------------------------------------- /backend/public/landing/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /backend/public/landing/favicons/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/favicons/android-chrome-384x384.png -------------------------------------------------------------------------------- /backend/public/landing/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /backend/public/landing/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #471f89 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /backend/public/landing/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /backend/public/landing/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /backend/public/landing/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/favicons/favicon.ico -------------------------------------------------------------------------------- /backend/public/landing/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /backend/public/landing/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /backend/public/landing/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "android-chrome-384x384.png", 12 | "sizes": "384x384", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /backend/public/landing/img/2x/confetti-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/confetti-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/2x/confetti-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/confetti-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/2x/confetti-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/confetti-3.png -------------------------------------------------------------------------------- /backend/public/landing/img/2x/confetti-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/confetti-4.png -------------------------------------------------------------------------------- /backend/public/landing/img/2x/confetti-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/confetti-5.png -------------------------------------------------------------------------------- /backend/public/landing/img/2x/confetti-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/confetti-6.png -------------------------------------------------------------------------------- /backend/public/landing/img/2x/confetti-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/confetti-7.png -------------------------------------------------------------------------------- /backend/public/landing/img/2x/distressed-dots-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/distressed-dots-bg.png -------------------------------------------------------------------------------- /backend/public/landing/img/2x/gradient-bright.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/gradient-bright.jpg -------------------------------------------------------------------------------- /backend/public/landing/img/2x/gradient.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/gradient.jpg -------------------------------------------------------------------------------- /backend/public/landing/img/2x/hypno-eye-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/hypno-eye-bg.png -------------------------------------------------------------------------------- /backend/public/landing/img/2x/large-button-squiggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/large-button-squiggles.png -------------------------------------------------------------------------------- /backend/public/landing/img/2x/scroll-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/scroll-arrow.png -------------------------------------------------------------------------------- /backend/public/landing/img/2x/sticky-header-blob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/2x/sticky-header-blob.png -------------------------------------------------------------------------------- /backend/public/landing/img/a-animation/2x/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/a-animation/2x/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/a-animation/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/a-animation/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/confetti-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/confetti-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/confetti-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/confetti-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/confetti-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/confetti-3.png -------------------------------------------------------------------------------- /backend/public/landing/img/confetti-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/confetti-4.png -------------------------------------------------------------------------------- /backend/public/landing/img/confetti-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/confetti-5.png -------------------------------------------------------------------------------- /backend/public/landing/img/confetti-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/confetti-6.png -------------------------------------------------------------------------------- /backend/public/landing/img/confetti-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/confetti-7.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/2x/dude-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/2x/dude-mask.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/2x/eyes-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/2x/eyes-normal.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/2x/girl-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/2x/girl-mask.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/2x/guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/2x/guide.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/2x/pop-box-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/2x/pop-box-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/2x/pop-box-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/2x/pop-box-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/2x/purple-note-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/2x/purple-note-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/2x/purple-note-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/2x/purple-note-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/2x/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/2x/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/2x/yellow-note-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/2x/yellow-note-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/2x/yellow-note-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/2x/yellow-note-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/dude-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/dude-mask.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/eyes-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/eyes-normal.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/girl-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/girl-mask.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/guide.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/pop-box-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/pop-box-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/pop-box-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/pop-box-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/purple-note-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/purple-note-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/purple-note-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/purple-note-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/yellow-note-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/yellow-note-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/dashboard-animation/yellow-note-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/dashboard-animation/yellow-note-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/distressed-dots-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/distressed-dots-bg.png -------------------------------------------------------------------------------- /backend/public/landing/img/footer-animation/2x/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/footer-animation/2x/arrow.png -------------------------------------------------------------------------------- /backend/public/landing/img/footer-animation/2x/squiggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/footer-animation/2x/squiggles.png -------------------------------------------------------------------------------- /backend/public/landing/img/footer-animation/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/footer-animation/arrow.png -------------------------------------------------------------------------------- /backend/public/landing/img/footer-animation/guide@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/footer-animation/guide@2x.png -------------------------------------------------------------------------------- /backend/public/landing/img/footer-animation/squiggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/footer-animation/squiggles.png -------------------------------------------------------------------------------- /backend/public/landing/img/gradient-bright.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/gradient-bright.jpg -------------------------------------------------------------------------------- /backend/public/landing/img/gradient.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/gradient.jpg -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/distressed-dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/distressed-dots.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/dude-eyes-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/dude-eyes-normal.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/dude-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/dude-mask.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/girl-eyes-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/girl-eyes-normal.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/guide.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/heart-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/heart-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/heart-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/heart-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/music-note-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/music-note-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/music-note-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/music-note-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/music-note-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/music-note-3.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/music-note-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/music-note-4.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/pop-box-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/pop-box-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/pop-box-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/pop-box-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/radiant-circles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/radiant-circles.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/squiggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/squiggles.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/2x/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/2x/video.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/distressed-dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/distressed-dots.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/dude-eyes-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/dude-eyes-normal.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/dude-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/dude-mask.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/girl-eyes-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/girl-eyes-normal.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/guide.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/heart-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/heart-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/heart-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/heart-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/music-note-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/music-note-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/music-note-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/music-note-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/music-note-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/music-note-3.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/music-note-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/music-note-4.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/pop-box-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/pop-box-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/pop-box-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/pop-box-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/radiant-circles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/radiant-circles.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/squiggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/squiggles.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/video-player-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/video-player-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/hero-animation/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hero-animation/video.png -------------------------------------------------------------------------------- /backend/public/landing/img/hypno-eye-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/hypno-eye-bg.png -------------------------------------------------------------------------------- /backend/public/landing/img/j-animation/2x/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/j-animation/2x/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/j-animation/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/j-animation/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/jam-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /backend/public/landing/img/jam.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /backend/public/landing/img/large-button-squiggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/large-button-squiggles.png -------------------------------------------------------------------------------- /backend/public/landing/img/large-button-top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /backend/public/landing/img/og-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/og-image.jpg -------------------------------------------------------------------------------- /backend/public/landing/img/scroll-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/scroll-arrow.png -------------------------------------------------------------------------------- /backend/public/landing/img/share-animation/2x/address-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/share-animation/2x/address-bar.png -------------------------------------------------------------------------------- /backend/public/landing/img/share-animation/2x/dude-finger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/share-animation/2x/dude-finger.png -------------------------------------------------------------------------------- /backend/public/landing/img/share-animation/2x/guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/share-animation/2x/guide.png -------------------------------------------------------------------------------- /backend/public/landing/img/share-animation/2x/pop-box-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/share-animation/2x/pop-box-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/share-animation/2x/pop-box-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/share-animation/2x/pop-box-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/share-animation/2x/share-animation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/share-animation/2x/share-animation.png -------------------------------------------------------------------------------- /backend/public/landing/img/share-animation/2x/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/share-animation/2x/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/share-animation/address-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/share-animation/address-bar.png -------------------------------------------------------------------------------- /backend/public/landing/img/share-animation/dude-finger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/share-animation/dude-finger.png -------------------------------------------------------------------------------- /backend/public/landing/img/share-animation/pop-box-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/share-animation/pop-box-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/share-animation/pop-box-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/share-animation/pop-box-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/share-animation/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/share-animation/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation-mobile/2x/dude-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation-mobile/2x/dude-mask.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation-mobile/2x/guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation-mobile/2x/guide.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation-mobile/2x/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation-mobile/2x/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation-mobile/dude-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation-mobile/dude-mask.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation-mobile/guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation-mobile/guide.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation-mobile/split-cost-animation-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation-mobile/split-cost-animation-mobile.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation-mobile/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation-mobile/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/2x/dude-eyes-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/2x/dude-eyes-normal.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/2x/girl-1-eyes-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/2x/girl-1-eyes-normal.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/2x/girl-1-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/2x/girl-1-mask.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/2x/girl-2-eyes-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/2x/girl-2-eyes-normal.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/2x/girl-2-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/2x/girl-2-mask.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/2x/guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/2x/guide.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/2x/light-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/2x/light-blue.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/2x/light-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/2x/light-yellow.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/2x/machine-person-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/2x/machine-person-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/2x/machine-person-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/2x/machine-person-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/2x/machine-person-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/2x/machine-person-3.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/2x/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/2x/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/dude-eyes-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/dude-eyes-normal.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/girl-1-eyes-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/girl-1-eyes-normal.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/girl-1-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/girl-1-mask.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/girl-2-eyes-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/girl-2-eyes-normal.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/girl-2-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/girl-2-mask.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/guide.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/light-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/light-blue.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/light-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/light-yellow.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/machine-person-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/machine-person-1.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/machine-person-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/machine-person-2.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/machine-person-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/machine-person-3.png -------------------------------------------------------------------------------- /backend/public/landing/img/split-cost-animation/static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/split-cost-animation/static.png -------------------------------------------------------------------------------- /backend/public/landing/img/sticky-header-blob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/backend/public/landing/img/sticky-header-blob.png -------------------------------------------------------------------------------- /backend/server/addToBugsnag.ts: -------------------------------------------------------------------------------- 1 | import env from "./env"; 2 | import * as _ from "lodash"; 3 | 4 | export const clearBugsnagSession = () => { 5 | try { 6 | env.bugsnag.user = {}; 7 | env.bugsnag.metaData = {}; 8 | } catch (error) { 9 | env.bugsnag.notify(`Clearing bugsnag metadata failed? ${error}`); 10 | } 11 | }; 12 | 13 | export const addToBugsnagUser = (userInfo: object) => { 14 | try { 15 | env.bugsnag.user = _.defaultsDeep( 16 | _.cloneDeep(userInfo), 17 | _.cloneDeep(env.bugsnag.user) 18 | ); 19 | } catch (error) { 20 | env.bugsnag.notify(`Setting bugsnag user failed? ${error}`); 21 | } 22 | }; 23 | 24 | export const addToBugsnagMetadata = (metadata: object) => { 25 | try { 26 | env.bugsnag.metaData = _.defaultsDeep( 27 | _.cloneDeep(metadata), 28 | _.cloneDeep(env.bugsnag.metaData) 29 | ); 30 | } catch (error) { 31 | env.bugsnag.notify(`Setting bugsnag metadata failed? ${error}`); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /backend/server/emails.test.ts: -------------------------------------------------------------------------------- 1 | import { verifyEmailHtml } from "./emails"; 2 | 3 | it("generates an email template", () => { 4 | expect( 5 | verifyEmailHtml({ 6 | username: "specialuser", 7 | verify_url: "https://jam.link/verify/123", 8 | }) 9 | ).toMatchSnapshot(); 10 | }); 11 | 12 | it("throws an error if we don't complete all the templates", () => { 13 | expect(() => 14 | verifyEmailHtml({ 15 | username: "specialuser", 16 | }) 17 | ).toThrowError("Expected locals to define verify_url"); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/server/error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AppError should be the instance of all errors on Jam that should 3 | * be considered properly handled and therefore not reported to a 4 | * bug tracker. 5 | */ 6 | export class AppError extends Error {} 7 | 8 | export class AppReqError extends AppError { 9 | constructor(errorType: TReqErrorType) { 10 | super(errorType); 11 | } 12 | } 13 | 14 | type TReqErrorType = 15 | | "NotFound/Invite" 16 | | "NotFound/FriendRequest" 17 | | "NotFound/Login" 18 | | "NotFound/EmailVerification" 19 | | "Unauthorized" 20 | | "Unauthorized/EmailNotVerified" 21 | | "AuthenticationFailed" 22 | | "AuthenticationFailed/InvalidDigest"; 23 | 24 | export type RequestErrorFn = (errorType: TReqErrorType) => never; 25 | 26 | export const requestError: RequestErrorFn = (message) => { 27 | throw new AppReqError(message); 28 | }; 29 | -------------------------------------------------------------------------------- /backend/server/index.ts: -------------------------------------------------------------------------------- 1 | import app from "./server"; 2 | 3 | const port = process.env.PORT || 5000; 4 | process.env.APP_MODE = "web"; 5 | 6 | // tslint:disable-next-line:no-console 7 | app.listen(port, () => console.log(`Listening on port ${port}`)); 8 | -------------------------------------------------------------------------------- /backend/server/logGraphqlError.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { GraphQLError } from "graphql"; 3 | 4 | export const logGraphqlError = (error: GraphQLError) => { 5 | if (!error.source || !error.locations) { 6 | console.error("GraphQL error occured but we don't have source?"); 7 | return; 8 | } 9 | 10 | const { line } = error.locations[0]; 11 | const source = error.source.body.split("\n"); 12 | source[line - 1] = chalk.bgYellowBright.whiteBright(source[line - 1]); 13 | const highlightedSource = source.join("\n"); 14 | 15 | const msg = ` 16 | 17 | ${chalk.bgRed.white( 18 | `[GraphQL Error] ${error.originalError?.message || "No original error?"}` 19 | )} 20 | 21 | • Path: ${chalk.bold.yellow(error.path?.join("/"))} 22 | • Offending source: 23 | 24 | ${highlightedSource} 25 | 26 | ${chalk.bgRed.white( 27 | `[GraphQL Error] ${error.originalError?.message || "No original error?"}` 28 | )}`; 29 | 30 | console.error(msg); 31 | }; 32 | -------------------------------------------------------------------------------- /backend/server/models/EmailVerification.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize-typescript"; 2 | import { User } from "./"; 3 | import env from "./../env"; 4 | 5 | @Sequelize.Table({ tableName: "email_verifications", underscored: true }) 6 | export default class EmailVerification extends Sequelize.Model< 7 | EmailVerification 8 | > { 9 | @Sequelize.PrimaryKey 10 | @Sequelize.IsUUID(4) 11 | @Sequelize.Column({ allowNull: false, autoIncrement: true }) 12 | id: string; 13 | 14 | @Sequelize.ForeignKey(() => User) 15 | @Sequelize.NotNull 16 | @Sequelize.IsUUID(4) 17 | @Sequelize.Column({ allowNull: false }) 18 | userId: string; 19 | 20 | @Sequelize.BelongsTo(() => User, "userId") 21 | user: User; 22 | 23 | @Sequelize.CreatedAt createdAt: Date; 24 | @Sequelize.UpdatedAt updatedAt: Date; 25 | 26 | url() { 27 | return new URL(`/verify/${this.id}`, env.domain).toString(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/server/models/Friend.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize-typescript"; 2 | import { User, FriendRequest } from "./"; 3 | import { AesEncryptedBlob, RsaEncryptedBlob } from "./types"; 4 | 5 | export enum FriendRequestStatus { 6 | Pending = "pending", 7 | Accepted = "accepted", 8 | Rejected = "rejected", 9 | } 10 | 11 | @Sequelize.Table({ tableName: "friends", underscored: true }) 12 | export default class Friend extends Sequelize.Model { 13 | @Sequelize.PrimaryKey 14 | @Sequelize.ForeignKey(() => User) 15 | @Sequelize.IsUUID(4) 16 | @Sequelize.Column({ allowNull: false }) 17 | userId: string; 18 | 19 | @Sequelize.BelongsTo(() => User, "userId") 20 | user: User; 21 | 22 | @Sequelize.ForeignKey(() => User) 23 | @Sequelize.IsUUID(4) 24 | @Sequelize.Column({ allowNull: false }) 25 | friendId: string; 26 | 27 | @Sequelize.BelongsTo(() => User, "friendId") 28 | friend: User; 29 | 30 | @Sequelize.ForeignKey(() => FriendRequest) 31 | @Sequelize.IsUUID(4) 32 | @Sequelize.Column({ allowNull: false }) 33 | friendRequestId: string; 34 | 35 | @Sequelize.BelongsTo(() => FriendRequest) 36 | friendRequest: FriendRequest; 37 | 38 | @Sequelize.CreatedAt createdAt: Date; 39 | @Sequelize.UpdatedAt updatedAt: Date; 40 | } 41 | -------------------------------------------------------------------------------- /backend/server/models/FriendRequest.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize-typescript"; 2 | import { User } from "./"; 3 | import { AesEncryptedBlob, RsaEncryptedBlob } from "./types"; 4 | import { Transactionable } from "sequelize/types"; 5 | 6 | export enum FriendRequestStatus { 7 | Pending = "pending", 8 | Accepted = "accepted", 9 | Rejected = "rejected", 10 | } 11 | 12 | @Sequelize.Table({ tableName: "friend_requests", underscored: true }) 13 | export default class FriendRequest extends Sequelize.Model { 14 | @Sequelize.PrimaryKey 15 | @Sequelize.IsUUID(4) 16 | @Sequelize.Column({ allowNull: false, autoIncrement: true }) 17 | id: string; 18 | 19 | @Sequelize.ForeignKey(() => User) 20 | @Sequelize.NotNull 21 | @Sequelize.IsUUID(4) 22 | @Sequelize.Column({ allowNull: false }) 23 | initiatorId: string; 24 | 25 | @Sequelize.BelongsTo(() => User, "initiatorId") 26 | initiator: User; 27 | 28 | @Sequelize.ForeignKey(() => User) 29 | @Sequelize.NotNull 30 | @Sequelize.IsUUID(4) 31 | @Sequelize.Column({ allowNull: false }) 32 | recipientId: string; 33 | 34 | @Sequelize.BelongsTo(() => User, "recipientId") 35 | recipient: User; 36 | 37 | @Sequelize.Column({ 38 | type: Sequelize.DataType.STRING, 39 | defaultValue: FriendRequestStatus.Pending, 40 | }) 41 | status: FriendRequestStatus; 42 | 43 | @Sequelize.CreatedAt createdAt: Date; 44 | @Sequelize.UpdatedAt updatedAt: Date; 45 | 46 | async accept(updateOptions: Transactionable) { 47 | this.update( 48 | { 49 | status: FriendRequestStatus.Accepted, 50 | }, 51 | updateOptions 52 | ); 53 | } 54 | 55 | async reject(updateOptions: Transactionable) { 56 | this.update( 57 | { 58 | status: FriendRequestStatus.Rejected, 59 | }, 60 | updateOptions 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /backend/server/models/SRPHandshake.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize-typescript"; 2 | import { Op } from "sequelize"; 3 | import { User } from "."; 4 | 5 | const byteStringPattern = (size: number) => new RegExp(`^[\\da-f]{${size}}$`); 6 | 7 | @Sequelize.Scopes(() => ({ 8 | complete: { 9 | where: { sessionKey: { [Op.not]: null } }, 10 | }, 11 | })) 12 | @Sequelize.Table({ tableName: "srp_handshakes", underscored: true }) 13 | export default class SRPHandshake extends Sequelize.Model { 14 | @Sequelize.IsUUID(4) 15 | @Sequelize.PrimaryKey 16 | @Sequelize.Column({ autoIncrement: true }) 17 | id: string; 18 | 19 | @Sequelize.CreatedAt createdAt: Date; 20 | 21 | @Sequelize.UpdatedAt updatedAt: Date; 22 | 23 | @Sequelize.ForeignKey(() => User) 24 | @Sequelize.NotNull 25 | @Sequelize.IsUUID(4) 26 | @Sequelize.Column({ allowNull: false }) 27 | userId: string; 28 | 29 | @Sequelize.BelongsTo(() => User) 30 | user: User; 31 | 32 | @Sequelize.Column({ 33 | type: Sequelize.DataType.STRING, 34 | allowNull: false, 35 | validate: { is: byteStringPattern(64) }, 36 | }) 37 | serverSecret: string; 38 | 39 | @Sequelize.Column({ 40 | type: Sequelize.DataType.STRING, 41 | allowNull: false, 42 | validate: { is: byteStringPattern(512) }, 43 | }) 44 | clientPublic: string; 45 | 46 | @Sequelize.Column({ 47 | type: Sequelize.DataType.STRING, 48 | validate: { is: byteStringPattern(64) }, 49 | }) 50 | sessionKey: string | null; 51 | 52 | @Sequelize.Column({ 53 | type: Sequelize.DataType.STRING, 54 | allowNull: false, 55 | }) 56 | sessionWrapper: string; 57 | 58 | isComplete(): this is SRPHandshake & { sessionKey: string } { 59 | return this.sessionKey !== null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /backend/server/models/types.ts: -------------------------------------------------------------------------------- 1 | export interface AesEncryptedBlob { 2 | ciphertext: string; 3 | iv: string; 4 | salt: string; 5 | algorithm: string; 6 | } 7 | 8 | export interface RsaEncryptedBlob { 9 | ciphertext: string; 10 | algorithm: string; 11 | } 12 | -------------------------------------------------------------------------------- /backend/server/pulse.ts: -------------------------------------------------------------------------------- 1 | import { IncomingWebhook } from "@slack/webhook"; 2 | import env from "./env"; 3 | import { slackNotificationQueue } from "./queue"; 4 | 5 | export const slackWebhook = new IncomingWebhook(env.slackPulseWebhookUrl, { 6 | icon_emoji: ":heartbeat:", 7 | }); 8 | 9 | export const slackPulse = (message: string) => 10 | slackNotificationQueue.add({ message }); 11 | -------------------------------------------------------------------------------- /backend/server/queries/loginPreviewTodo.ts: -------------------------------------------------------------------------------- 1 | import { sequelize, User, Login } from "../models"; 2 | import { QueryTypes } from "sequelize"; 3 | import * as _ from "lodash"; 4 | 5 | type RawQueryResultType = { friendId: string; loginIds: string[] }[]; 6 | 7 | export const loginPreviewTodo = async (userId: string) => { 8 | const todo: RawQueryResultType = await sequelize.query( 9 | `SELECT 10 | friend_id AS "friendId", array_agg(login_data.id) AS "loginIds" 11 | FROM friends 12 | CROSS JOIN login_data 13 | LEFT JOIN login_access 14 | ON login_access.login_data_id = login_data.id 15 | AND login_access.user_id = friend_id 16 | WHERE friends.user_id = :user_id 17 | AND login_data.user_id = :user_id 18 | AND login_data.share_previews = 't' 19 | AND login_access.login_data_id IS NULL 20 | GROUP BY friend_id;`, 21 | { 22 | replacements: { user_id: userId }, 23 | type: QueryTypes.SELECT, 24 | raw: true, 25 | } 26 | ); 27 | 28 | const byFriendId = _.keyBy(todo, "friendId"); 29 | const friends = await User.findAll({ 30 | where: { id: Object.keys(byFriendId) }, 31 | attributes: ["id", "publicKey"], 32 | }); 33 | 34 | const withPubKeys = friends.map((friend) => ({ 35 | ...byFriendId[friend.id], 36 | publicKey: friend.publicKey, 37 | })); 38 | 39 | return { 40 | todo: withPubKeys, 41 | logins: await Login.findAll({ 42 | where: { 43 | id: _.uniq(_.flatMap(todo, "loginIds")), 44 | managerId: userId, 45 | memberId: userId, 46 | }, 47 | }), 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /backend/server/queue.ts: -------------------------------------------------------------------------------- 1 | import Queue from "bull"; 2 | import env from "./env"; 3 | import { User } from "./models"; 4 | const { setQueues } = require("bull-board"); 5 | 6 | export const verifyEmailQueue = new Queue<{ userId: string }>( 7 | "verify-email", 8 | env.redisUrl 9 | ); 10 | 11 | export const verifyWaitlistEmailQueue = new Queue<{ email: string }>( 12 | "verify-waitlist-email", 13 | env.redisUrl 14 | ); 15 | 16 | // Catch all for emails involving an action performed by one user affecting another 17 | export const friendNotificationEmailQueue = new Queue<{ 18 | name: string; 19 | toId: string; 20 | fromId: string; 21 | data?: object; 22 | }>("friend-notification-email", env.redisUrl); 23 | 24 | export const friendInviteEmailQueue = new Queue<{ 25 | inviteId: string; 26 | fromId: string; 27 | }>("friend-invite-email", env.redisUrl); 28 | 29 | export const betaInviteEmailQueue = new Queue<{ 30 | waitlistEntryId: string; 31 | }>("beta-invite-email", env.redisUrl); 32 | 33 | export const betaInviteScheduler = new Queue<{}>( 34 | "beta-invite-scheduler", 35 | env.redisUrl 36 | ); 37 | 38 | export const slackNotificationQueue = new Queue<{ 39 | message: string; 40 | }>("slack-notification", env.redisUrl); 41 | 42 | setQueues([ 43 | verifyEmailQueue, 44 | verifyWaitlistEmailQueue, 45 | friendNotificationEmailQueue, 46 | friendInviteEmailQueue, 47 | betaInviteEmailQueue, 48 | betaInviteScheduler, 49 | slackNotificationQueue, 50 | ]); 51 | -------------------------------------------------------------------------------- /backend/server/resolvers/Invite.ts: -------------------------------------------------------------------------------- 1 | import { Invite, InviteLoginAccess, LoginData, Login } from "./../models"; 2 | import { InviteResolvers } from "../resolvers"; 3 | import { GraphQLContext, requireVerifiedUser } from "../server"; 4 | import { Op } from "sequelize"; 5 | 6 | const InviteResolver: InviteResolvers> = { 7 | loginShares: async (invite, { includePreviews }) => { 8 | let shares: InviteLoginAccess[]; 9 | 10 | if (includePreviews) { 11 | shares = await InviteLoginAccess.findAll({ 12 | where: { inviteId: invite.id }, 13 | include: [LoginData], 14 | }); 15 | } else { 16 | shares = await InviteLoginAccess.findAll({ 17 | where: { 18 | inviteId: invite.id, 19 | credentialsKey: { 20 | [Op.not]: null, 21 | }, 22 | }, 23 | include: [LoginData], 24 | }); 25 | } 26 | 27 | return shares.map((share) => ({ 28 | id: share.loginData.id, 29 | preview: share.loginData.preview, 30 | previewKey: share.previewKey, 31 | credentialsKey: share.credentialsKey, 32 | schemaVersion: share.loginData.schemaVersion, 33 | type: share.loginData.type, 34 | })); 35 | }, 36 | loginPreviewTodos: async (invite: Required, parent, ctx, info) => { 37 | const { user: currentUser } = requireVerifiedUser(ctx.req); 38 | 39 | return Login.loginPreviewTodoForInvite({ 40 | loginOwner: currentUser, 41 | inviteId: invite.id, 42 | }); 43 | }, 44 | }; 45 | 46 | export default InviteResolver; 47 | -------------------------------------------------------------------------------- /backend/server/resolvers/MyLogin.ts: -------------------------------------------------------------------------------- 1 | import { LoginAccess } from "./../models"; 2 | import { MyLoginResolvers, MyLogin } from "../resolvers"; 3 | import { GraphQLContext } from "../server"; 4 | import { allMembersOf } from "../db/loginMembers"; 5 | 6 | const MyLoginResolver: MyLoginResolvers> = { 7 | manager: async (login: Required) => { 8 | const managerAccess = await LoginAccess.findOne({ 9 | where: { loginDataId: login.id, status: "manager" }, 10 | include: ["user"], 11 | }); 12 | 13 | if (!managerAccess) throw new Error("Not found!"); 14 | 15 | return managerAccess.user; 16 | }, 17 | members: async (login: Required, parent, ctx, info) => { 18 | return allMembersOf(login); 19 | }, 20 | }; 21 | 22 | export default MyLoginResolver; 23 | -------------------------------------------------------------------------------- /backend/server/resolvers/PendingOutboundFriendRequest.ts: -------------------------------------------------------------------------------- 1 | import { User, Login } from "./../models"; 2 | import { 3 | PendingOutboundFriendRequestResolvers, 4 | PendingOutboundFriendRequest, 5 | } from "../resolvers"; 6 | import { GraphQLContext, requireVerifiedUser } from "../server"; 7 | 8 | const PendingOutboundFriendRequestResolver: PendingOutboundFriendRequestResolvers< 9 | GraphQLContext, 10 | Required 11 | > = { 12 | loginPreviewTodos: async (friendRequest, parent, ctx, info) => { 13 | if (!friendRequest.recipient) 14 | throw new Error( 15 | "Expected recipient to be included in resolved friend request" 16 | ); 17 | const { user: currentUser } = requireVerifiedUser(ctx.req); 18 | const friendUser = await User.findByPk(friendRequest.recipient.id); 19 | 20 | if (!friendUser) 21 | throw new Error( 22 | "Invariant failed: Model lookup for Friend shouldn't be null!" 23 | ); 24 | 25 | return Login.loginPreviewTodoForFriend({ 26 | loginOwner: currentUser, 27 | friend: friendRequest.recipient, 28 | }); 29 | }, 30 | }; 31 | 32 | export default PendingOutboundFriendRequestResolver; 33 | -------------------------------------------------------------------------------- /backend/server/resolvers/SharedLogin.ts: -------------------------------------------------------------------------------- 1 | import { LoginAccess } from "./../models"; 2 | import { SharedLoginResolvers, SharedLogin } from "../resolvers"; 3 | import { GraphQLContext } from "../server"; 4 | import { userMembersOf } from "../db/loginMembers"; 5 | 6 | const SharedLoginResolver: SharedLoginResolvers< 7 | GraphQLContext, 8 | Required 9 | > = { 10 | manager: async (login: Required) => { 11 | const managerAccess = await LoginAccess.findOne({ 12 | where: { loginDataId: login.id, status: "manager" }, 13 | include: ["user"], 14 | }); 15 | 16 | if (!managerAccess) throw new Error("Not found!"); 17 | 18 | return managerAccess.user; 19 | }, 20 | members: async (login: Required, parent, ctx, info) => { 21 | return userMembersOf(login); 22 | }, 23 | }; 24 | 25 | export default SharedLoginResolver; 26 | -------------------------------------------------------------------------------- /backend/server/sendEmail.ts: -------------------------------------------------------------------------------- 1 | import mailgunJs from "mailgun-js"; 2 | import env from "./env"; 3 | import chalk from "chalk"; 4 | 5 | export interface IEmail { 6 | from?: string; 7 | to: string; 8 | subject: string; 9 | text: string; 10 | html: string; 11 | } 12 | 13 | export const sendEmail = async (message: IEmail) => { 14 | const config = { 15 | apiKey: env.mailgunApiKey, 16 | domain: env.mailgunDomain, 17 | }; 18 | const mailgun = mailgunJs(config); 19 | 20 | const subjectPrefix = env.isProd() ? "" : `[${env.NODE_ENV}] `; 21 | 22 | const to = env.isProd() ? message.to : "support+development@jam.link"; 23 | 24 | if (env.isDevelopment()) { 25 | console.info( 26 | chalk.bold( 27 | `${chalk.greenBright("[#email]")} sending "${message.subject}" to ${to}` 28 | ) 29 | ); 30 | } 31 | 32 | const data = { 33 | from: message.from ? message.from : `Jam! `, 34 | to, 35 | subject: subjectPrefix + message.subject, 36 | text: message.text, 37 | html: message.html, 38 | }; 39 | 40 | return mailgun.messages().send(data, (error) => { 41 | if (error) { 42 | console.warn(`Mailgun send failure: ${JSON.stringify(error)}`, { 43 | level: "error", 44 | }); 45 | if (error.message.indexOf("parameter is not a valid address") === -1) { 46 | throw error; 47 | } 48 | } 49 | return; 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /backend/server/utils.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import env from "./env"; 3 | 4 | export const toInt = (value: string): number => { 5 | const int = parseInt(value); 6 | if (_.isNaN(int)) throw new Error("Invalid parse!"); 7 | 8 | return int; 9 | }; 10 | 11 | export const sleep = async (ms: number) => { 12 | if (env.isProd()) { 13 | env.bugsnag.notify("Calling sleep() in prod??"); 14 | } 15 | 16 | return new Promise((resolve) => setTimeout(() => resolve(), ms)); 17 | }; 18 | 19 | export const isUuid = (value: string) => 20 | /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/.test(value); 21 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "react", 15 | "outDir": "build", 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "strictPropertyInitialization": false, 19 | "module": "commonjs", 20 | "typeRoots": [ 21 | "./../typings", 22 | "./node_modules/@types", 23 | "./../node_modules/@types" 24 | ] 25 | }, 26 | "include": ["server"] 27 | } 28 | -------------------------------------------------------------------------------- /backend/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "strict": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /bin/build-prod-extension: -------------------------------------------------------------------------------- 1 | ../extension/bin/build-prod-extension -------------------------------------------------------------------------------- /bin/compile-quicktype: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | INPUT="src/types/schema.ts" 4 | OUTPUT="src/types/index.ts" 5 | 6 | cd react-app 7 | 8 | ./node_modules/.bin/quicktype --src-lang typescript -l typescript -o "$OUTPUT" "$INPUT" 9 | 10 | cd .. 11 | 12 | bin/prettier "react-app/$OUTPUT" 13 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd backend 4 | bin/console 5 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source './bin/strict-mode.sh' 4 | 5 | # Kill our process and all of our child processes upon ctrl+c. 6 | trap "exit" INT TERM ERR 7 | trap "kill 0" EXIT 8 | 9 | wait_for_server() { 10 | printf "Waiting for server to respond on port 5000" 11 | until $(curl --output /dev/null --silent -I http://localhost:5000); do 12 | printf '.' 13 | sleep 0.25 14 | done 15 | } 16 | 17 | redis-server & 18 | bin/dev-server & 19 | bin/dev-worker & 20 | # A bug in react-scripts 3.4.1 broke backgrounded jobs. Workaround: https://git.io/JfXHx 21 | BROWSER=none yarn dev-client | cat - & 22 | yarn dev-extension & 23 | bin/react-storybook & 24 | 25 | # GraphQL Codegen dumps errors instantly if server isn't up. Let's keep output clean... 26 | wait_for_server 27 | bin/graphql-codegen --watch & 28 | 29 | wait %1 30 | wait %2 31 | wait %3 32 | wait %4 33 | wait %5 34 | wait %6 35 | wait %7 36 | -------------------------------------------------------------------------------- /bin/dev-server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source './bin/strict-mode.sh' 4 | source './bin/shared/ts-node-dev-instance.sh' 5 | 6 | cd backend 7 | 8 | ts-node-dev-instance 9229 server/index.ts 9 | -------------------------------------------------------------------------------- /bin/dev-static: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source './bin/strict-mode.sh' 4 | source './bin/shared/ts-node-dev-instance.sh' 5 | 6 | cd static 7 | 8 | ts-node-dev-instance 9231 index.ts 9 | -------------------------------------------------------------------------------- /bin/dev-worker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source './bin/strict-mode.sh' 4 | source './bin/shared/ts-node-dev-instance.sh' 5 | 6 | cd backend 7 | 8 | ts-node-dev-instance 9230 --transpileOnly server/worker.ts 9 | 10 | -------------------------------------------------------------------------------- /bin/dump-dev-sql: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pg_dump --data-only --exclude-table-data '"SequelizeMeta"' jam-dev > db/dev.sql 4 | -------------------------------------------------------------------------------- /bin/graphql-codegen: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source './bin/strict-mode.sh' 4 | 5 | # Kill our process and all of our child processes upon ctrl+c. 6 | trap "exit" INT TERM ERR 7 | trap "kill 0" EXIT 8 | 9 | cd backend 10 | yarn run graphql-codegen "$@" & 11 | 12 | cd ../react-app 13 | yarn run graphql-codegen "$@" & 14 | 15 | wait %1 16 | wait %2 17 | -------------------------------------------------------------------------------- /bin/heroku-postbuild: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source './bin/strict-mode.sh' 4 | 5 | # Compile our backend TypeScript to JS 6 | ( 7 | cd backend || exit 1 8 | yarn build 9 | ) 10 | 11 | # Compile our CRA bundle 12 | ( 13 | cd react-app || exit 1 14 | yarn 15 | yarn build 16 | ) 17 | 18 | # Copy the bundle into our backend's public directory 19 | cp -a ./react-app/build/. ./backend/public/ 20 | 21 | # Copy the public directory into the newly created backend build 22 | cp -r backend/public/ backend/build/public 23 | 24 | # Rename so we don't autoserve this 25 | mv backend/build/public/index.html backend/build/public/app.html 26 | -------------------------------------------------------------------------------- /bin/integration-test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NODE_ENV=test ./node_modules/.bin/jest --watch __tests__/*.test.ts "$@" 4 | -------------------------------------------------------------------------------- /bin/migrate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source './bin/strict-mode.sh' 4 | 5 | NODE_ENV=${NODE_ENV:-DEVELOPMENT} 6 | 7 | cd backend 8 | 9 | node_modules/.bin/sequelize db:migrate 10 | -------------------------------------------------------------------------------- /bin/prettier: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file should be in the project root's bin directory. 4 | # We need to know the project root so we can 5 | PROJECT_ROOT="$(dirname $(dirname $(readlink -f $0)))" 6 | 7 | # Ensure we use the prettier binary from the server's dependencies 8 | # This is important since bin/graphql-codegen calls this file from the react-app subdirectory 9 | PRETTIER_BIN="$PROJECT_ROOT/node_modules/.bin/prettier" 10 | 11 | function list-prettier-files() { 12 | git ls-files -- *.{yml,tsx,ts,mjml,md,json,js,html,graphql,css} 13 | } 14 | 15 | # Support passing in a short list of files 16 | if [ "$#" -eq "0" ]; then 17 | files="$(list-prettier-files | tr "\n" " ")" 18 | else 19 | files="$@" 20 | fi 21 | 22 | $PRETTIER_BIN --write -- $files 23 | -------------------------------------------------------------------------------- /bin/react-storybook: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd react-app 4 | yarn storybook 5 | -------------------------------------------------------------------------------- /bin/reload-dev-sql: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source './bin/strict-mode.sh' 4 | 5 | DB="jam-dev" 6 | 7 | num_connections() { 8 | local query="select COUNT(*) from pg_stat_activity where datname = '$DB'"; 9 | local count="$(psql --tuples-only --command "$query")" 10 | echo "${count//[[:space:]]/}" 11 | } 12 | 13 | wait_for_no_connections() { 14 | if [ "$(num_connections)" -eq "0" ]; then 15 | return 0; 16 | fi 17 | 18 | local last_count=$(num_connections) 19 | 20 | printf "Waiting for $last_count connections to disconnect" 21 | while [ "$(num_connections)" -ne "0" ]; do 22 | printf . 23 | sleep 0.1 24 | done 25 | 26 | echo "!" 27 | } 28 | 29 | if [ "$(pgrep -f "psql $DB")" ]; then 30 | echo "Found psql connections open to $DB. Killing..." 31 | pkill -f "psql $DB" 32 | fi 33 | 34 | wait_for_no_connections 35 | 36 | dropdb "$DB" 37 | createdb "$DB" 38 | bin/migrate 39 | psql "$DB" < db/dev.sql 40 | -------------------------------------------------------------------------------- /bin/shared/ts-node-dev-instance.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function ts-node-dev-instance() { 4 | local inspect_port="$1" 5 | shift 6 | 7 | ./node_modules/.bin/ts-node-dev \ 8 | --watch "$(git ls-files *.graphql | tr "\n" ",").env" \ 9 | --project tsconfig.json \ 10 | --inspect="127.0.0.1:$inspect_port" \ 11 | --respawn "$@" 12 | } 13 | -------------------------------------------------------------------------------- /bin/strict-mode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Taken from dkubb/dockerfiles: https://git.io/viGZZ 4 | # 5 | # Reference: 6 | # http://www.davidpashley.com/articles/writing-robust-shell-scripts/ 7 | # http://kvz.io/blog/2013/11/21/bash-best-practices/ 8 | # http://redsymbol.net/articles/unofficial-bash-strict-mode/ 9 | 10 | set -o errexit # Exit when an expression fails 11 | set -o pipefail # Exit when a command in a pipeline fails 12 | set -o nounset # Exit when an undefined variable is used 13 | set -o noglob # Disable shell globbing 14 | set -o noclobber # Disable automatic file overwriting 15 | 16 | IFS=$'\n\t' # Set default field separator to not split on spaces 17 | -------------------------------------------------------------------------------- /extension/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /extension/README.md: -------------------------------------------------------------------------------- 1 | # Jam's Chrome extension 2 | 3 | Based on [chrome-extension-webpack-boilerplate](https://github.com/samuelsimoes/chrome-extension-webpack-boilerplate) 4 | -------------------------------------------------------------------------------- /extension/client: -------------------------------------------------------------------------------- 1 | ../react-app/src/ -------------------------------------------------------------------------------- /extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jam-extension", 3 | "version": "0.0.15", 4 | "description": "Share accounts with your friends", 5 | "scripts": { 6 | "build": "node utils/build.js", 7 | "start": "node utils/webserver.js" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "^7.9.6", 11 | "@babel/plugin-transform-runtime": "^7.9.6", 12 | "@babel/polyfill": "^7.8.7", 13 | "@babel/preset-env": "^7.9.6", 14 | "@babel/preset-react": "^7.9.4", 15 | "@rebass/forms": "^4.0.6", 16 | "@rebass/preset": "^4.0.5", 17 | "@types/chrome": "^0.0.110", 18 | "@types/lodash": "^4.14.150", 19 | "@types/node": "^14.0.1", 20 | "@types/react": "^16.9.35", 21 | "@types/react-dom": "^16.9.8", 22 | "@types/rebass": "^4.0.5", 23 | "@types/rebass__forms": "^4.0.2", 24 | "@types/styled-components": "^5.1.0", 25 | "babel-loader": "8.0.6", 26 | "babel-plugin-lodash": "^3.3.4", 27 | "babel-plugin-macros": "^2.8.0", 28 | "commander": "^5.1.0", 29 | "copy-webpack-plugin": "5.0.5", 30 | "css-loader": "3.2.0", 31 | "file-loader": "4.3.0", 32 | "fs-extra": "8.1.0", 33 | "html-loader": "0.5.5", 34 | "html-webpack-plugin": "3.2.0", 35 | "http-server": "^0.12.3", 36 | "style-loader": "1.0.0", 37 | "ts-loader": "^7.0.4", 38 | "typescript": "^3.9.7", 39 | "webpack": "4.41.2", 40 | "webpack-cli": "3.3.10", 41 | "webpack-dev-server": "3.9.0", 42 | "write-file-webpack-plugin": "4.5.1" 43 | }, 44 | "dependencies": { 45 | "@rebass/forms": "^4.0.6", 46 | "@rebass/preset": "^4.0.5", 47 | "clean-webpack-plugin": "3.0.0", 48 | "react": "^16.13.1", 49 | "react-dom": "^16.13.1", 50 | "react-image-crop": "^8.6.2", 51 | "react-router-dom": "^5.1.2", 52 | "rebass": "^4.0.7", 53 | "styled-components": "^5.1.0", 54 | "unstated": "^2.1.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /extension/src/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /extension/src/img/icon-128-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/extension/src/img/icon-128-dev.png -------------------------------------------------------------------------------- /extension/src/img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/extension/src/img/icon-128.png -------------------------------------------------------------------------------- /extension/src/img/icon-16-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/extension/src/img/icon-16-dev.png -------------------------------------------------------------------------------- /extension/src/img/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/extension/src/img/icon-16.png -------------------------------------------------------------------------------- /extension/src/img/icon-34-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/extension/src/img/icon-34-dev.png -------------------------------------------------------------------------------- /extension/src/img/icon-34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/extension/src/img/icon-34.png -------------------------------------------------------------------------------- /extension/src/img/icon-48-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/extension/src/img/icon-48-dev.png -------------------------------------------------------------------------------- /extension/src/img/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/extension/src/img/icon-48.png -------------------------------------------------------------------------------- /extension/src/img/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/extension/src/img/logo-192.png -------------------------------------------------------------------------------- /extension/src/img/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/extension/src/img/logo-512.png -------------------------------------------------------------------------------- /extension/src/js/popup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { Box } from "rebass/styled-components"; 4 | 5 | import { App } from "@client/App"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById("popup-root") 12 | ); 13 | -------------------------------------------------------------------------------- /extension/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jam! Share accounts with friends", 3 | "short_name": "Jam", 4 | "description": "Share accounts with friends without revealing the password", 5 | "background": { 6 | "page": "background.html" 7 | }, 8 | "browser_action": { 9 | "default_popup": "popup.html", 10 | "default_icon": "icon-34.png" 11 | }, 12 | "icons": { 13 | "16": "icon-16.png", 14 | "48": "icon-48.png", 15 | "128": "icon-128.png" 16 | }, 17 | "manifest_version": 2, 18 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 19 | "permissions": [ 20 | "cookies", 21 | "http://*/*", 22 | "https://*/*", 23 | "activeTab", 24 | "storage" 25 | ], 26 | "externally_connectable": { 27 | "matches": ["https://*.jam.link/*"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /extension/src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["es2017", "dom", "es2019"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "baseUrl": ".", 16 | "pretty": true, 17 | "sourceMap": true, 18 | "strict": false, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "noImplicitReturns": false, 22 | "paths": { 23 | "@client/*": ["client/*"] 24 | }, 25 | "typeRoots": [ 26 | "./typings", 27 | "./node_modules/@types", 28 | "./../node_modules/@types" 29 | ], 30 | "types": ["chrome", "node"] 31 | }, 32 | "include": ["src/**/*", "typings/**/*"], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /extension/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg"; 2 | declare module "*.gif"; 3 | declare module "*.png"; 4 | -------------------------------------------------------------------------------- /extension/typings/rebass__forms: -------------------------------------------------------------------------------- 1 | client/../typings/rebass__forms -------------------------------------------------------------------------------- /extension/utils/build.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"), 2 | config = require("../webpack.config"); 3 | 4 | delete config.chromeExtensionBoilerplate; 5 | 6 | webpack(config, function (err) { 7 | if (err) throw err; 8 | }); 9 | -------------------------------------------------------------------------------- /extension/utils/env.js: -------------------------------------------------------------------------------- 1 | // tiny wrapper with default env vars 2 | module.exports = { 3 | NODE_ENV: process.env.NODE_ENV || "development", 4 | PORT: process.env.PORT || 4000, 5 | ENABLE_SOURCE_MAPS: 6 | process.env.ENABLE_SOURCE_MAPS === "true" || 7 | process.env.NODE_ENV !== "production", 8 | }; 9 | -------------------------------------------------------------------------------- /extension/utils/webserver.js: -------------------------------------------------------------------------------- 1 | var WebpackDevServer = require("webpack-dev-server"), 2 | webpack = require("webpack"), 3 | config = require("../webpack.config"), 4 | env = require("./env"), 5 | path = require("path"); 6 | 7 | var options = config.chromeExtensionBoilerplate || {}; 8 | var excludeEntriesToHotReload = options.notHotReload || []; 9 | 10 | for (var entryName in config.entry) { 11 | if (excludeEntriesToHotReload.indexOf(entryName) === -1) { 12 | config.entry[entryName] = [ 13 | "webpack-dev-server/client?http://localhost:" + env.PORT, 14 | "webpack/hot/dev-server", 15 | ].concat(config.entry[entryName]); 16 | } 17 | } 18 | 19 | config.plugins = [new webpack.HotModuleReplacementPlugin()].concat( 20 | config.plugins || [] 21 | ); 22 | 23 | delete config.chromeExtensionBoilerplate; 24 | 25 | var compiler = webpack(config); 26 | 27 | var server = new WebpackDevServer(compiler, { 28 | hot: true, 29 | contentBase: path.join(__dirname, "../build"), 30 | sockPort: env.PORT, 31 | headers: { 32 | "Access-Control-Allow-Origin": "*", 33 | }, 34 | disableHostCheck: true, 35 | }); 36 | 37 | server.listen(env.PORT); 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jam", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:5000/", 6 | "workspaces": [ 7 | "backend", 8 | "react-app", 9 | "extension" 10 | ], 11 | "dependencies": { 12 | "prettier": "^2.0.5" 13 | }, 14 | "scripts": { 15 | "start": "cd backend && yarn start", 16 | "worker": "cd backend && yarn worker", 17 | "dev-client": "cd react-app && yarn start", 18 | "dev-extension": "cd extension && yarn start", 19 | "heroku-postbuild": "bin/heroku-postbuild" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | }, 36 | "devDependencies": {} 37 | } 38 | -------------------------------------------------------------------------------- /react-app/.env.sample: -------------------------------------------------------------------------------- 1 | ./../backend/.env.sample -------------------------------------------------------------------------------- /react-app/.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ["../src/**/*.stories.[tj]sx"], 3 | addons: [ 4 | "@storybook/preset-create-react-app", 5 | "@storybook/addon-actions", 6 | "@storybook/addon-links", 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /react-app/.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from "@storybook/addons"; 2 | import theme from "./theme"; 3 | 4 | addons.setConfig({ 5 | theme: theme, 6 | }); 7 | -------------------------------------------------------------------------------- /react-app/.storybook/preview-body.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /react-app/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /react-app/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from "styled-components/macro"; 2 | import { addDecorator } from "@storybook/react"; 3 | import React from "react"; 4 | 5 | import theme from "./../src/theme"; 6 | import { BrowserEnvProvider } from "./../src/BrowserEnv"; 7 | 8 | addDecorator((storyFn) => ( 9 | 10 | {storyFn()} 11 | 12 | )); 13 | -------------------------------------------------------------------------------- /react-app/.storybook/theme.js: -------------------------------------------------------------------------------- 1 | import { create } from "@storybook/theming/create"; 2 | 3 | export default create({ 4 | // base: "dark" 5 | }); 6 | -------------------------------------------------------------------------------- /react-app/graphql-codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "http://localhost:5000/graphql" 3 | documents: 4 | - "src/api.graphql" 5 | generates: 6 | src/api/graphql.ts: 7 | plugins: 8 | - "typescript" 9 | - "typescript-operations" 10 | - "typescript-graphql-request" 11 | config: 12 | avoidOptionals: true 13 | hooks: 14 | afterAllFileWrite: 15 | - ./../bin/prettier 16 | -------------------------------------------------------------------------------- /react-app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | globals: { 5 | "ts-jest": { 6 | tsConfig: "tsconfig.test.json", 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /react-app/public/account-share-email-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/public/account-share-email-icon.png -------------------------------------------------------------------------------- /react-app/public/email-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/public/email-header.png -------------------------------------------------------------------------------- /react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/public/favicon.ico -------------------------------------------------------------------------------- /react-app/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/public/favicon.png -------------------------------------------------------------------------------- /react-app/public/img/2x/no-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/public/img/2x/no-avatar.png -------------------------------------------------------------------------------- /react-app/public/img/no-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/public/img/no-avatar.png -------------------------------------------------------------------------------- /react-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/public/logo192.png -------------------------------------------------------------------------------- /react-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/public/logo512.png -------------------------------------------------------------------------------- /react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Jam!", 3 | "name": "Jam—Accounts with your friends", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /react-app/public/respond-to-friend-request-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/public/respond-to-friend-request-button.png -------------------------------------------------------------------------------- /react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /react-app/public/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/public/social-card.png -------------------------------------------------------------------------------- /react-app/src/AuthRequired.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { AppContainer, LoadedApp } from "./AppContainer"; 3 | import { Signin } from "./Signin"; 4 | import { Subscribe } from "unstated"; 5 | import { BetaOnboardPage } from "./components/BetaOnboarding"; 6 | import { Page } from "./components/Page"; 7 | import { LoadingPage } from "./components/LoadingAnim"; 8 | 9 | export const AuthRequired = (props: { 10 | children: (app: LoadedApp) => JSX.Element; 11 | unauthenticatedView?: JSX.Element; 12 | app: AppContainer; 13 | }) => { 14 | if (props.app.state.loading) return ; 15 | 16 | if (props.app.state.kind === "app/unauthenticated") { 17 | return props.unauthenticatedView || ; 18 | } 19 | 20 | if (props.app.state.kind !== "app/loaded") return null; 21 | 22 | const loadedApp = props.app.assertLoaded(); 23 | 24 | if (loadedApp.state.currentUser.isInBetaOnboarding) 25 | return ( 26 | 27 | 28 | 29 | ); 30 | 31 | return props.children(loadedApp); 32 | }; 33 | -------------------------------------------------------------------------------- /react-app/src/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clientEnv from "./clientEnv"; 3 | import { Image, ImageProps } from "rebass/styled-components"; 4 | 5 | type SizeProps = 6 | | { 7 | width: string; 8 | height: string; 9 | circle?: undefined | false; 10 | size?: undefined; 11 | } 12 | | { 13 | width?: undefined; 14 | height?: undefined; 15 | circle: true; 16 | size: string; 17 | }; 18 | 19 | type Props = SizeProps & { uri: string } & Omit< 20 | ImageProps, 21 | "css" | "width" | "height" | "circle" | "size" 22 | >; 23 | 24 | export const Avatar: React.FC = ({ 25 | uri, 26 | circle, 27 | size, 28 | width, 29 | height, 30 | sx: origSx, 31 | ...imgProps 32 | }) => { 33 | const absoluteUrl = clientEnv.absolutePath(uri); 34 | let sx = { ...origSx }; 35 | let dim: { width: string; height: string }; 36 | 37 | if (circle) { 38 | if (!size) throw new Error("Size should be defined when circle specified"); 39 | dim = { width: size, height: size }; 40 | sx = { ...sx, borderRadius: "50%" }; 41 | } else { 42 | if (!width || !height) 43 | throw new Error( 44 | "If circle isn't specified, width and height must be provided" 45 | ); 46 | dim = { width, height }; 47 | } 48 | 49 | if (uri === "/img/no-avatar.png") { 50 | return ( 51 | 60 | ); 61 | } else { 62 | return ; 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /react-app/src/NewLogin.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { LoadedApp } from "./AppContainer"; 3 | import * as _ from "lodash"; 4 | import { EditLogin } from "./components/EditLogin"; 5 | 6 | import { Redirect } from "react-router-dom"; 7 | import { LoginV1 } from "./types"; 8 | import { TShareRecipient } from "./api"; 9 | import { LoginType } from "./api/graphql"; 10 | 11 | export const NewLogin: React.FC<{ 12 | addLogin: (login: { 13 | details: LoginV1; 14 | shareWith: TShareRecipient[]; 15 | sharePreviews: boolean; 16 | }) => Promise; 17 | friends: TShareRecipient[]; 18 | }> = (props) => { 19 | const [submitted, setSubmitted] = useState(false); 20 | 21 | const addLogin = async ({ 22 | login, 23 | shareWith, 24 | sharePreviews, 25 | }: { 26 | login: LoginV1; 27 | shareWith: TShareRecipient[]; 28 | sharePreviews: boolean; 29 | }) => { 30 | await props.addLogin({ details: login, shareWith, sharePreviews }); 31 | setSubmitted(true); 32 | }; 33 | 34 | if (submitted) return ; 35 | 36 | return ( 37 | 43 | ); 44 | }; 45 | 46 | export const NewLoginPage: React.FC<{ app: LoadedApp }> = (props) => { 47 | const [friends, setFriends] = useState([]); 48 | 49 | useEffect(() => { 50 | const loadFriends = async () => 51 | setFriends(await props.app.api().getPotentialShares()); 52 | 53 | loadFriends(); 54 | }, []); 55 | 56 | return ( 57 | 59 | props.app.addLogin({ ...data, type: LoginType.RawCredentials }) 60 | } 61 | friends={friends} 62 | /> 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /react-app/src/assertNever.ts: -------------------------------------------------------------------------------- 1 | export function assertNever(...args: never[]): never { 2 | throw new Error( 3 | "Unexhaustive case statement! assertNever was called with " + 4 | args.toString() 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /react-app/src/bugsnagUser.ts: -------------------------------------------------------------------------------- 1 | import env from "./clientEnv"; 2 | import * as _ from "lodash"; 3 | 4 | export const addToBugsnagUser = (userInfo: object) => { 5 | try { 6 | env.bugsnag.user = _.defaultsDeep( 7 | _.cloneDeep(userInfo), 8 | _.cloneDeep(env.bugsnag.user) 9 | ); 10 | } catch (error) { 11 | env.bugsnag.notify(`Setting bugsnag user failed? ${error}`); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /react-app/src/colors.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | text: { 3 | primary: "#555A60", 4 | info: "#696D74", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /react-app/src/components/BoldedListSentence.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { assertNever } from "./../assertNever"; 3 | 4 | export const BoldedListSentence: React.FC<{ words: string[] }> = ({ 5 | words, 6 | }) => { 7 | if (words.length === 0) 8 | throw new Error("Shouldn't be called with empty list"); 9 | if (words.length === 1) return {words[0]}; 10 | 11 | if (words.length === 2) 12 | return ( 13 | <> 14 | {words[0]} and {words[1]} 15 | 16 | ); 17 | 18 | if (words.length > 2) 19 | return ( 20 | <> 21 | {words 22 | .slice(0, -1) 23 | .map((login) => {login}) 24 | .reduce((prev, curr) => [prev, ", ", curr])} 25 | , and {words[words.length - 1]} 26 | 27 | ); 28 | 29 | return assertNever(); 30 | }; 31 | -------------------------------------------------------------------------------- /react-app/src/components/Emoji.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components/macro"; 2 | 3 | export const Emoji = styled.span` 4 | font-style: normal; 5 | `; 6 | -------------------------------------------------------------------------------- /react-app/src/components/EmojiList.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components/macro"; 2 | 3 | export const EmojiList = styled.ul` 4 | margin: 0 auto; 5 | font-size: 16px; 6 | line-height: 30px; 7 | color: #696d74; 8 | 9 | list-style: none; 10 | text-indent: none; 11 | padding-left: 0; 12 | 13 | & > li { 14 | list-style: none; 15 | text-indent: none; 16 | padding-left: 0; 17 | 18 | & > strong { 19 | font-weight: bold; 20 | } 21 | } 22 | 23 | margin-bottom: 21px; 24 | `; 25 | -------------------------------------------------------------------------------- /react-app/src/components/FloatingIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box } from "rebass/styled-components"; 3 | 4 | export const FloatingIcon: React.FC<{ 5 | img: string; 6 | iconSize: number; 7 | size?: number; 8 | }> = (props) => { 9 | const size = props.size || props.iconSize * 2; 10 | 11 | return ( 12 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /react-app/src/components/FormError.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled, { css } from "styled-components/macro"; 3 | 4 | const FormErrorBlock = styled.span<{ visible: boolean }>` 5 | background: rgba(243, 60, 60, 0.8); 6 | border-radius: 10px; 7 | font-size: 18px; 8 | font-weight: 500; 9 | width: 100%; 10 | display: block; 11 | text-align: center; 12 | line-height: 1.53; 13 | padding: 20px; 14 | color: white; 15 | word-wrap: wrap; 16 | min-height: 27px; 17 | 18 | ${(props) => 19 | props.visible 20 | ? css` 21 | opacity: 1; 22 | ` 23 | : css` 24 | opacity: 0; 25 | display: none; 26 | `} 27 | 28 | transition: all 0.5s ease-in-out; 29 | `; 30 | 31 | export const FormError = (props: { msg?: React.ReactChild }) => ( 32 | {props.msg || ""} 33 | ); 34 | -------------------------------------------------------------------------------- /react-app/src/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import _ from "lodash"; 3 | import styled, { css } from "styled-components/macro"; 4 | import theme from "./../theme"; 5 | import { Image, ImageProps } from "rebass/styled-components"; 6 | import icons, { IconNames } from "./../icons"; 7 | 8 | type Props = Omit & { 9 | kind: IconNames; 10 | }; 11 | 12 | export const Icon: React.FC = ({ kind, ...props }) => { 13 | return ( 14 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /react-app/src/components/InviteVia.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components/macro"; 3 | 4 | import smsImg from "./../img/invite-via-sms.svg"; 5 | import emailImg from "./../img/invite-via-email.svg"; 6 | import linkImg from "./../img/invite-via-link.svg"; 7 | 8 | import * as _ from "lodash"; 9 | 10 | const iconMap = { 11 | email: emailImg, 12 | sms: smsImg, 13 | link: linkImg, 14 | }; 15 | 16 | const InviteViaIcon = styled.div<{ type: "email" | "sms" | "link" }>` 17 | width: 40px; 18 | height: 40px; 19 | border-radius: 50px; 20 | background: url(${(props) => iconMap[props.type]}); 21 | background-repeat: no-repeat; 22 | cursor: pointer; 23 | `; 24 | 25 | const ViaText = styled.span` 26 | font-weight: bold; 27 | font-size: 16px; 28 | line-height: 40px; 29 | 30 | margin-left: 24px; 31 | vertical-align: middle; 32 | display: table-cell; 33 | 34 | color: #555a60; 35 | `; 36 | 37 | const ViaWrap = styled.div` 38 | display: flex; 39 | flex-direction: row; 40 | height: 40px; 41 | padding: 10px 0 10px 25px; 42 | margin-top: 5px; 43 | 44 | &:hover { 45 | background-color: #ffd7f7; 46 | cursor: pointer; 47 | } 48 | `; 49 | 50 | export const InviteVia = (props: { 51 | type: "email" | "sms" | "link"; 52 | text: string; 53 | onClick?: () => any; 54 | "data-jam-analyze"?: string; 55 | }) => { 56 | const onClick = props.onClick || _.noop; 57 | return ( 58 | 59 | 60 | {props.text} 61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /react-app/src/components/LegalCheckbox.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css, keyframes } from "styled-components/macro"; 2 | 3 | import checkmark from "../img/checkmark.svg"; 4 | 5 | const glowingRed = keyframes` 6 | from { 7 | background: #F383A4; 8 | border: 2px solid #f24a7c; 9 | } 10 | 11 | 50% { 12 | background: #f24a7c; 13 | border-color: #F5064E; 14 | } 15 | 16 | to { 17 | background: #F383A4; 18 | border-color: #f24a7c; 19 | } 20 | 21 | `; 22 | 23 | export const LegalCheckbox = styled.input.attrs({ 24 | type: "checkbox", 25 | })<{ error?: string }>` 26 | width: 25px; 27 | height: 25px; 28 | cursor: pointer; 29 | background: #31115e; 30 | border-radius: 3px; 31 | border: 2px solid #d6cef8; 32 | appearance: none; 33 | outline: none; 34 | transition: background-color 0.5s ease; 35 | 36 | &:hover { 37 | transition: border-color 0.5s ease; 38 | border-color: white; 39 | } 40 | 41 | &:checked { 42 | background-image: url(${checkmark}); 43 | background-repeat: no-repeat; 44 | background-position: center center; 45 | } 46 | 47 | ${(props) => 48 | props.error && 49 | css` 50 | background: #f383a4; 51 | border: 2px solid #f24a7c; 52 | animation: ${glowingRed} 2s ease-in-out infinite; 53 | `} 54 | `; 55 | -------------------------------------------------------------------------------- /react-app/src/components/LegalDoc.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components/macro"; 2 | 3 | export const LegalDoc = styled.div` 4 | color: #555a60; 5 | 6 | & > h1 { 7 | font-weight: 900; 8 | font-size: 36px; 9 | text-align: center; 10 | } 11 | 12 | & > h2 { 13 | font-weight: bold; 14 | font-size: 30px; 15 | } 16 | 17 | & > p { 18 | line-height: 1.53; 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /react-app/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import spinner from "./../img/spinner.gif"; 3 | import spinnerBlack from "./../img/spinner-black.gif"; 4 | import { Image, ImageProps } from "rebass/styled-components"; 5 | 6 | type ImgProps = Omit; 7 | 8 | export const LoadingSpinner = ( 9 | props: ImgProps & { color?: "white" | "black" } = { color: "white" } 10 | ) => ( 11 | 16 | ); 17 | -------------------------------------------------------------------------------- /react-app/src/components/LocalStorageCheck.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Card } from "./Card"; 4 | import { Body } from "./SimpleMessageCard"; 5 | import styled from "styled-components/macro"; 6 | import { Page } from "./Page"; 7 | import { Emoji } from "./Emoji"; 8 | 9 | const localStorageAvailable = () => { 10 | try { 11 | localStorage.setItem("@jam/storage-test", "42"); 12 | return localStorage.getItem("@jam/storage-test") === "42"; 13 | } catch (error) { 14 | return false; 15 | } 16 | }; 17 | 18 | const CookiesHeader = styled.div` 19 | display: flex; 20 | flex-direction: row; 21 | justify-content: center; 22 | height: 48px; 23 | 24 | & > ${Emoji} { 25 | font-size: 48px; 26 | margin-right: 10px; 27 | margin-left: -10px; 28 | } 29 | 30 | & > h1 { 31 | font-weight: 900; 32 | font-size: 18px; 33 | line-height: 48px; 34 | text-align: center; 35 | 36 | color: #555a60; 37 | } 38 | 39 | margin-bottom: 30px; 40 | `; 41 | 42 | export const LocalStorageCheck: React.FC = (props) => { 43 | return localStorageAvailable() ? ( 44 | <>{props.children} 45 | ) : ( 46 | 47 | 48 | 49 | 🍪 50 |

Cookie?

51 |
52 | Jam needs cookies to function. Can you please enable them? 53 |
54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /react-app/src/components/MagicLinkCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Card } from "./../components/Card"; 3 | import { Box, Text, Flex } from "rebass/styled-components"; 4 | import magicLinkImg from "./../img/magic-link-with-bg.svg"; 5 | import { Btn } from "./Button"; 6 | 7 | export const MagicLinkCard = () => ( 8 | 9 | 19 | 20 | Magic Link ✨ 21 | 22 | 28 | Share access to your account without revealing your password 29 | 30 | 31 | 32 | ); 33 | -------------------------------------------------------------------------------- /react-app/src/components/NoImagePlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled, { css } from "styled-components/macro"; 3 | 4 | const defaultColors = [ 5 | "#E776B3", 6 | "#769CE7", 7 | "#76E7A3", 8 | "#F3AE6E", 9 | "#F36E6E", 10 | "#F36EEE", 11 | "#D01D1D", 12 | "#20D01D", 13 | "#2B1DD0", 14 | "#CDD01D", 15 | ]; 16 | 17 | const randomColorBy = (letter: string) => 18 | defaultColors[letter.charCodeAt(0) % defaultColors.length]; 19 | 20 | interface IconProps { 21 | backgroundColor?: string; 22 | circle?: boolean; 23 | size: number; 24 | text?: string; 25 | } 26 | 27 | export const NoImagePlaceholder = styled.span` 28 | width: ${(props) => props.size}px; 29 | height: ${(props) => props.size}px; 30 | 31 | display: block; 32 | position: relative; 33 | 34 | ${(props) => 35 | props.circle && 36 | css` 37 | border-radius: 50%; 38 | `} 39 | 40 | /* Make broken images display the first letter of title */ 41 | /* font-size: 32px; */ 42 | font-size: ${(props) => props.size * 0.5}px; 43 | border-radius: 50%; 44 | background: ${(props) => 45 | props.backgroundColor || randomColorBy((props.text || "?")[0])}; 46 | &::before { 47 | content: "${(props) => (props.text || "?")[0]}"; 48 | } 49 | color: white; 50 | font-weight: 900; 51 | width: ${(props) => props.size}px; 52 | height: ${(props) => props.size}px; 53 | align-items: center; 54 | justify-content: center; 55 | text-transform: uppercase; 56 | z-index: 2; 57 | display: flex; 58 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.25); 59 | `; 60 | -------------------------------------------------------------------------------- /react-app/src/components/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text, Box } from "rebass/styled-components"; 3 | import { SimpleCard } from "./Card"; 4 | import { Btn } from "./Button"; 5 | import { Emoji } from "./Emoji"; 6 | import { Link } from "react-router-dom"; 7 | 8 | export const NotFound = () => ( 9 | 10 | 19 | 20 | 🤷‍♂️ 21 | 22 | Not found 23 | 24 | 25 | 26 | Whoops, we're not able to find the page you were looking for. 27 | 28 | 29 | Either we have a bug or you mistyped something in the URL. 30 | 31 | {/* 32 | // @ts-expect-error */} 33 | 34 | 🏠 Go home 35 | 36 | 37 | 38 | ); 39 | -------------------------------------------------------------------------------- /react-app/src/components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { css, keyframes } from "styled-components/macro"; 3 | import magnifyingGlass from "./../img/magnifyingGlass.svg"; 4 | 5 | const SearchInput = styled.input.attrs({ type: "text" })` 6 | height: 50px; 7 | width: 100%; 8 | background: url(${magnifyingGlass}), rgba(185, 180, 184, 0.35); 9 | background-repeat: no-repeat; 10 | background-position: 10px; 11 | border: 1px solid rgba(255, 255, 255, 0.24); 12 | box-sizing: border-box; 13 | border-radius: 10px; 14 | 15 | font-style: normal; 16 | font-weight: 500; 17 | font-size: 18px; 18 | line-height: 50px; 19 | /* identical to box height */ 20 | 21 | text-indent: 35px; 22 | 23 | color: #b3b6bb; 24 | 25 | &::placeholder { 26 | color: #b3b6bb; 27 | } 28 | 29 | outline: none; 30 | 31 | caret-color: #555a60; 32 | `; 33 | 34 | export const SearchBar: React.FC = (props) => ( 35 | 40 | ); 41 | -------------------------------------------------------------------------------- /react-app/src/components/SelectUsers.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components/macro"; 3 | import * as _ from "lodash"; 4 | import { SelectableLoginMember, Grid } from "./LoginMember"; 5 | import { props } from "ramda"; 6 | 7 | type TSelectUser = { id: string; name: string; avatarUrl?: string }; 8 | 9 | export const SelectUsers: React.FC<{ 10 | users: TSelectUser[]; 11 | selected: TSelectUser[]; 12 | onChange: (value: TSelectUser[]) => void; 13 | }> = (props) => { 14 | const [selectedIds, setSelectedIds] = useState<{ 15 | [key: string]: boolean; 16 | }>( 17 | _.fromPairs(props.selected.map((selectedUser) => [selectedUser.id, true])) 18 | ); 19 | 20 | // Sort users so that existing members come first, so that they line up with where they were displayed 21 | // before edit mode in the ViewLogin state. Using `useState` so that we don't re-sort users on each render 22 | // which would move users around when selected 23 | const [sortedUsers, _set] = useState( 24 | _.sortBy(props.users, (user) => !selectedIds[user.id]) 25 | ); 26 | 27 | React.useEffect(() => { 28 | props.onChange(props.users.filter((user) => selectedIds[user.id])); 29 | }, [selectedIds]); 30 | 31 | return ( 32 | 33 | {sortedUsers.map((user, index) => ( 34 | { 39 | setSelectedIds({ 40 | ...selectedIds, 41 | [user.id]: !selectedIds[user.id], 42 | }); 43 | }} 44 | selected={selectedIds[user.id]} 45 | /> 46 | ))} 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /react-app/src/components/SimpleMessageCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components/macro"; 3 | import { Emoji } from "./Emoji"; 4 | 5 | export const Header = styled.div` 6 | display: flex; 7 | flex-direction: row; 8 | justify-content: center; 9 | height: 48px; 10 | width: 100%; 11 | 12 | & > ${Emoji} { 13 | font-size: 48px; 14 | margin-right: 10px; 15 | margin-left: -10px; 16 | } 17 | 18 | & > h1 { 19 | font-weight: 900; 20 | font-size: 18px; 21 | margin-top: 20px; 22 | text-align: center; 23 | 24 | color: #555a60; 25 | } 26 | 27 | margin-bottom: 30px; 28 | `; 29 | 30 | export const Body = styled.div` 31 | font-size: 16px; 32 | line-height: 24px; 33 | width: 100%; 34 | max-width: 300px; 35 | justify-content: center; 36 | align-items: center; 37 | margin: 0 auto; 38 | margin-bottom: 24px; 39 | 40 | color: #696d74; 41 | `; 42 | -------------------------------------------------------------------------------- /react-app/src/components/SubmitBtn.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components/macro"; 2 | 3 | export const SubmitBtn = styled.button.attrs(() => ({ 4 | type: "submit", 5 | }))` 6 | border: 2px solid #ffffff; 7 | box-sizing: border-box; 8 | border-radius: 5px; 9 | outline: none; 10 | border: none; 11 | font-style: italic; 12 | font-weight: bold; 13 | font-size: 24px; 14 | line-height: 28px; 15 | align-items: center; 16 | text-align: center; 17 | opacity: 100%; 18 | padding: 5px; 19 | 20 | &:disabled { 21 | background-color: #c25cdc; 22 | } 23 | 24 | background-color: #f253cf; 25 | cursor: pointer; 26 | 27 | color: #ffffff; 28 | `; 29 | -------------------------------------------------------------------------------- /react-app/src/components/YourFriends.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { UserList, ViewFriendRow } from "./AddFriendRow"; 3 | import { Card } from "./Card"; 4 | import distanceInWordsToNow from "date-fns/formatDistanceToNow"; 5 | import { SocialGraphFriend } from "../FindFriends"; 6 | 7 | export const YourFriends: React.FC<{ friends: SocialGraphFriend[] }> = ({ 8 | friends, 9 | }) => ( 10 | 11 | 12 | {friends.map((friend, index) => { 13 | let description = "Not sharing anything... yet!"; 14 | 15 | const receivingCount = friend.loginsSharedWithMe.length; 16 | const givingCount = friend.loginsSharedWithThem.length; 17 | 18 | if (receivingCount > 0) { 19 | description = `Sharing ${receivingCount} account${ 20 | receivingCount > 1 ? "s" : "" 21 | } with you`; 22 | } else if (givingCount > 0) { 23 | description = `Has access to ${givingCount} of your accounts`; 24 | } 25 | return ( 26 | 27 | ); 28 | })} 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /react-app/src/components/glow.tsx: -------------------------------------------------------------------------------- 1 | import { keyframes } from "styled-components/macro"; 2 | 3 | export const glow = keyframes` 4 | from { 5 | opacity: 1; 6 | } 7 | 8 | 50% { 9 | opacity: 0.6; 10 | } 11 | 12 | to { 13 | opacity: 1; 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /react-app/src/friendRequests.ts: -------------------------------------------------------------------------------- 1 | import { PendingInboundFriendRequest } from "./api/graphql"; 2 | import { LoadedApp } from "./AppContainer"; 3 | 4 | export const acceptFriendRequest = async (app: LoadedApp, id: string) => { 5 | const { 6 | me: { encryptedPrivateKey }, 7 | } = await app.graphql().GetMe(); 8 | 9 | const result = await app.graphql().AcceptFriendRequest({ id }); 10 | if (!result) throw new Error("Bad response from AcceptFriendRequest?"); 11 | }; 12 | 13 | export const rejectFriendRequest = async (app: LoadedApp, id: string) => { 14 | const result = await app.graphql().RejectFriendRequest({ requestId: id }); 15 | if (!result) throw new Error("Bad response from AcceptFriendRequest?"); 16 | }; 17 | -------------------------------------------------------------------------------- /react-app/src/generics.ts: -------------------------------------------------------------------------------- 1 | import { ApiReturnType } from "./graphqlApi"; 2 | import * as _ from "lodash"; 3 | 4 | export type Unboxed = T extends (infer U)[] ? U : T; 5 | 6 | export type Replace = Omit & Record; 7 | export type ReplaceEach, T> = Replace< 8 | Unboxed, 9 | K, 10 | T 11 | >[]; 12 | 13 | // expands object types one level deep 14 | export type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; 15 | 16 | // expands object types recursively 17 | export type ExpandRecursively = T extends object 18 | ? T extends infer O 19 | ? { [K in keyof O]: ExpandRecursively } 20 | : never 21 | : T; 22 | -------------------------------------------------------------------------------- /react-app/src/icons.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | import checkImg from "./img/checkmark.svg"; 4 | import arrowImg from "./img/create-arrow.svg"; 5 | import backArrowImg from "./img/back-arrow.svg"; 6 | import trashCan from "./img/trash-can.svg"; 7 | import spinnerImg from "./img/spinner.gif"; 8 | import blackSpinnerImg from "./img/spinner-black.gif"; 9 | import copyIcon from "./img/copy-icon.svg"; 10 | import checkmark from "./img/checkmark.svg"; 11 | import approved from "./img/approved.svg"; 12 | import approvedWhite from "./img/approved-white.svg"; 13 | import invalid from "./img/invalid.svg"; 14 | 15 | // Compose a flat structure of icons so we can guarantee the type we export is always correct 16 | const flatIcons = { 17 | back: backArrowImg, 18 | next: arrowImg, 19 | trash: trashCan, 20 | "spinner.white": spinnerImg, 21 | "spinner.black": blackSpinnerImg, 22 | checkmark, 23 | copy: copyIcon, 24 | "circledCheckmark.whiteOnGreen": approved, 25 | "circledCheckmark.greenOnWhite": approvedWhite, 26 | circledExclamation: invalid, 27 | }; 28 | 29 | export type IconNames = keyof typeof flatIcons; 30 | 31 | export default flatIcons; 32 | -------------------------------------------------------------------------------- /react-app/src/img/addFriendsCardBg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/addFriendsCardBg.png -------------------------------------------------------------------------------- /react-app/src/img/addFriendsCardVectors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/addFriendsCardVectors.png -------------------------------------------------------------------------------- /react-app/src/img/addLoginCardBg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/addLoginCardBg.png -------------------------------------------------------------------------------- /react-app/src/img/addLoginCardVector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/addLoginCardVector.png -------------------------------------------------------------------------------- /react-app/src/img/approved-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /react-app/src/img/approved.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /react-app/src/img/back-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/back-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /react-app/src/img/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/broken-heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/camera-icon-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/camera-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/checkmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /react-app/src/img/chrome-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/chrome-icon.png -------------------------------------------------------------------------------- /react-app/src/img/close-button-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/close-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/copy-done-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /react-app/src/img/copy-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/copy-link-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /react-app/src/img/create-arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /react-app/src/img/domain-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/domain-icon.png -------------------------------------------------------------------------------- /react-app/src/img/edit-pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/email-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/griflan: -------------------------------------------------------------------------------- 1 | ./../../../backend/public/landing/img/ -------------------------------------------------------------------------------- /react-app/src/img/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/heart-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/heart-icon.png -------------------------------------------------------------------------------- /react-app/src/img/home-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/invalid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /react-app/src/img/invite-friend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /react-app/src/img/invite-present-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/invite-via-email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /react-app/src/img/invite-via-sms.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /react-app/src/img/login-complete-checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /react-app/src/img/magnifyingGlass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/new-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/new-account.png -------------------------------------------------------------------------------- /react-app/src/img/next-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /react-app/src/img/password-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/password-icon.png -------------------------------------------------------------------------------- /react-app/src/img/save-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/send-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/send-arrow.png -------------------------------------------------------------------------------- /react-app/src/img/spinner-black.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/spinner-black.gif -------------------------------------------------------------------------------- /react-app/src/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/spinner.gif -------------------------------------------------------------------------------- /react-app/src/img/trash-can.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react-app/src/img/username-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backus/jam/88bb8f64621e59fe09127edab2e84f5efe3672a9/react-app/src/img/username-icon.png -------------------------------------------------------------------------------- /react-app/src/index.css: -------------------------------------------------------------------------------- 1 | @media screen { 2 | html { 3 | background-image: url(./img/griflan/gradient.jpg); 4 | } 5 | } 6 | 7 | @media screen and (-webkit-min-device-pixel-ratio: 1.3), 8 | screen and (min-device-pixel-ratio: 1.3), 9 | screen and (min-resolution: 1.3dppx) { 10 | html { 11 | background-image: url(./img/griflan/2x/gradient.jpg); 12 | background-size: 1257px 871px; 13 | } 14 | } 15 | 16 | html { 17 | min-height: 100%; 18 | background-color: #aa00ff; 19 | 20 | background-position: center center; 21 | background-repeat: no-repeat; 22 | background-attachment: fixed; 23 | } 24 | 25 | body { 26 | font-size: 18px; 27 | height: 100%; 28 | padding: 0; 29 | margin: 0; 30 | 31 | font-family: azo-sans-web, sans-serif; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-osx-font-smoothing: grayscale; 34 | 35 | -webkit-overflow-scrolling: auto; 36 | } 37 | 38 | * { 39 | font-family: azo-sans-web, sans-serif; 40 | } 41 | 42 | body.modal-open { 43 | overflow: hidden; 44 | } 45 | -------------------------------------------------------------------------------- /react-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | // browserslist doesn't automatically polyfill. See: https://git.io/JvpEq 2 | import "react-app-polyfill/ie11"; 3 | import "react-app-polyfill/stable"; 4 | 5 | // polyfills for IE11 https://reactjs.org/docs/javascript-environment-requirements.html 6 | import "core-js/es/map"; 7 | import "core-js/es/set"; 8 | 9 | import React from "react"; 10 | import ReactDOM from "react-dom"; 11 | import * as serviceWorker from "./serviceWorker"; 12 | 13 | import "./index.css"; 14 | import env from "./clientEnv"; 15 | import { App } from "./App"; 16 | import bugsnagReact from "@bugsnag/plugin-react"; 17 | 18 | env.bugsnag.use(bugsnagReact, React); 19 | 20 | // wrap your entire app tree in the ErrorBoundary provided 21 | const ErrorBoundary = env.bugsnag.getPlugin("react"); 22 | 23 | ReactDOM.render( 24 | 25 | 26 | , 27 | document.getElementById("root") 28 | ); 29 | 30 | try { 31 | // If you want your app to work offline and load faster, you can change 32 | // unregister() to register() below. Note this comes with some pitfalls. 33 | // Learn more about service workers: https://bit.ly/CRA-PWA 34 | serviceWorker.unregister(); 35 | } catch (error) { 36 | env.bugsnag.notify(error, { severity: "info" }); 37 | } 38 | -------------------------------------------------------------------------------- /react-app/src/loginSchemaMapper.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { 3 | LoginCredentialsV0, 4 | LoginV1, 5 | LoginPreviewV0, 6 | LoginPreviewV1, 7 | } from "./types"; 8 | 9 | export class LoginMapper { 10 | public static credentialsToV1(v0: LoginCredentialsV0): LoginV1 { 11 | return { 12 | info: _.pick(v0, ["title", "domain"]), 13 | detail: _.pick(v0, ["note"]), 14 | secret: { 15 | // We should never be dealing with a v0 login that encodes browser_state 16 | rawCredentials: _.pick(v0, ["username", "password"]), 17 | }, 18 | }; 19 | } 20 | 21 | public static previewToV1(v0: LoginPreviewV0): LoginPreviewV1 { 22 | return { info: v0 }; 23 | } 24 | 25 | public static loginV1ToV0(v1: LoginV1): LoginCredentialsV0 { 26 | if (v1.secret.browserState) { 27 | throw new Error( 28 | "Can't map browser state v1 down to v0! Shouldn't be necessary!" 29 | ); 30 | } 31 | 32 | return { 33 | ...v1.info, 34 | ...v1.secret.rawCredentials!, 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /react-app/src/pages/ChangePasswordPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import * as _ from "lodash"; 3 | import { ChangePassword } from "../components/ChangePassword"; 4 | import { LoadedApp } from "../AppContainer"; 5 | import { WithFlash } from "../Flash"; 6 | import { Redirect } from "react-router-dom"; 7 | import { toCreateAccountParams } from "../srp"; 8 | import { AppCrypto } from "../crypto"; 9 | import { useCurrentBrowserEnv } from "../BrowserEnv"; 10 | 11 | export const ChangePasswordPage: React.FC<{ app: LoadedApp }> = ({ app }) => { 12 | const { currentUser } = app.state; 13 | 14 | const [saved, setSaved] = useState(false); 15 | 16 | const browserEnv = useCurrentBrowserEnv(); 17 | 18 | const handleSave = async (newPassword: string) => { 19 | const { params, masterKey: newMasterKey } = await toCreateAccountParams( 20 | currentUser.email, 21 | newPassword 22 | ); 23 | const newAppCrypto = await AppCrypto.fromKeyMaterial(newMasterKey); 24 | const newWrappedPrivkey = await newAppCrypto.wrapPrivateKey( 25 | await currentUser.exportablePrivateKey() 26 | ); 27 | 28 | await app.graphql().UpdatePassword({ 29 | params: { 30 | encryptedPrivateKey: newWrappedPrivkey, 31 | ..._.pick(params, [ 32 | "srpPbkdf2Salt", 33 | "masterKeyPbkdf2Salt", 34 | "srpSalt", 35 | "srpVerifier", 36 | ]), 37 | }, 38 | }); 39 | 40 | setSaved(true); 41 | 42 | await app.signOut(); 43 | browserEnv.afterSignOut(); 44 | }; 45 | 46 | if (!currentUser.forcedPasswordChangeEnabled) return ; 47 | 48 | if (saved) 49 | return ( 50 | 51 | 52 | 53 | ); 54 | 55 | return ; 56 | }; 57 | -------------------------------------------------------------------------------- /react-app/src/pages/EditProfilePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { EditProfile } from "../components/EditProfile"; 3 | import { LoadedApp } from "../AppContainer"; 4 | import { PublicApi } from "../api"; 5 | import clientEnv from "../clientEnv"; 6 | import { WithFlash } from "../Flash"; 7 | import { Redirect } from "react-router-dom"; 8 | 9 | export const EditProfilePage: React.FC<{ app: LoadedApp }> = ({ app }) => { 10 | const { currentUser } = app.state; 11 | const publicApi = new PublicApi(); 12 | 13 | const [saved, setSaved] = useState(false); 14 | 15 | const handleSave = async (changes: { avatar?: Blob; username?: string }) => { 16 | let avatarUrl: null | string = null; 17 | if (changes.avatar) { 18 | const outcome = await publicApi.getAvatarUploadUrl(); 19 | 20 | if (outcome.kind !== "Success") { 21 | clientEnv.bugsnag.notify("Error creating avatar upload url!"); 22 | return; 23 | } 24 | 25 | avatarUrl = outcome.data.avatarUrl; 26 | 27 | await fetch(outcome.data.uploadUrl, { 28 | method: "PUT", 29 | body: changes.avatar, 30 | }); 31 | } 32 | 33 | await app 34 | .api() 35 | .updateAccount({ avatarUrl, username: changes.username || null }); 36 | 37 | app.reloadMe(); 38 | 39 | setSaved(true); 40 | 41 | return true; 42 | }; 43 | 44 | if (saved) 45 | return ( 46 | 47 | 48 | 49 | ); 50 | 51 | return ( 52 | 57 | publicApi.isUsernameAvailable(username) 58 | } 59 | onSave={handleSave} 60 | /> 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /react-app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /react-app/src/routing.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | RouteProps, 7 | RouteChildrenProps, 8 | Link, 9 | } from "react-router-dom"; 10 | import _ from "lodash"; 11 | import { Provider, Subscribe } from "unstated"; 12 | import { AppContainer } from "./AppContainer"; 13 | import { LoadingPage } from "./components/LoadingAnim"; 14 | import { Expand } from "./generics"; 15 | 16 | interface GuestRouteChildrenProps extends RouteChildrenProps { 17 | app: AppContainer; 18 | } 19 | 20 | type TGuestRouteProps = Expand< 21 | Omit & { 22 | userView: JSX.Element; 23 | children: 24 | | ((props: GuestRouteChildrenProps) => React.ReactNode) 25 | | React.ReactNode; 26 | app: AppContainer; 27 | } 28 | >; 29 | 30 | export const GuestRoute: React.FC = ({ 31 | children, 32 | userView, 33 | app, 34 | ...rest 35 | }) => ( 36 | { 39 | if (app.state.loading) return ; 40 | 41 | if (app.state.kind === "app/unauthenticated") { 42 | return _.isFunction(children) 43 | ? children({ ...routeProps, app }) 44 | : children; 45 | } else { 46 | return userView; 47 | } 48 | }} 49 | > 50 | ); 51 | -------------------------------------------------------------------------------- /react-app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom/extend-expect"; 6 | -------------------------------------------------------------------------------- /react-app/src/types/serializer.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | import { LoginV1, Convert } from "."; 4 | 5 | export class Serializer { 6 | private static optionalKeys = [ 7 | "detail.note", 8 | "secret.browserState", 9 | "secret.rawCredentials", 10 | ]; 11 | 12 | static loginV1ToJson(value: LoginV1) { 13 | this.assertDomainRequiredForBrowserState(value); 14 | 15 | return Convert.loginV1ToJson( 16 | this.enforceOptionalKeys(value, this.optionalKeys) 17 | ); 18 | } 19 | 20 | static toLoginV1(json: string) { 21 | const login = this.removeKeysForUndefinedPaths( 22 | Convert.toLoginV1(json), 23 | this.optionalKeys 24 | ); 25 | 26 | this.assertDomainRequiredForBrowserState(login); 27 | 28 | return login; 29 | } 30 | 31 | private static assertDomainRequiredForBrowserState(value: LoginV1) { 32 | if (value.secret.browserState && !_.isString(value.info.domain)) { 33 | throw new Error( 34 | "Invariant violated! info.domain should be specified for browserState logins" 35 | ); 36 | } 37 | } 38 | 39 | private static enforceOptionalKeys( 40 | object: T, 41 | paths: string[] 42 | ): T { 43 | for (let i = 0; i < paths.length; i++) { 44 | if (_.has(object, paths[i]) && _.isUndefined(_.get(object, paths[i]))) { 45 | throw new Error( 46 | `Found undefined value at ${paths[i]}. Expected omitted key` 47 | ); 48 | } 49 | } 50 | 51 | return object; 52 | } 53 | 54 | private static removeKeysForUndefinedPaths( 55 | object: T, 56 | paths: string[] 57 | ): T { 58 | const undefinedPaths = _.filter( 59 | paths, 60 | (path) => _.has(object, path) && _.isUndefined(_.get(object, path)) 61 | ); 62 | 63 | return _.omit(object, undefinedPaths) as T; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /react-app/src/useKeyPress.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | // Taken from: https://usehooks.com/useKeyPress/ 4 | export function useKeyPress( 5 | targetKey: string, 6 | options = { preventDefault: false } 7 | ) { 8 | // State for keeping track of whether key is pressed 9 | const [keyPressed, setKeyPressed] = useState(false); 10 | 11 | // If pressed key is our target key then set to true 12 | function downHandler(event: KeyboardEvent) { 13 | if (event.key === targetKey) { 14 | if (options.preventDefault) { 15 | event.preventDefault(); 16 | } 17 | setKeyPressed(true); 18 | } 19 | } 20 | 21 | // If released key is our target key then set to false 22 | const upHandler = (event: KeyboardEvent) => { 23 | if (event.key === targetKey) { 24 | if (options.preventDefault) { 25 | event.preventDefault(); 26 | } 27 | setKeyPressed(false); 28 | } 29 | }; 30 | 31 | // Add event listeners 32 | useEffect(() => { 33 | window.addEventListener("keydown", downHandler); 34 | window.addEventListener("keyup", upHandler); 35 | // Remove event listeners on cleanup 36 | return () => { 37 | window.removeEventListener("keydown", downHandler); 38 | window.removeEventListener("keyup", upHandler); 39 | }; 40 | }, []); // Empty array ensures that effect is only run on mount and unmount 41 | 42 | return keyPressed; 43 | } 44 | -------------------------------------------------------------------------------- /react-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext", "es2020.string"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react", 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | "noErrorTruncation": true, 20 | "typeRoots": [ 21 | "./typings", 22 | "./node_modules/@types", 23 | "./../node_modules/@types" 24 | ] 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /react-app/typings/rebass__forms/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@rebass/forms/styled-components" { 2 | export * from "@rebass/forms"; 3 | } 4 | --------------------------------------------------------------------------------