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

suculent / thinx-device-api / #252646157

09 May 2025 01:17PM UTC coverage: 71.943% (-0.03%) from 71.97%
#252646157

push

suculent
fixed Google login issue; improved logging

1832 of 3470 branches covered (52.8%)

Branch coverage included in aggregate %.

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

40 existing lines in 3 files now uncovered.

8186 of 10455 relevant lines covered (78.3%)

7.49 hits per line

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

22.38
/lib/router.google.js
1
// /api/v2/oauth/google
2

3
const Globals = require("./thinx/globals");
1✔
4
const Database = require("../lib/thinx/database.js");
1✔
5

6
let db_uri = new Database().uri();
1✔
7
const prefix = Globals.prefix();
1✔
8
var userlib = require("nano")(db_uri).use(prefix + "managed_users");
1✔
9

10
var AuditLog = require("../lib/thinx/audit");
1✔
11
var alog = new AuditLog();
1✔
12

13
const https = require('https');
1✔
14
const sha256 = require("sha256");
1✔
15

16
const app_config = Globals.app_config(); // public_url and public_url but it is the same now
1✔
17

18
//
19
// OAuth2 for Google
20
//
21

22
const google_ocfg = Globals.google_ocfg();
1✔
23

24
const oAuthConfig = {
1✔
25
  client: {
26
    id: process.env.GOOGLE_OAUTH_ID,
27
    secret: process.env.GOOGLE_OAUTH_SECRET
28
  },
29
  auth: {
30
    authorizeHost: 'https://accounts.google.com',
31
    authorizePath: '/o/oauth2/v2/auth',
32
    tokenHost: 'https://www.googleapis.com',
33
    tokenPath: '/oauth2/v4/token'
34
  }
35
};
36

37
const { AuthorizationCode } = require('simple-oauth2');
1✔
38

