├── .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 |
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 |
7 |
--------------------------------------------------------------------------------
/backend/public/landing/img/jam.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
5 |
--------------------------------------------------------------------------------
/react-app/src/img/approved.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/react-app/src/img/back-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/react-app/src/img/back-button.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/react-app/src/img/bell.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/react-app/src/img/broken-heart.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/react-app/src/img/camera-icon-hover.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/react-app/src/img/camera-icon.svg:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/react-app/src/img/close-button.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/react-app/src/img/copy-done-button.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/react-app/src/img/copy-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/react-app/src/img/copy-link-button.svg:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/react-app/src/img/email-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/react-app/src/img/griflan:
--------------------------------------------------------------------------------
1 | ./../../../backend/public/landing/img/
--------------------------------------------------------------------------------
/react-app/src/img/hamburger.svg:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/react-app/src/img/invalid.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/react-app/src/img/invite-friend.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/react-app/src/img/invite-present-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/react-app/src/img/invite-via-email.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/react-app/src/img/invite-via-sms.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/react-app/src/img/login-complete-checkmark.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/react-app/src/img/magnifyingGlass.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------