├── helpers
├── email
│ ├── index.js
│ ├── templates.js
│ └── sendEmail.js
├── google
│ ├── maps.js
│ └── getTzAndLoc.js
├── mixpanel.js
├── privacy
│ └── showLocationPolicy.js
├── analytics
│ ├── types.js
│ └── index.js
├── rest
│ ├── index.js
│ └── twivatar.js
├── auth
│ ├── session.js
│ ├── anonymous.js
│ └── jwt.js
├── gravatar.js
├── errors.js
└── github.js
├── resolvers
├── index.js
├── unpinUser.js
├── removeManualPlace.js
├── unfollow.js
├── removeManualPerson.js
├── followUser.js
├── manualPlace.js
├── manualPerson.js
├── pinUser.js
├── unpinManualPlace.js
├── unpinManualPerson.js
├── updateTimezone.js
├── deleteAccount.js
├── updateLocationAndTimezone.js
├── shared
│ └── countPinned.js
├── countryFlagEmoji.js
├── updateLocationAndTimezoneForUser.js
├── pinManualPerson.js
├── pinManualPlace.js
├── updateUser.js
├── placesAutoComplete.js
├── addManualPlace.js
├── countryFlagIcon.js
├── updateManualPerson.js
├── updateManualPlace.js
├── addManualPerson.js
├── allUsersByName.js
└── sortFollowings.js
├── static
└── assets
│ ├── favicon.png
│ └── country-flags
│ ├── JP.svg
│ ├── BD.svg
│ ├── PW.svg
│ ├── ID.svg
│ ├── MC.svg
│ ├── UA.svg
│ ├── PL.svg
│ ├── SO.svg
│ ├── VN.svg
│ ├── YE.svg
│ ├── CH.svg
│ ├── FR.svg
│ ├── IE.svg
│ ├── IT.svg
│ ├── LU.svg
│ ├── NL.svg
│ ├── EE.svg
│ ├── LV.svg
│ ├── NG.svg
│ ├── PE.svg
│ ├── MG.svg
│ ├── BH.svg
│ ├── CO.svg
│ ├── CZ.svg
│ ├── MT.svg
│ ├── RU.svg
│ ├── BJ.svg
│ ├── FI.svg
│ ├── GL.svg
│ ├── BF.svg
│ ├── BE.svg
│ ├── QA.svg
│ ├── AE.svg
│ ├── DE.svg
│ ├── HU.svg
│ ├── ML.svg
│ ├── SL.svg
│ ├── AM.svg
│ ├── BG.svg
│ ├── BO.svg
│ ├── CI.svg
│ ├── GA.svg
│ ├── IM.svg
│ ├── LT.svg
│ ├── TD.svg
│ ├── TO.svg
│ ├── LB.svg
│ ├── PS.svg
│ ├── ES.svg
│ ├── GN.svg
│ ├── AT.svg
│ ├── CL.svg
│ ├── GM.svg
│ ├── MV.svg
│ ├── BS.svg
│ ├── CR.svg
│ ├── TH.svg
│ ├── RO.svg
│ ├── CG.svg
│ ├── NE.svg
│ ├── MR.svg
│ ├── LA.svg
│ ├── TL.svg
│ ├── JM.svg
│ ├── MU.svg
│ ├── KW.svg
│ ├── SD.svg
│ ├── LI.svg
│ ├── TZ.svg
│ ├── LS.svg
│ ├── SI.svg
│ ├── TR.svg
│ ├── CN.svg
│ ├── DK.svg
│ ├── GH.svg
│ ├── IN.svg
│ ├── MW.svg
│ ├── SE.svg
│ ├── MA.svg
│ ├── NF.svg
│ ├── MM.svg
│ ├── AR.svg
│ ├── CM.svg
│ ├── DJ.svg
│ ├── EG.svg
│ ├── CA.svg
│ ├── PA.svg
│ ├── SN.svg
│ ├── TN.svg
│ ├── SC.svg
│ ├── BR.svg
│ ├── PT.svg
│ ├── SY.svg
│ ├── CX.svg
│ ├── HT.svg
│ ├── NR.svg
│ ├── JO.svg
│ ├── ZM.svg
│ ├── BW.svg
│ ├── GW.svg
│ ├── SR.svg
│ ├── DZ.svg
│ ├── AL.svg
│ ├── GG.svg
│ ├── KP.svg
│ ├── VC.svg
│ ├── TG.svg
│ ├── GR.svg
│ ├── KH.svg
│ ├── RW.svg
│ ├── MX.svg
│ ├── PR.svg
│ ├── TT.svg
│ ├── KZ.svg
│ ├── AW.svg
│ ├── OM.svg
│ ├── PY.svg
│ ├── PF.svg
│ ├── ST.svg
│ ├── AG.svg
│ ├── FM.svg
│ ├── JE.svg
│ ├── TW.svg
│ ├── BB.svg
│ ├── SV.svg
│ ├── MH.svg
│ ├── AF.svg
│ ├── FO.svg
│ ├── IS.svg
│ ├── NI.svg
│ ├── NO.svg
│ └── BZ.svg
├── .gitignore
├── .npmignore
├── index.js
├── utils
├── config.js
├── normalizeById.js
└── randomPhrase.js
├── schema
└── index.js
└── now.json
/helpers/email/index.js:
--------------------------------------------------------------------------------
1 | export * from './templates'
2 |
--------------------------------------------------------------------------------
/resolvers/index.js:
--------------------------------------------------------------------------------
1 | import resolvers from './resolvers'
2 | export default resolvers
3 |
--------------------------------------------------------------------------------
/static/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/therehq/there-server/HEAD/static/assets/favicon.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
3 | *.log
4 |
5 | # Secrets
6 | .env
7 | now-secrets.json
8 | secrets/
9 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
3 | *.log
4 |
5 | # Secrets
6 | .env
7 | now-secrets.json
8 | # secrets/
9 |
--------------------------------------------------------------------------------
/helpers/google/maps.js:
--------------------------------------------------------------------------------
1 | export const client = require('@google/maps').createClient({
2 | key: process.env.GOOGLE_API_KEY,
3 | Promise: global.Promise,
4 | })
5 |
--------------------------------------------------------------------------------
/resolvers/unpinUser.js:
--------------------------------------------------------------------------------
1 | export default async (obj, args, ctx, info) => {
2 | // Unpin
3 | await ctx.user.removePinnedUsers(args.userId)
4 | return { userId: args.userId }
5 | }
6 |
--------------------------------------------------------------------------------
/helpers/mixpanel.js:
--------------------------------------------------------------------------------
1 | import Mixpanel from 'mixpanel'
2 |
3 | // Utilities
4 | import config from '../utils/config'
5 |
6 | export const mixpanel = Mixpanel.init(config.mixpanelProjectToken)
7 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | // Set options as a parameter, environment variable, or rc file.
3 | require = require('esm')(module /*, options*/)
4 | module.exports = require('./main.js')
5 |
--------------------------------------------------------------------------------
/helpers/privacy/showLocationPolicy.js:
--------------------------------------------------------------------------------
1 | const ALWAYS = 'always'
2 | const NEVER = 'never'
3 |
4 | export const types = {
5 | ALWAYS,
6 | NEVER,
7 | }
8 |
9 | export const allTypes = [ALWAYS, NEVER]
10 |
--------------------------------------------------------------------------------
/helpers/analytics/types.js:
--------------------------------------------------------------------------------
1 | export const eventTypes = {
2 | OPEN: 'open',
3 | PING: 'ping',
4 | TRAY_CLICK: 'tray-click',
5 | QUIT: 'quit',
6 | }
7 |
8 | export const allEventTypes = Object.values(eventTypes)
9 |
--------------------------------------------------------------------------------
/resolvers/removeManualPlace.js:
--------------------------------------------------------------------------------
1 | import { User } from '../models'
2 | import followingList from './followingList'
3 |
4 | export default async (obj, { id }, ctx, info) => {
5 | // Remove place
6 | await ctx.user.removeManualPlace(id)
7 |
8 | return { id }
9 | }
10 |
--------------------------------------------------------------------------------
/resolvers/unfollow.js:
--------------------------------------------------------------------------------
1 | import { User } from '../models'
2 | import followingList from './followingList'
3 |
4 | export default async (obj, { userId }, ctx, info) => {
5 | // Unfollow
6 | await ctx.user.removeFollowing(userId)
7 |
8 | return { id: userId }
9 | }
10 |
--------------------------------------------------------------------------------
/resolvers/removeManualPerson.js:
--------------------------------------------------------------------------------
1 | import { User } from '../models'
2 | import followingList from './followingList'
3 |
4 | export default async (obj, { id }, ctx, info) => {
5 | // Remove person
6 | await ctx.user.removeManualPerson(id)
7 |
8 | return { id }
9 | }
10 |
--------------------------------------------------------------------------------
/resolvers/followUser.js:
--------------------------------------------------------------------------------
1 | import followingList from './followingList'
2 |
3 | export default async (obj, args, ctx, info) => {
4 | // Follow
5 | await ctx.user.addFollowing(args.userId)
6 | const followedUser = await ctx.models.User.findById(args.userId)
7 |
8 | return followedUser
9 | }
10 |
--------------------------------------------------------------------------------
/resolvers/manualPlace.js:
--------------------------------------------------------------------------------
1 | import { ManualPlace } from '../models'
2 |
3 | export default async (obj, args, ctx) => {
4 | if (!args.id) {
5 | throw new Error('No place ID was sent.')
6 | }
7 |
8 | const place = await ManualPlace.findById(args.id)
9 |
10 | if (!place) {
11 | throw new Error('No place matched the ID.')
12 | }
13 |
14 | return place.get({ plain: true })
15 | }
16 |
--------------------------------------------------------------------------------
/resolvers/manualPerson.js:
--------------------------------------------------------------------------------
1 | import { ManualPerson } from '../models'
2 |
3 | export default async (obj, args, ctx) => {
4 | if (!args.id) {
5 | throw new Error('No Person ID was sent.')
6 | }
7 |
8 | const person = await ManualPerson.findById(args.id)
9 |
10 | if (!person) {
11 | throw new Error('No person matched the ID.')
12 | }
13 |
14 | return person.get({ plain: true })
15 | }
16 |
--------------------------------------------------------------------------------
/helpers/email/templates.js:
--------------------------------------------------------------------------------
1 | import sendEmail from './sendEmail'
2 |
3 | export const EMAIL_VERIFICATION = '6591781'
4 | export const sendEmailVerification = (
5 | To,
6 | { securityCode, callbackUrl, firstName },
7 | ) =>
8 | sendEmail({
9 | To,
10 | TemplateId: EMAIL_VERIFICATION,
11 | TemplateModel: {
12 | securityCode,
13 | callbackUrl,
14 | firstName: firstName || '',
15 | },
16 | })
17 |
--------------------------------------------------------------------------------
/utils/config.js:
--------------------------------------------------------------------------------
1 | const { NOW } = process.env
2 | const host = NOW ? 'https://apiv1.there.team' : 'http://localhost:9900'
3 | const mixpanelProjectToken = NOW
4 | ? 'e7859c5640d175b8f34d425735fba85e' // PROD
5 | : '31a53a5d9fb1a091846b5abffac684e7' // DEV
6 |
7 | export default {
8 | apiUrl: host,
9 | mixpanelProjectToken,
10 |
11 | // users
12 | maxPinned: 4,
13 |
14 | // flags
15 | countryFlagsHash: 'EkD3d',
16 | }
17 |
--------------------------------------------------------------------------------
/resolvers/pinUser.js:
--------------------------------------------------------------------------------
1 | import config from '../utils/config'
2 | import { countPinned } from './shared/countPinned'
3 |
4 | export default async (obj, args, ctx, info) => {
5 | const pinnedCount = await countPinned(ctx.user)
6 |
7 | if (pinnedCount >= config.maxPinned) {
8 | return new Error('Max pinned limit reached')
9 | }
10 |
11 | // Pin
12 | await ctx.user.addPinnedUsers(args.userId)
13 | return { userId: args.userId }
14 | }
15 |
--------------------------------------------------------------------------------
/utils/normalizeById.js:
--------------------------------------------------------------------------------
1 | export default arrayOfObjectsWithId => {
2 | if (
3 | typeof arrayOfObjectsWithId !== 'object' ||
4 | !arrayOfObjectsWithId.length
5 | ) {
6 | throw new Error(`It can only normalize arrays.`)
7 | }
8 |
9 | let byId = {}
10 | let ids = []
11 |
12 | for (let { id, ...otherProps } of arrayOfObjectsWithId) {
13 | byId[id] = otherProps
14 | ids.push(id)
15 | }
16 |
17 | return { byId, ids }
18 | }
19 |
--------------------------------------------------------------------------------
/helpers/rest/index.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express'
2 |
3 | // Controllers
4 | import uploadManualPhoto from './uploadManualPhoto'
5 | import twivatar from './twivatar'
6 |
7 | const router = Router()
8 | export default router
9 |
10 | // API Intro!
11 | router.get('/', (req, res) => {
12 | res.send('REST API for There! 😴 REST?')
13 | })
14 |
15 | router.post('/upload-manual-photo', ...uploadManualPhoto())
16 | router.get('/twivatar/:user', twivatar)
17 |
--------------------------------------------------------------------------------
/resolvers/unpinManualPlace.js:
--------------------------------------------------------------------------------
1 | import { ManualPlace } from '../models'
2 |
3 | export default async (obj, { id }, ctx) => {
4 | const [affected] = await ManualPlace.update(
5 | { pinned: false, pinnedAt: null },
6 | { where: { id } },
7 | )
8 |
9 | if (affected === 1) {
10 | // Later if we had a better caching in the app,
11 | // we should return the updated row
12 | return { pinned: false }
13 | }
14 |
15 | // Nothing affected
16 | return null
17 | }
18 |
--------------------------------------------------------------------------------
/resolvers/unpinManualPerson.js:
--------------------------------------------------------------------------------
1 | import { ManualPerson } from '../models'
2 |
3 | export default async (obj, { id }, ctx) => {
4 | const [affected] = await ManualPerson.update(
5 | { pinned: false, pinnedAt: null },
6 | { where: { id } },
7 | )
8 |
9 | if (affected === 1) {
10 | // Later if we had a better caching in the app,
11 | // we should return the updated row
12 | return { pinned: false }
13 | }
14 |
15 | // Nothing affected
16 | return null
17 | }
18 |
--------------------------------------------------------------------------------
/helpers/analytics/index.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express'
2 |
3 | const router = Router()
4 |
5 | // API Intro!
6 | router.get('/', (req, res) => {
7 | res.send('Analytics API for There! 🔄 / ⚠️ Deprecated')
8 | })
9 |
10 | // ⚠️ MAINTAIN Mode - using mixpanel instead
11 | // Submit events from the app
12 | router.post('/event', async (req, res) => {
13 | // Send the response
14 | res.json({ ok: true, analyticsEvent: {}, message: '⚠️ Deprecated' })
15 | })
16 |
17 | export default router
18 |
--------------------------------------------------------------------------------
/helpers/auth/session.js:
--------------------------------------------------------------------------------
1 | import { User } from '../../models'
2 |
3 | export const serializeUser = (user, done) => {
4 | done(null, user.id)
5 | }
6 |
7 | export const deserializeUser = (id, done) => {
8 | if (!id) {
9 | done(null, {})
10 | return
11 | }
12 |
13 | User.findById(id)
14 | .then(user => {
15 | if (user) {
16 | done(null, user.get())
17 | } else {
18 | done(null, {})
19 | }
20 | })
21 | .catch(error => done(error, {}))
22 | }
23 |
--------------------------------------------------------------------------------
/resolvers/updateTimezone.js:
--------------------------------------------------------------------------------
1 | export default async (obj, args, ctx) => {
2 | // Check if email is already registered
3 | const [affected] = await ctx.models.User.update(
4 | { timezone: args.timezone, city: '', fullLocation: '' },
5 | {
6 | where: { id: ctx.userId },
7 | },
8 | )
9 |
10 | // Return user
11 | if (affected === 1) {
12 | const user = await ctx.models.User.findById(ctx.userId)
13 | return user.dataValues
14 | }
15 |
16 | // Nothing affected
17 | return null
18 | }
19 |
--------------------------------------------------------------------------------
/resolvers/deleteAccount.js:
--------------------------------------------------------------------------------
1 | import { mixpanel } from '../helpers/mixpanel'
2 |
3 | export default async (obj, args, ctx, info) => {
4 | const userId = ctx.userId
5 |
6 | if (!ctx.user) {
7 | return new Error('No user')
8 | }
9 |
10 | try {
11 | mixpanel.track('Delete Account', {
12 | distinct_id: userId,
13 | userId,
14 | })
15 | } catch (err) {
16 | console.log(err)
17 | }
18 |
19 | // Delete user and its data
20 | await ctx.user.destroy({ force: true })
21 | return true
22 | }
23 |
--------------------------------------------------------------------------------
/resolvers/updateLocationAndTimezone.js:
--------------------------------------------------------------------------------
1 | import getTimezoneAndLocation from '../helpers/google/getTzAndLoc'
2 |
3 | export default async (obj, { placeId }, ctx) => {
4 | const { city, fullLocation, timezone } = await getTimezoneAndLocation(placeId)
5 |
6 | // Check for signed in user
7 | if (!ctx.user) {
8 | throw new Error('Unauthorized')
9 | }
10 |
11 | // Update user with data!
12 | const res = await ctx.user.update({
13 | city,
14 | fullLocation,
15 | timezone,
16 | })
17 |
18 | return res.get({ plain: true })
19 | }
20 |
--------------------------------------------------------------------------------
/helpers/gravatar.js:
--------------------------------------------------------------------------------
1 | import qs from 'qs'
2 | import crypto from 'crypto'
3 |
4 | const BASE_URL = 'https://gravatar.com/avatar/'
5 |
6 | export const getGravatarUrl = (email, opts) => {
7 | if (email.indexOf('@') === -1) {
8 | throw new Error('Please specify an email')
9 | }
10 |
11 | const query = qs.stringify(opts)
12 | const neatEmail = email.toLowerCase().trim()
13 | const hash = crypto
14 | .createHash('md5')
15 | .update(neatEmail)
16 | .digest('hex')
17 |
18 | return BASE_URL + `${hash}.jpg` + (query ? `?${query}` : '')
19 | }
20 |
--------------------------------------------------------------------------------
/resolvers/shared/countPinned.js:
--------------------------------------------------------------------------------
1 | import { ManualPerson, ManualPlace } from '../../models'
2 |
3 | export const countPinned = async user => {
4 | const pinnedManualPeople = () =>
5 | user.countManualPeople({ where: { pinned: true } })
6 | const pinnedManualPlaces = () =>
7 | user.countManualPlaces({ where: { pinned: true } })
8 | const pinnedUsers = () => user.countPinnedUsers()
9 |
10 | const counts = await Promise.all([
11 | pinnedManualPeople(),
12 | pinnedManualPlaces(),
13 | pinnedUsers(),
14 | ])
15 |
16 | return counts.reduce((sum, c) => sum + c, 0)
17 | }
18 |
--------------------------------------------------------------------------------
/schema/index.js:
--------------------------------------------------------------------------------
1 | import { makeExecutableSchema } from 'graphql-tools'
2 |
3 | import resolvers from '../resolvers'
4 | import typeDefs from './typeDefs'
5 | import * as OrmModels from '../models'
6 | import { asyncErrorHandler } from '../helpers/errors'
7 |
8 | export const schema = makeExecutableSchema({
9 | typeDefs,
10 | resolvers,
11 | })
12 |
13 | export const models = OrmModels
14 |
15 | // Fetch current logged in user a single time,
16 | // so we don't refetch it in every resolver
17 | export const getUser = asyncErrorHandler(
18 | async userId => await OrmModels.User.findById(userId),
19 | )
20 |
--------------------------------------------------------------------------------
/resolvers/countryFlagEmoji.js:
--------------------------------------------------------------------------------
1 | import { flag as getFlagEmoji } from 'country-emoji'
2 | import config from '../utils/config'
3 |
4 | export default (obj, args, ctx) => {
5 | const { fullLocation } = obj
6 |
7 | if (!fullLocation) {
8 | return null
9 | }
10 |
11 | let locationParts
12 | if (fullLocation.includes(',')) {
13 | locationParts = fullLocation.split(',')
14 | } else if (fullLocation.includes('-')) {
15 | locationParts = fullLocation.split('-')
16 | } else {
17 | locationParts = fullLocation
18 | }
19 |
20 | const countryName = locationParts[locationParts.length - 1]
21 |
22 | return getFlagEmoji(countryName)
23 | }
24 |
--------------------------------------------------------------------------------
/resolvers/updateLocationAndTimezoneForUser.js:
--------------------------------------------------------------------------------
1 | import { User } from '../models'
2 | import getTimezoneAndLocation from '../helpers/google/getTzAndLoc'
3 |
4 | export default async (obj, { placeId, userId }, ctx) => {
5 | const { city, fullLocation, timezone } = await getTimezoneAndLocation(placeId)
6 |
7 | // Check for signed in user
8 | if (!userId || !placeId) {
9 | throw new Error('Data missing')
10 | }
11 |
12 | // Update user with data!
13 | await User.update(
14 | {
15 | city,
16 | fullLocation,
17 | timezone,
18 | },
19 | { where: { id: userId } },
20 | )
21 |
22 | const res = await User.findById(userId)
23 |
24 | return res.get({ plain: true })
25 | }
26 |
--------------------------------------------------------------------------------
/resolvers/pinManualPerson.js:
--------------------------------------------------------------------------------
1 | import { ManualPerson } from '../models'
2 | import config from '../utils/config'
3 | import { countPinned } from './shared/countPinned'
4 |
5 | export default async (obj, { id }, ctx) => {
6 | const pinnedCount = await countPinned(ctx.user)
7 |
8 | if (pinnedCount >= config.maxPinned) {
9 | return new Error('Max pinned limit reached')
10 | }
11 |
12 | const [affected] = await ManualPerson.update(
13 | { pinned: true, pinnedAt: Date.now() },
14 | { where: { id } },
15 | )
16 |
17 | if (affected === 1) {
18 | // Later if we had a better caching in the app,
19 | // we should return the updated row
20 | return { pinned: true }
21 | }
22 |
23 | // Nothing affected
24 | return null
25 | }
26 |
--------------------------------------------------------------------------------
/resolvers/pinManualPlace.js:
--------------------------------------------------------------------------------
1 | import { ManualPlace } from '../models'
2 | import { countPinned } from './shared/countPinned'
3 | import config from '../utils/config'
4 |
5 | export default async (obj, { id }, ctx) => {
6 | const pinnedCount = await countPinned(ctx.user)
7 |
8 | if (pinnedCount >= config.maxPinned) {
9 | return new Error('Max pinned limit reached')
10 | }
11 |
12 | const [affected] = await ManualPlace.update(
13 | { pinned: true, pinnedAt: Date.now() },
14 | { where: { id } },
15 | )
16 |
17 | if (affected === 1) {
18 | // Later if we had a better caching in the app,
19 | // we should return the updated row
20 | return { pinned: true }
21 | }
22 |
23 | // Nothing affected
24 | return null
25 | }
26 |
--------------------------------------------------------------------------------
/resolvers/updateUser.js:
--------------------------------------------------------------------------------
1 | export default async (obj, args, ctx) => {
2 | // Check if email is already registered
3 | if (args.email) {
4 | const userWithEmail = await ctx.models.User.findOne({
5 | where: { email: args.email },
6 | })
7 | if (userWithEmail) {
8 | const registeredEmail = args.email
9 | delete args.email
10 | return new Error(`${registeredEmail} is registered. Sign in by email!`)
11 | }
12 | }
13 |
14 | const [affected] = await ctx.models.User.update(args, {
15 | where: { id: ctx.userId },
16 | })
17 | // Return user
18 | if (affected === 1) {
19 | const user = await ctx.models.User.findById(ctx.userId)
20 | return user.dataValues
21 | }
22 | // Nothing affected
23 | return {}
24 | }
25 |
--------------------------------------------------------------------------------
/static/assets/country-flags/JP.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
38 |
--------------------------------------------------------------------------------
/static/assets/country-flags/BD.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
38 |
--------------------------------------------------------------------------------
/static/assets/country-flags/PW.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
38 |
--------------------------------------------------------------------------------
/resolvers/placesAutoComplete.js:
--------------------------------------------------------------------------------
1 | import Raven from 'raven'
2 |
3 | export default async (obj, args, ctx) => {
4 | try {
5 | const googleMaps = require('@google/maps').createClient({
6 | key: process.env.GOOGLE_API_KEY,
7 | Promise: global.Promise,
8 | })
9 |
10 | const { json: { predictions } } = await googleMaps
11 | .placesAutoComplete({
12 | input: args.query,
13 | language: 'en',
14 | types: '(cities)',
15 | })
16 | .asPromise()
17 |
18 | if (typeof predictions !== 'undefined' && predictions.length > 0) {
19 | return predictions.map(({ description, place_id }) => {
20 | return { description, placeId: place_id }
21 | })
22 | } else {
23 | return []
24 | }
25 | } catch (e) {
26 | Raven.captureException(e)
27 | return []
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/static/assets/country-flags/ID.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
38 |
--------------------------------------------------------------------------------
/static/assets/country-flags/MC.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
38 |
--------------------------------------------------------------------------------
/static/assets/country-flags/UA.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
38 |
--------------------------------------------------------------------------------
/static/assets/country-flags/PL.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
38 |
--------------------------------------------------------------------------------
/helpers/google/getTzAndLoc.js:
--------------------------------------------------------------------------------
1 | import { client as mapsClient } from './maps'
2 |
3 | export default async placeId => {
4 | if (!placeId) {
5 | return {}
6 | }
7 |
8 | const {
9 | json: { result },
10 | } = await mapsClient
11 | .place({
12 | placeid: placeId,
13 | language: 'en',
14 | })
15 | .asPromise()
16 |
17 | // Extract and rename information for saving in database
18 | const {
19 | name: city,
20 | formatted_address: fullLocation,
21 | geometry: {
22 | location: { lat, lng },
23 | },
24 | } = result
25 |
26 | // Fetch timezone string based on geometry (Lat/Lng)
27 | const {
28 | json: { timeZoneId: timezone },
29 | } = await mapsClient
30 | .timezone({
31 | location: [lat, lng],
32 | timestamp: Date.now() / 1000,
33 | })
34 | .asPromise()
35 |
36 | // Return data in correct format
37 | return {
38 | city,
39 | fullLocation,
40 | timezone,
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/static/assets/country-flags/SO.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
--------------------------------------------------------------------------------
/static/assets/country-flags/VN.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
--------------------------------------------------------------------------------
/helpers/auth/anonymous.js:
--------------------------------------------------------------------------------
1 | import v4 from 'uuid/v4'
2 |
3 | // Utilities
4 | import { mixpanel } from '../mixpanel'
5 | import { encodeJwt } from './jwt'
6 |
7 | // Models
8 | import { User } from '../../models'
9 |
10 | export const loginAnonymously = async (req, res, next) => {
11 | try {
12 | // Gen a new userId
13 | const userId = v4()
14 |
15 | const token = encodeJwt(userId)
16 |
17 | // Create user
18 | const user = await User.create({
19 | id: userId,
20 | isAnonymous: true,
21 | })
22 |
23 | // Track sign up
24 | try {
25 | mixpanel.track('Sign Up', {
26 | distinct_id: userId,
27 | method: 'anonymous',
28 | })
29 | } catch (err) {
30 | console.log(err)
31 | }
32 |
33 | // Pass user data
34 | req.user = user.get({ plain: true })
35 |
36 | res.json({ token, user })
37 | // next()
38 | } catch (err) {
39 | console.error(err)
40 | return next(err)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/static/assets/country-flags/YE.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
--------------------------------------------------------------------------------
/static/assets/country-flags/CH.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
--------------------------------------------------------------------------------
/helpers/errors.js:
--------------------------------------------------------------------------------
1 | import Raven from 'raven'
2 |
3 | /**
4 | *
5 | * @param {Function} children
6 | */
7 | export const asyncErrorHandler = children => (...args) => {
8 | try {
9 | return children(...args)
10 | } catch (e) {
11 | Raven.captureException(e)
12 | console.error(e)
13 | return
14 | }
15 | }
16 |
17 | export const capture = Raven.captureException
18 |
19 | export const expressJsonErrorHandler = (err, req, res, next) => {
20 | if (res.headersSent) {
21 | return next(err)
22 | }
23 |
24 | if (!err) {
25 | return next()
26 | }
27 |
28 | const id = Raven.captureException(err)
29 |
30 | res
31 | .status(500)
32 | .send(
33 | `Oops, something went wrong! Our engineers have been alerted and will fix this asap. (Error Id : ${id})
Chat support is online: https://go.crisp.chat/chat/embed/?website_id=bb14ccd2-0869-40e7-b0f1-b520e93db7e1`,
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/static/assets/country-flags/FR.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
--------------------------------------------------------------------------------
/static/assets/country-flags/IE.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
--------------------------------------------------------------------------------
/static/assets/country-flags/IT.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
--------------------------------------------------------------------------------
/static/assets/country-flags/LU.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
--------------------------------------------------------------------------------
/static/assets/country-flags/NL.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "name": "there-server",
4 | "alias": "apiv1.there.team",
5 | "env": {
6 | "GOOGLE_API_KEY": "@there-google-api-key",
7 | "FIREBASE_ADMIN_SDK": "@there-firebase-admin-sdk",
8 | "SENTRY_DSN": "@there-api-sentry-dsn",
9 | "DB_HOST": "@there-main-db-host",
10 | "DB_NAME": "@there-main-db-name",
11 | "DB_USERNAME": "@there-main-db-username",
12 | "DB_PASSWORD": "@there-main-db-password",
13 | "TWITTER_CONSUMER_KEY": "@there-twitter-consumer-key",
14 | "TWITTER_CONSUMER_SECRET": "@there-twitter-consumer-secret",
15 | "SLACK_CLIENT_ID": "@there-slack-client-id",
16 | "SLACK_CLIENT_SECRET": "@there-slack-client-secret",
17 | "SESSION_SECRET": "@there-session-secret",
18 | "JWT_SECRET": "@there-jwt-secret",
19 | "GH_TOKEN_RELEASES": "@there-github-token-releases",
20 | "MAILJET_KEY_PUBLIC": "@there-mailjet-key-public",
21 | "MAILJET_KEY_PRIVATE": "@there-mailjet-key-private",
22 | "POSTMARK_SERVER_KEY": "@there-postmark-server-key"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/static/assets/country-flags/EE.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/LV.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/NG.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/PE.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/MG.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/helpers/email/sendEmail.js:
--------------------------------------------------------------------------------
1 | import postmark from 'postmark'
2 | import fs from 'mz/fs'
3 | import path from 'path'
4 | const debug = require('debug')('send-email')
5 |
6 | const client = new postmark.Client(process.env.POSTMARK_SERVER_KEY)
7 |
8 | const sendEmail = async ({ To, TemplateId, TemplateModel }) => {
9 | return new Promise((res, rej) => {
10 | client.sendEmailWithTemplate(
11 | {
12 | From: 'support@there.team',
13 | To,
14 | TemplateId,
15 | TemplateModel,
16 | },
17 | async err => {
18 | if (err) {
19 | // 406 means the user became inactive, either by having an email
20 | // hard bounce or they marked as spam
21 | if (err.code === 406) {
22 | debug(`Email user is deactive: ${to}`)
23 | }
24 |
25 | console.error('Error sending email:')
26 | console.error(err)
27 | return rej(err)
28 | }
29 | res()
30 | debug(`email to ${To} sent successfully`)
31 | },
32 | )
33 | })
34 | }
35 |
36 | export default sendEmail
37 |
--------------------------------------------------------------------------------
/static/assets/country-flags/BH.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/CO.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/CZ.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/MT.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/RU.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/BJ.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/helpers/rest/twivatar.js:
--------------------------------------------------------------------------------
1 | import cheerio from 'cheerio'
2 | import Debug from 'debug'
3 | import bent from 'bent'
4 | const getString = bent('string')
5 |
6 | const debug = Debug('twivatar')
7 | const oneMonthInSec = 2628000
8 |
9 | const get = username => {
10 | const url = 'https://mobile.twitter.com/' + username
11 | return new Promise((resolve, reject) => {
12 | getString(url)
13 | .then(body => {
14 | const $ = cheerio.load(body)
15 |
16 | resolve(
17 | ($('.avatar img').attr('src') || '').replace('_normal', '_80x80'),
18 | )
19 | })
20 | .catch(reject)
21 | })
22 | }
23 |
24 | export default async (req, res, next) => {
25 | let imageUrl
26 | try {
27 | imageUrl = await get(req.params.user)
28 | } catch (error) {
29 | console.warn(error)
30 | }
31 |
32 | debug(`Fetching Twitter avatar for ${req.params.user}...`)
33 |
34 | if (!imageUrl) {
35 | return res.status(404).send('')
36 | }
37 |
38 | res.setHeader('Cache-Control', `public, max-age=${oneMonthInSec}`)
39 |
40 | const stream = await bent()(imageUrl)
41 | stream.pipe(res)
42 | }
43 |
--------------------------------------------------------------------------------
/resolvers/addManualPlace.js:
--------------------------------------------------------------------------------
1 | import { ManualPlace } from '../models'
2 | import getTzAndLoc from '../helpers/google/getTzAndLoc'
3 | import followingList from './followingList'
4 |
5 | export default async (obj, args, ctx, info) => {
6 | const {
7 | name,
8 | timezone: inputTimezone,
9 | placeId,
10 | photoUrl,
11 | photoCloudObject,
12 | } = args
13 |
14 | // Find timezone and exact location based on placeId
15 | let { city, fullLocation, timezone } = await getTzAndLoc(placeId)
16 |
17 | // Used for UTC
18 | if (!placeId && !city && inputTimezone) {
19 | city = null
20 | fullLocation = null
21 | timezone = inputTimezone
22 | }
23 |
24 | // Create and save place
25 | const savedPlace = await ManualPlace.create({
26 | name,
27 | photoUrl,
28 | photoCloudObject,
29 |
30 | city,
31 | fullLocation,
32 | timezone,
33 | })
34 |
35 | try {
36 | await ctx.user.addManualPlace(savedPlace)
37 | } catch (err) {
38 | Raven.captureException(err)
39 | console.log(err)
40 | return err
41 | }
42 |
43 | return savedPlace.get({ plain: true })
44 | }
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/FI.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/GL.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/BF.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/resolvers/countryFlagIcon.js:
--------------------------------------------------------------------------------
1 | import { code as getCode } from 'country-emoji'
2 | import config from '../utils/config'
3 |
4 | export default (obj, args, ctx) => {
5 | const { fullLocation } = obj
6 |
7 | if (!fullLocation) {
8 | return null
9 | }
10 |
11 | let locationParts
12 | if (fullLocation.includes(',')) {
13 | locationParts = fullLocation.split(',')
14 | } else if (fullLocation.includes('-')) {
15 | locationParts = fullLocation.split('-')
16 | } else {
17 | locationParts = fullLocation
18 | }
19 |
20 | // Check the last part for country name
21 | let countryName = locationParts[locationParts.length - 1]
22 |
23 | let code = getCode(countryName)
24 |
25 | if (!code) {
26 | if (locationParts.length >= 3) {
27 | // Check the second part for country name
28 | countryName = locationParts[locationParts.length - 2]
29 | code = getCode(countryName)
30 |
31 | if (!code) {
32 | return null
33 | }
34 | } else {
35 | return null
36 | }
37 | }
38 |
39 | return `${config.apiUrl}/static/assets/country-flags-${
40 | config.countryFlagsHash
41 | }/${code}.svg`
42 | }
43 |
--------------------------------------------------------------------------------
/resolvers/updateManualPerson.js:
--------------------------------------------------------------------------------
1 | import { ManualPerson } from '../models'
2 |
3 | // Utilities
4 | import getTzAndLoc from '../helpers/google/getTzAndLoc'
5 |
6 | export default async (obj, args, ctx) => {
7 | if (!args.id) {
8 | throw new Error('No ID was specified.')
9 | }
10 |
11 | const { id, placeId, timezone: inputTimezone, ...newValues } = args
12 |
13 | if (Boolean(placeId)) {
14 | // Only update location if it has been changed
15 | const { city, fullLocation, timezone } = await getTzAndLoc(placeId)
16 | // Set location/timezone values to be updated
17 | newValues.city = city
18 | newValues.timezone = timezone
19 | newValues.fullLocation = fullLocation
20 | }
21 |
22 | // UTC
23 | if (Boolean(inputTimezone)) {
24 | newValues.timezone = inputTimezone
25 | }
26 |
27 | const [affected] = await ManualPerson.update(newValues, {
28 | where: { id },
29 | })
30 |
31 | if (affected === 1) {
32 | // We don't fetch from DB here, later if we had a
33 | // better caching in the app, we should return the
34 | // new row
35 | return newValues
36 | }
37 |
38 | // Nothing affected
39 | return null
40 | }
41 |
--------------------------------------------------------------------------------
/resolvers/updateManualPlace.js:
--------------------------------------------------------------------------------
1 | import { ManualPlace } from '../models'
2 |
3 | // Utilities
4 | import getTzAndLoc from '../helpers/google/getTzAndLoc'
5 |
6 | export default async (obj, args, ctx) => {
7 | if (!args.id) {
8 | throw new Error('No ID was specified.')
9 | }
10 |
11 | const { id, placeId, timezone: inputTimezone, ...newValues } = args
12 |
13 | if (Boolean(placeId)) {
14 | // Only update location if it has been changed
15 | const { city, fullLocation, timezone } = await getTzAndLoc(placeId)
16 | // Set location/timezone values to be updated
17 | newValues.city = city
18 | newValues.timezone = timezone
19 | newValues.fullLocation = fullLocation
20 | }
21 |
22 | // UTC
23 | if (Boolean(inputTimezone)) {
24 | newValues.timezone = inputTimezone
25 | }
26 |
27 | const [affected] = await ManualPlace.update(newValues, {
28 | where: { id },
29 | })
30 |
31 | if (affected === 1) {
32 | // We don't fetch from DB here, later if we had a
33 | // better caching in the app, we should return the
34 | // new row
35 | return newValues
36 | }
37 |
38 | // Nothing affected
39 | return null
40 | }
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/BE.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/QA.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/AE.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/DE.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/HU.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/ML.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/SL.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/AM.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/BG.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/BO.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/CI.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/GA.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/IM.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/LT.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/TD.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/TO.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/static/assets/country-flags/LB.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/static/assets/country-flags/PS.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/ES.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/GN.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
--------------------------------------------------------------------------------
/static/assets/country-flags/AT.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/CL.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/GM.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/MV.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/BS.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/static/assets/country-flags/CR.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/static/assets/country-flags/TH.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/resolvers/addManualPerson.js:
--------------------------------------------------------------------------------
1 | import Raven from 'raven'
2 |
3 | import { ManualPerson } from '../models'
4 | import getTzAndLoc from '../helpers/google/getTzAndLoc'
5 | import followingList from './followingList'
6 |
7 | export default async (
8 | obj,
9 | {
10 | firstName,
11 | lastName,
12 | twitterHandle,
13 | placeId,
14 | timezone: inputTimezone,
15 | photoUrl,
16 | photoCloudObject,
17 | },
18 | ctx,
19 | info,
20 | ) => {
21 | // Find timezone and exact location based on placeId
22 | let { city, fullLocation, timezone } = await getTzAndLoc(placeId)
23 |
24 | // For UTC
25 | if (!placeId && !city && inputTimezone) {
26 | city = null
27 | fullLocation = null
28 | timezone = inputTimezone
29 | }
30 | // Create and save place
31 | const savedPerson = await ManualPerson.create({
32 | firstName,
33 | lastName,
34 | twitterHandle,
35 | photoUrl,
36 | photoCloudObject,
37 |
38 | city,
39 | fullLocation,
40 | timezone,
41 | })
42 |
43 | try {
44 | await ctx.user.addManualPerson(savedPerson)
45 | } catch (err) {
46 | Raven.captureException(err)
47 | console.log(err)
48 | return err
49 | }
50 |
51 | return savedPerson.get({ plain: true })
52 | }
53 |
--------------------------------------------------------------------------------
/static/assets/country-flags/RO.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/static/assets/country-flags/CG.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/NE.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/resolvers/allUsersByName.js:
--------------------------------------------------------------------------------
1 | import { flag } from 'country-emoji'
2 | import { Op } from 'sequelize'
3 | import { User } from '../models'
4 |
5 | // Utilities
6 | import { types as locationPolicyTypes } from '../helpers/privacy/showLocationPolicy'
7 |
8 | export default async (obj, args, ctx) => {
9 | const users = await User.findAll({
10 | where: {
11 | [Op.or]: {
12 | fullName: { [Op.like]: `%${args.name.split(' ').join('%')}%` },
13 | twitterHandle: { [Op.like]: `%${args.name.replace('@', '')}%` },
14 | },
15 | },
16 | limit: args.limit || undefined,
17 | })
18 |
19 | return users.map(wrappedUser => {
20 | const user = wrappedUser.get({ plain: true })
21 |
22 | // Add the flag
23 | if (user.showLocationPolicy === locationPolicyTypes.NEVER) {
24 | user.countryFlag = '☁️'
25 | } else if (user.fullLocation) {
26 | const locationParts = user.fullLocation.split(',')
27 | const countryName = locationParts[locationParts.length - 1]
28 | user.countryFlag = flag(countryName)
29 | } else {
30 | user.countryFlag = ''
31 | }
32 |
33 | // Privacy!
34 | delete user.city
35 | delete user.fullLocation
36 | delete user.email
37 | delete user.twitterId
38 | return user
39 | })
40 | }
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/MR.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/static/assets/country-flags/LA.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/TL.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/JM.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/static/assets/country-flags/MU.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/static/assets/country-flags/KW.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/static/assets/country-flags/SD.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/LI.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/TZ.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/static/assets/country-flags/LS.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/SI.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/TR.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/resolvers/sortFollowings.js:
--------------------------------------------------------------------------------
1 | import { FollowingsOrder } from '../models'
2 | import followingList from './followingList'
3 |
4 | export default async (obj, { peopleIds, placesIds }, ctx, info) => {
5 | const values = {}
6 |
7 | if (peopleIds) {
8 | values.peopleIds = JSON.stringify(peopleIds)
9 | }
10 | if (placesIds) {
11 | values.placesIds = JSON.stringify(placesIds)
12 | }
13 |
14 | let newFollowingsOrder
15 |
16 | // Check if we currently have an order record
17 | const currentOrder = await ctx.user.getFollowingsOrder()
18 |
19 | if (currentOrder) {
20 | // Save it before updating, since .update, mutates
21 | // `currentOrder` data!
22 | const futureFollowingsOrder = {
23 | peopleIds: peopleIds || currentOrder.get('peopleIds'),
24 | placesIds: placesIds || currentOrder.get('placesIds'),
25 | }
26 | // Update the current order
27 | await currentOrder.update(values)
28 | // Set reserved data to pass to followingList resolver
29 | newFollowingsOrder = futureFollowingsOrder
30 | } else {
31 | // Create a new order record!
32 | const followingsOrder = FollowingsOrder.build(values)
33 | await ctx.user.setFollowingsOrder(followingsOrder)
34 | newFollowingsOrder = followingsOrder.get({ plain: true })
35 | }
36 |
37 | return await followingList(null, null, ctx, info, newFollowingsOrder)
38 | }
39 |
--------------------------------------------------------------------------------
/static/assets/country-flags/CN.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
--------------------------------------------------------------------------------
/static/assets/country-flags/DK.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
46 |
--------------------------------------------------------------------------------
/static/assets/country-flags/GH.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/IN.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/static/assets/country-flags/MW.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/SE.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
46 |
--------------------------------------------------------------------------------
/static/assets/country-flags/MA.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/NF.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/static/assets/country-flags/MM.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/AR.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/static/assets/country-flags/CM.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/static/assets/country-flags/DJ.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/static/assets/country-flags/EG.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/static/assets/country-flags/CA.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/static/assets/country-flags/PA.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
43 |
--------------------------------------------------------------------------------
/static/assets/country-flags/SN.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/helpers/github.js:
--------------------------------------------------------------------------------
1 | import fetch from 'node-fetch'
2 | import Lru from 'lru-cache'
3 | import ms from 'ms'
4 |
5 | const debug = require('debug')('github')
6 | const { GH_TOKEN_RELEASES } = process.env
7 |
8 | // Cache
9 | const latestReleaseAssetsKey = 'github-latest-release'
10 | const cache = Lru({
11 | max: 10,
12 | maxAge: ms('5m'),
13 | })
14 |
15 | export const getLatestReleaseDlLink = async () => {
16 | const cachedAssets = cache.get(latestReleaseAssetsKey)
17 |
18 | let assets
19 | if (!cachedAssets) {
20 | debug('Fetching latest release...')
21 |
22 | // We should fetch for the first time
23 | const response = await fetch(
24 | 'https://api.github.com/repos/therepm/there-desktop/releases/latest',
25 | { headers: { Authorization: `token ${GH_TOKEN_RELEASES}` } },
26 | )
27 | ;({ assets } = await response.json())
28 | cache.set(latestReleaseAssetsKey, assets)
29 | } else {
30 | debug('Using cached latest release assets')
31 |
32 | // The assets have been cached
33 | assets = cachedAssets
34 | }
35 |
36 | // Check if there is no assets, return an error
37 | if (!assets) {
38 | debug(`No assets were found.`)
39 | throw new Error(
40 | `Sorry, couldn't find the file now. Chat with us on https://there.team`,
41 | )
42 | }
43 |
44 | const asset = assets.find(asset => asset.name.endsWith('-mac.zip'))
45 | return asset.browser_download_url
46 | }
47 |
--------------------------------------------------------------------------------
/static/assets/country-flags/TN.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/SC.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/static/assets/country-flags/BR.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
46 |
--------------------------------------------------------------------------------
/static/assets/country-flags/PT.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/SY.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/CX.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/static/assets/country-flags/HT.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/static/assets/country-flags/NR.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/JO.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/ZM.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/static/assets/country-flags/BW.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/static/assets/country-flags/GW.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
46 |
--------------------------------------------------------------------------------
/static/assets/country-flags/SR.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/static/assets/country-flags/DZ.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/AL.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/static/assets/country-flags/GG.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/static/assets/country-flags/KP.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
46 |
--------------------------------------------------------------------------------
/static/assets/country-flags/VC.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/TG.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/static/assets/country-flags/GR.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/static/assets/country-flags/KH.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/RW.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/helpers/auth/jwt.js:
--------------------------------------------------------------------------------
1 | import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'
2 | import {
3 | encode as jwtSimpleEncode,
4 | decode as jwtSimpleDecode,
5 | } from 'jwt-simple'
6 | import Raven from 'raven'
7 |
8 | const { JWT_SECRET } = process.env
9 |
10 | export const jwtStrategy = new JwtStrategy(
11 | {
12 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
13 | secretOrKey: JWT_SECRET,
14 | ignoreExpiration: true,
15 | passReqToCallback: true,
16 | },
17 | (req, jwtPayload, done) => {
18 | req.userId = jwtPayload.userId
19 | return done(null, { id: jwtPayload.userId })
20 | },
21 | )
22 |
23 | export const encodeJwt = userId => jwtSimpleEncode({ userId }, JWT_SECRET)
24 |
25 | export const parseUserIdIfAuthorized = (req, res, next) => {
26 | const { authorization } = req.headers
27 |
28 | // If request is not authorized, skip!
29 | if (
30 | !authorization ||
31 | !authorization.includes('Bearer') ||
32 | authorization.toLowerCase().includes('null')
33 | ) {
34 | next()
35 | return
36 | }
37 |
38 | try {
39 | const token = authorization.trim().split(' ')[1]
40 |
41 | if (token.split('.').length !== 3) {
42 | // Token is invalid
43 | // https://stackoverflow.com/a/38712298/4726475
44 | next()
45 | }
46 |
47 | const payload = jwtSimpleDecode(token, JWT_SECRET)
48 |
49 | if (payload && payload.userId) {
50 | req.userId = payload.userId
51 | }
52 | } catch (err) {
53 | Raven.captureException(err)
54 | } finally {
55 | next()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/static/assets/country-flags/MX.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/static/assets/country-flags/PR.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/static/assets/country-flags/TT.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/utils/randomPhrase.js:
--------------------------------------------------------------------------------
1 | const firstAdj = [
2 | 'blank',
3 | 'fewer',
4 | 'kind',
5 | 'one',
6 | 'lovely',
7 | 'Popular',
8 | 'Damanded',
9 | 'final',
10 | 'useful',
11 | 'rubbed',
12 | 'plenty',
13 | 'huge',
14 | 'fine',
15 | 'nice',
16 | 'free',
17 | 'friendly',
18 | 'cool',
19 | 'Peaceful',
20 | ]
21 |
22 | const secondAdj = [
23 | 'blank',
24 | 'calm',
25 | 'safe',
26 | 'private',
27 | 'red',
28 | 'pink',
29 | 'blue',
30 | 'missing',
31 | 'national',
32 | 'perfect',
33 | 'nighty',
34 | 'loose',
35 | 'tight',
36 | 'baggy',
37 | 'huge',
38 | 'flat',
39 | 'fast',
40 | 'mysterious',
41 | ]
42 |
43 | const names = [
44 | 'boat',
45 | 'birds',
46 | 'bridge',
47 | 'building',
48 | 'cake',
49 | 'cheese',
50 | 'clock',
51 | 'classroom',
52 | 'flower',
53 | 'freedom',
54 | 'war',
55 | 'wealth',
56 | 'wall',
57 | 'wolf',
58 | 'poem',
59 | 'object',
60 | 'ocean',
61 | 'muscle',
62 | 'mirror',
63 | 'nose',
64 | 'nuts',
65 | 'month',
66 | 'police',
67 | 'shadow',
68 | 'stone',
69 | 'stage',
70 | 'corn',
71 | 'feathers',
72 | 'environment',
73 | 'iron',
74 | 'lamp',
75 | 'land',
76 | ]
77 |
78 | export const generateRandomPhrase = () => {
79 | const phraseWords = [firstAdj, secondAdj, names].map(collection => {
80 | const randomIndex = Math.floor(Math.random() * collection.length)
81 |
82 | const [firstChar, ...restOfWord] = collection[randomIndex]
83 |
84 | return firstChar.toUpperCase() + restOfWord.join('')
85 | })
86 |
87 | return phraseWords.join(' ')
88 | }
89 |
--------------------------------------------------------------------------------
/static/assets/country-flags/KZ.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/AW.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
48 |
--------------------------------------------------------------------------------
/static/assets/country-flags/OM.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
46 |
--------------------------------------------------------------------------------
/static/assets/country-flags/PY.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/PF.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
49 |
--------------------------------------------------------------------------------
/static/assets/country-flags/ST.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
49 |
--------------------------------------------------------------------------------
/static/assets/country-flags/AG.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
46 |
--------------------------------------------------------------------------------
/static/assets/country-flags/FM.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/static/assets/country-flags/JE.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
46 |
--------------------------------------------------------------------------------
/static/assets/country-flags/TW.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/BB.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
46 |
--------------------------------------------------------------------------------
/static/assets/country-flags/SV.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/static/assets/country-flags/MH.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/AF.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/static/assets/country-flags/FO.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/static/assets/country-flags/IS.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
48 |
--------------------------------------------------------------------------------
/static/assets/country-flags/NI.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/static/assets/country-flags/NO.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
48 |
--------------------------------------------------------------------------------
/static/assets/country-flags/BZ.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
50 |
--------------------------------------------------------------------------------