39
module.exports = function (app) {
1✔
40

41
  let redis_client = app.redis_client;
15✔
42

43
  var user = app.owner;
15✔
44

45
   /*
46
   * OAuth 2 with Google
47
   */
48

49
  function createUserWithGoogle(req, ores, odata, userWrapper, access_token) {
50
    console.log("[google] Creating new user...");
×
51

52
    // No e-mail to validate.
53
    let will_require_activation = true;
×
54
    if (typeof (odata.email) === "undefined") {
×
55
      will_require_activation = false;
×
56
    }
57

58
    // No such owner, create...
59
    user.create(userWrapper, will_require_activation, ores, (/*res, success, status*/) => {
×
60

61
      console.log("[OID:" + req.session.owner + "] [NEW_SESSION] [oauth] 2860:");
×
62

63
      alog.log(req.session.owner, "OAuth User created: " + userWrapper.given_name + " " + userWrapper.family_name);
×
64

65
      console.log("IMPORTANT DEBUG 'access_token': ", access_token);
×
66

67
      // This is weird. Token should be random and with prefix.
68
      const token = sha256(access_token.token.access_token); // "o:"+
×
69
      app.redis_client.v4.set(token, JSON.stringify(userWrapper));
×
70
      app.redis_client.v4.expire(token, 3600);
×
71

72
      const ourl = app_config.public_url + "/auth.html?t=" + token + "&g=true"; // require GDPR consent
×
73
      console.log("OURL", ourl);
×
74
      console.log("Redirecting to:", ourl);
×
75
      ores.redirect(ourl);
×
76
    });
77
  }
78

79
  // processGoogleCallbackError only
80
  function failOnDeletedAccountDocument(error, ores) {
81
    // User does not exist
82
    if (error.toString().indexOf("Error: deleted") !== -1) {
×
83
      // Redirect to error page with reason for deleted documents
84
      console.log("[processGoogleCallbackError][oauth] user document deleted");
×
85
      ores.redirect(app_config.public_url + '/error.html?success=failed&title=OAuth-Error&reason=account_doc_deleted');
×
86
      return true;
×
87
    }
88
    return false;
×
89
  }
90

91
  // processGoogleCallbackError only
92
  function failOnDeletedAccount(udoc, ores) {
93
    if (typeof (udoc) === "undefined") return false;
×
94
    if ((typeof (udoc.deleted) !== "undefined") && udoc.deleted === true) {
×
95
      console.log("[processGoogleCallbackError][oauth] user account marked as deleted");
×
96
      ores.redirect(
×
97
        app_config.public_url + '/error.html?success=failed&title=OAuth-Error&reason=account_deleted'
98
      );
99
      return true;
×
100
    }
101
    return false;
×
102
  }
103

104
  // from userWrapper, this uses owner and createUserWithGoogle uses whole wrapper to create a user object (only if udoc does not exist),
105
  // and to store it in redis with token temporarily to allow login without accessing the CouchDB
106
  function processGoogleCallbackError(error, ores, udoc, req, odata, userWrapper, access_token) {
107

108
    if (failOnDeletedAccountDocument(error, ores)) return;
×
109
    if (failOnDeletedAccount(udoc, ores)) return;
×
110

111
    // In case the document is undefined (and identity confirmed by Google), create new one...
112
    if (typeof (udoc) === "undefined" || udoc === null) {
×
113
      console.log("Setting session owner from Google User Wrapper...");
×
114
      req.session.owner = userWrapper.owner;
×
115
      console.log("[OID:" + req.session.owner + "] [NEW_SESSION] on UserWrapper /login");
×
116
      createUserWithGoogle(req, ores, odata, userWrapper, access_token);
×
117
    }
118
  }
119

120
  function needsGDPR(doc) {
121
    var gdpr = false;
×
122
    if (typeof (doc.info) !== "undefined") {
×
123
      if (typeof (doc.gdpr_consent) !== "undefined" && doc.gdpr_consent === true) {
×
124
        gdpr = true;
×
125
      }
126
    }
127
    return gdpr;
×
128
  }
129

130
  // Routes are same for API v1 and v2
131

132
  // Initial page redirecting to OAuth2 provider
133
  app.get('/api/oauth/google', function (req, res) {
15✔
134
    // User requested login, destroy existing session first...
135
    if (typeof (req.session) !== "undefined") {
1!
136
      req.session.destroy();
1✔
137
    }
138
    require("crypto").randomBytes(48, (_err, buffer) => {
1✔
139
      var token = buffer.toString('hex');
1✔
140
      redis_client.v4.set("oa:google:" + token, 60); // auto-expires in 1 minute; TODO: verify
1✔
141

142
      const client = new AuthorizationCode(oAuthConfig);
1✔
143

144
      const authorizationUri = client.authorizeURL({
1✔
145
        redirect_uri: google_ocfg.web.redirect_uris[0],
146
        scope: 'email',
147
        state: sha256(token) // returned upon auth provider call back
148
      });
149
      res.redirect(authorizationUri);
1✔
150
    });
151
  });
152

153
  app.get('/api/oauth/google/callback', async (req, res) => {
15✔
154

155
    const code = req.query.code;
1✔
156
    if (typeof (code) !== "string") {
1!
157
      res.set(403).end();
1✔
158
      return;
1✔
159
    } else {
160
      if (code.length > 255) {
×
161
        res.set(403).end();
×
162
        return; // should not DoS the regex now; lgtm [js/type-confusion-through-parameter-tampering]
×
163
      }
164
    }
165

166
    const tokenParams = {
×
167
      code: code,
168
      redirect_uri: google_ocfg.web.redirect_uris[0],
169
      scope: 'email',
170
    };
171

UNCOV
172
    const client = new AuthorizationCode(oAuthConfig);
×
173
    let accessToken;
174

175
    try {
×
UNCOV
176
      accessToken = await client.getToken(tokenParams);
×
177
    } catch (error) {
UNCOV
178
      console.log('Access Token Error', error.message);
×
UNCOV
179
      res.set(400).end();
×
180
      return;
×
181
    }
182

UNCOV
183
    console.log("[debug] accessToken format:", {accessToken} ); // TODO: remove when done!
×
184

UNCOV
185
    let gat_url = 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=' + accessToken.token.access_token;
×
186

187
    https.get(gat_url, (res3) => {
×
188

UNCOV
189
      let data = '';
×
190
      res3.on('data', (chunk) => { data += chunk; });
×
191
      res3.on('end', () => {
×
192

193
        const odata = JSON.parse(data);
×
194
        const email = odata.email;
×
195

UNCOV
196
        if (typeof (email) === "undefined") {
×
UNCOV
197
          res.redirect(
×
198
            app_config.public_url + '/error.html?success=failed&title=Sorry&reason=' +
199
            'E-mail missing.'
200
          );
201
          return;
×
202
        }
203

UNCOV
204
        const family_name = odata.family_name;
×
205
        const given_name = odata.given_name;
×
UNCOV
206
        const owner_id = sha256(prefix + email);
×
207

UNCOV
208
        var userWrapper = {
×
209
          first_name: given_name,
210
          last_name: family_name,
211
          email: email,
212
          owner: owner_id,
213
          username: owner_id
214
        };
215

216
        // Check user and make note on user login
UNCOV
217
        userlib.get(owner_id,  (error, udoc) => {
×
218

219
          if (error) {
×
220
            // may also end-up creating new user
221
            processGoogleCallbackError(error, res, udoc, req, odata, userWrapper, accessToken);
×
222
            return;
×
223
          }
224
          console.log(`ℹ️ [info] Calling trackUserLogin on Google Auth Callback...`);
×
225
          user.trackUserLogin(owner_id);
×
226
          console.log(`ℹ️ [info] Calling updateLastSeen on Google Auth Callback...`);
×
227
          user.updateLastSeen(udoc, false);
×
228
          alog.log(owner_id, "OAuth2 User logged in...");
×
229
          var token = sha256(accessToken.token.access_token);
×
230
          redis_client.v4.set(token, JSON.stringify(userWrapper));
×
UNCOV
231
          redis_client.v4.expire(token, 3600);
×
UNCOV
232
          const ourl = app_config.public_url + "/auth.html?t=" + token + "&g=" + needsGDPR(udoc); // require GDPR consent
×
UNCOV
233
          res.redirect(ourl);
×
234
        });
235
      });
236
    }).on("error", (err) => {
UNCOV
237
      console.log("Error: " + err.message);
×
238
      // deepcode ignore OR: there is noting injected in the URL
UNCOV
239
      res.redirect(app_config.public_url + '/error.html?success=failed&title=OAuth-Error&reason=' + err.message);
×
240
    });
241

242
  });
243

244
};
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

© 2026 Coveralls, Inc