• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cofacts / rumors-api / 5892775065

17 Aug 2023 03:17PM UTC coverage: 88.141% (-0.08%) from 88.218%
5892775065

push

github

web-flow
Merge pull request #312 from cofacts/compress-logs

feat: record less bytes in GraphQL request

707 of 855 branches covered (82.69%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 1 file covered. (100.0%)

1441 of 1582 relevant lines covered (91.09%)

22.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

34.35
/src/auth.js
1
import passport from 'koa-passport';
2
import client, { processMeta } from 'util/client';
3
import FacebookStrategy from 'passport-facebook';
4
import TwitterStrategy from 'passport-twitter';
5
import GithubStrategy from 'passport-github2';
6
import GoogleStrategy from 'passport-google-oauth20';
7
import InstagramStrategy from 'passport-instagram-graph';
8
import Router from 'koa-router';
9

10
/**
11
 * Serialize to session
12
 */
13
passport.serializeUser((user, done) => {
2✔
14
  done(null, user.id);
×
15
});
16

17
/**
18
 * De-serialize and populates ctx.state.user
19
 */
20
passport.deserializeUser((userId, done) => {
2✔
21
  try {
×
22
    done(null, { userId });
×
23
  } catch (err) {
24
    done(err);
×
25
  }
26
});
27

28
/**
29
 * Common verify callback for all login strategies.
30
 * It tries first authenticating user with profile.id agains fieldName in DB.
31
 * If not applicable, search for existing users with their email.
32
 * If still not applicable, create a user with currently given profile.
33
 *
34
 * @param {object} profile - passport profile object
35
 * @param {'facebookId'|'githubId'|'twitterId'|'googleId'|'instagramId'} fieldName - The elasticsearch ID field name in user document
36
 */
37
export async function verifyProfile(profile, fieldName) {
38
  // Find user with such user id
39
  //
40
  const { body: users } = await client.search({
3✔
41
    index: 'users',
42
    type: 'doc',
43
    q: `${fieldName}:${profile.id}`,
44
  });
45

46
  if (users.hits.total) {
3✔
47
    return processMeta(users.hits.hits[0]);
1✔
48
  }
49

50
  const now = new Date().toISOString();
2✔
51
  const email = profile.emails && profile.emails[0].value;
2✔
52
  const avatar = profile.photos && profile.photos[0].value;
2✔
53
  const username = profile.displayName ? profile.displayName : profile.username;
2✔
54

55
  // Find user with such email
56
  //
57
  if (email) {
2!
58
    const { body: usersWithEmail } = await client.search({
2✔
59
      index: 'users',
60
      type: 'doc',
61
      q: `email:${email}`,
62
    });
63

64
    if (usersWithEmail.hits.total) {
2✔
65
      const id = usersWithEmail.hits.hits[0]._id;
1✔
66
      // Fill in fieldName with profile.id so that it does not matter if user's
67
      // email gets changed in the future.
68
      //
69
      const { body: updateUserResult } = await client.update({
1✔
70
        index: 'users',
71
        type: 'doc',
72
        id,
73
        body: {
74
          doc: {
75
            [fieldName]: profile.id,
76
            updatedAt: now,
77
          },
78
        },
79
        _source: true,
80
      });
81

82
      if (updateUserResult.result === 'updated') {
1!
83
        return processMeta({ ...updateUserResult.get, _id: id });
1✔
84
      }
85

86
      throw new Error(updateUserResult.result);
×
87
    }
88
  }
89

90
  // No user in DB, create one
91
  //
92
  const { body: createUserResult } = await client.index({
1✔
93
    index: 'users',
94
    type: 'doc',
95
    body: {
96
      email,
97
      name: username,
98
      avatarUrl: avatar,
99
      [fieldName]: profile.id,
100
      createdAt: now,
101
      updatedAt: now,
102
    },
103
  });
104

105
  if (createUserResult.result === 'created') {
1!
106
    return processMeta(
1✔
107
      (await client.get({
108
        index: 'users',
109
        type: 'doc',
110
        id: createUserResult._id,
111
      })).body
112
    );
113
  }
114

115
  throw new Error(createUserResult.result);
×
116
}
117

118
if (process.env.FACEBOOK_APP_ID) {
2!
119
  passport.use(
×
120
    new FacebookStrategy(
121
      {
122
        clientID: process.env.FACEBOOK_APP_ID,
123
        clientSecret: process.env.FACEBOOK_SECRET,
124
        callbackURL: process.env.FACEBOOK_CALLBACK_URL,
125
        profileFields: ['id', 'displayName', 'photos', 'email'],
126
        graphAPIVersion: 'v10.0',
127
      },
128
      (token, tokenSecret, profile, done) =>
129
        verifyProfile(profile, 'facebookId')
×
130
          .then(user => done(null, user))
×
131
          .catch(done)
132
    )
133
  );
134
}
135

136
if (process.env.TWITTER_CONSUMER_KEY) {
2!
137
  passport.use(
×
138
    new TwitterStrategy(
139
      {
140
        consumerKey: process.env.TWITTER_CONSUMER_KEY,
141
        consumerSecret: process.env.TWITTER_CONSUMER_SECRET,
142
        callbackURL: process.env.TWITTER_CALLBACK_URL,
143

144
        // https://github.com/jaredhanson/passport-twitter/issues/67#issuecomment-275288663
145
        userProfileURL:
146
          'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true',
147
      },
148
      (token, tokenSecret, profile, done) =>
149
        verifyProfile(profile, 'twitterId')
×
150
          .then(user => done(null, user))
×
151
          .catch(done)
152
    )
153
  );
154
}
155

156
if (process.env.GITHUB_CLIENT_ID) {
2!
157
  passport.use(
×
158
    new GithubStrategy(
159
      {
160
        clientID: process.env.GITHUB_CLIENT_ID,
161
        clientSecret: process.env.GITHUB_SECRET,
162
        callbackURL: process.env.GITHUB_CALLBACK_URL,
163
      },
164
      (token, tokenSecret, profile, done) =>
165
        verifyProfile(profile, 'githubId')
×
166
          .then(user => done(null, user))
×
167
          .catch(done)
168
    )
169
  );
170
}
171

172
if (process.env.GOOGLE_CLIENT_ID) {
2!
173
  passport.use(
×
174
    new GoogleStrategy(
175
      {
176
        clientID: process.env.GOOGLE_CLIENT_ID,
177
        clientSecret: process.env.GOOGLE_SECRET,
178
        callbackURL: process.env.GOOGLE_CALLBACK_URL,
179
      },
180
      (token, tokenSecret, profile, done) =>
181
        verifyProfile(profile, 'googleId')
×
182
          .then(user => done(null, user))
×
183
          .catch(done)
184
    )
185
  );
186
}
187

188
// Note: Instagram Basic Display API doesn't provide email
189
// and it needs the `user_media` scope to get profile photo.
190
if (process.env.INSTAGRAM_CLIENT_ID) {
2!
191
  passport.use(
×
192
    new InstagramStrategy(
193
      {
194
        clientID: process.env.INSTAGRAM_CLIENT_ID,
195
        clientSecret: process.env.INSTAGRAM_SECRET,
196
        callbackURL: process.env.INSTAGRAM_CALLBACK_URL,
197
      },
198
      (token, tokenSecret, profile, done) =>
199
        verifyProfile(profile, 'instagramId')
×
200
          .then(user => done(null, user))
×
201
          .catch(done)
202
    )
203
  );
204
}
205

206
// Exports route handlers
207
//
208
export const loginRouter = Router()
2✔
209
  .use((ctx, next) => {
210
    // Memorize redirect in session
211
    //
212
    if (!ctx.query.redirect || !ctx.query.redirect.startsWith('/')) {
×
213
      const err = new Error(
×
214
        '`redirect` must present in query string and start with `/`'
215
      );
216
      err.status = 400;
×
217
      err.expose = true;
×
218
      throw err;
×
219
    }
220
    ctx.session.appId = ctx.query.appId || 'RUMORS_SITE';
×
221
    ctx.session.redirect = ctx.query.redirect;
×
222
    ctx.session.origin = new URL(ctx.get('Referer')).origin;
×
223
    return next();
×
224
  })
225
  .get('/facebook', passport.authenticate('facebook', { scope: ['email'] }))
226
  .get('/twitter', passport.authenticate('twitter'))
227
  .get('/github', passport.authenticate('github', { scope: ['user:email'] }))
228
  .get('/google', passport.authenticate('google', { scope: ['profile email'] }))
229
  .get(
230
    '/instagram',
231
    passport.authenticate('instagram', { scope: ['user_profile'] })
232
  );
233

234
const handlePassportCallback = strategy => (ctx, next) =>
10✔
235
  passport.authenticate(strategy, (err, user) => {
×
236
    if (!err && !user) err = new Error('No such user');
×
237

238
    if (err) {
×
239
      err.status = 401;
×
240
      throw err;
×
241
    }
242

243
    ctx.login(user);
×
244
  })(ctx, next);
245

246
export const authRouter = Router()
2✔
247
  .use(async (ctx, next) => {
248
    // Perform redirect after login
249
    //
250
    if (!ctx.session.redirect || !ctx.session.appId) {
×
251
      const err = new Error(
×
252
        '`appId` and `redirect` must be set before. Did you forget to go to /login/*?'
253
      );
254
      err.status = 400;
×
255
      err.expose = true;
×
256
      throw err;
×
257
    }
258

259
    await next();
×
260

261
    let basePath = '';
×
262
    if (
×
263
      ctx.session.appId === 'RUMORS_SITE' ||
×
264
      ctx.session.appId === 'DEVELOPMENT_FRONTEND'
265
    ) {
266
      const validOrigins = (
×
267
        process.env.RUMORS_SITE_REDIRECT_ORIGIN || ''
×
268
      ).split(',');
269

270
      basePath =
×
271
        validOrigins.find(o => o === ctx.session.origin) || validOrigins[0];
×
272
    }
273

274
    // TODO: Get basePath from DB for other client apps
275
    try {
×
276
      ctx.redirect(new URL(ctx.session.redirect, basePath).href);
×
277
    } catch (err) {
278
      err.status = 400;
×
279
      err.expose = true;
×
280
      throw err;
×
281
    }
282

283
    // eslint-disable-next-line require-atomic-updates
284
    ctx.session.appId = undefined;
×
285
    // eslint-disable-next-line require-atomic-updates
286
    ctx.session.redirect = undefined;
×
287
  })
288
  .get('/facebook', handlePassportCallback('facebook'))
289
  .get('/twitter', handlePassportCallback('twitter'))
290
  .get('/github', handlePassportCallback('github'))
291
  .get('/google', handlePassportCallback('google'))
292
  .get('/instagram', handlePassportCallback('instagram'));
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc