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

gofynd / example-extension-nextjs / 12136016319

03 Dec 2024 08:47AM UTC coverage: 83.81% (-2.8%) from 86.611%
12136016319

push

github

web-flow
Merge pull request #3 from gofynd/review-fixes

ID: FPP-2010; Extension review fixes

46 of 60 branches covered (76.67%)

Branch coverage included in aggregate %.

28 of 32 new or added lines in 3 files covered. (87.5%)

4 existing lines in 1 file now uncovered.

130 of 150 relevant lines covered (86.67%)

12.88 hits per line

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

79.73
/server.js
1
// Import required modules and dependencies
2
const express = require('express');
12✔
3
const bodyParser = require('body-parser');
12✔
4
const passport = require('passport');
12✔
5
const OAuth2Strategy = require('passport-oauth2').Strategy;
12✔
6
const session = require('express-session');
12✔
7
const next = require('next');
12✔
8
const path = require("path");
12✔
9
const hmacSHA256 = require("crypto-js/hmac-sha256");
12✔
10

11
const config = require('./config');
12✔
12
const Session = require("./session/session");
12✔
13
const SessionStorage = require("./session/session_storage");
12✔
14
const { getSessionFromRequest } = require('./session/sessionUtils.js');
12✔
15

16
const cookieParser = require('cookie-parser');
12✔
17

18
// Set up environment and Next.js app
19
const isDev = process.env.NODE_ENV !== 'production';
12✔
20
const nextApp = next({ dev: isDev });
12✔
21
const handle = nextApp.getRequestHandler(); // Next.js request handler
12✔
22

23
// Extension configuration constants from environment variables
24
const {
25
    SESSION_COOKIE_NAME,
26
    EXTENSION_BASEURL,
27
    EXTENSION_ID,
28
    EXTENSION_SECRET,
29
    FP_API_DOMAIN
30
} = config;
12✔
31

32
// Initialize Express app
33
const app = express();
12✔
34

35
// Middleware setup
36
app.use(bodyParser.json());
12✔
37
app.use(bodyParser.urlencoded({ extended: false }));
12✔
38

39
// Middleware to parse cookies with a secret key
40
app.use(cookieParser("ext.session"));
12✔
41

42
// Set up session handling with express-session
43
app.use(session({
12✔
44
    secret: 'your-secret-key', // Replace with a secure secret
45
    resave: false,
46
    saveUninitialized: true,
47
}));
48

49
// Initialize Passport for authentication
50
app.use(passport.initialize());
12✔
51
app.use(passport.session());
12✔
52

53
// Serialize user to the session
54
passport.serializeUser((user, done) => {
12✔
55
    done(null, user);
×
56
});
57

58
// Deserialize user from the session
59
passport.deserializeUser((user, done) => {
12✔
60
    done(null, user);
×
61
});
62

63
// Function to configure and return the OAuth2 strategy
64
function getOAuthStrategy(companyId, applicationId) {
65
    const callbackURL = applicationId
12!
66
        ? `${EXTENSION_BASEURL}/fp/auth?application_id=${applicationId}`
67
        : `${EXTENSION_BASEURL}/fp/auth`;
68
    return new OAuth2Strategy({
12✔
69
        authorizationURL: `${FP_API_DOMAIN}/service/panel/authentication/v1.0/company/${companyId}/oauth/authorize`,
70
        tokenURL: `${FP_API_DOMAIN}/service/panel/authentication/v1.0/company/${companyId}/oauth/token`,
71
        clientID: EXTENSION_ID,
72
        clientSecret: EXTENSION_SECRET,
73
        callbackURL,
74
        passReqToCallback: true
75
    },
76
        (req, accessToken, refreshToken, token, profile, cb) => {
77
            req.token = token;
×
78
            return cb(null, { accessToken });
×
79
        }
80
    );
81
}
82

83
// OAuth authentication route
84
app.get('/fp/install', async (req, res, next) => {
12✔
85
    try {
12✔
86
        let companyId = parseInt(req.query.company_id);
12✔
87
        const state = Buffer.from(JSON.stringify("abcefg")).toString('base64');
12✔
88
        let extensionScope = [];
12✔
89
        let session = new Session(Session.generateSessionId());
12✔
90
        let redirectPath = req.query.redirect_path;
12✔
91
        let sessionExpires = new Date(Date.now() + 900000); // 15 min
12✔
92

93
        if (session.isNew) {
12!
94
            session.company_id = companyId;
12✔
95
            session.scope = extensionScope;
12✔
96
            session.expires = sessionExpires;
12✔
97
            session.access_mode = 'online'; // Always generate online mode token for extension launch
12✔
98
            session.extension_id = EXTENSION_ID;
12✔
99
            session.redirect_path = redirectPath;
12✔
100
        } else {
101
            if (session.expires) {
×
102
                session.expires = new Date(session.expires);
×
103
            }
104
        }
105

106
        req.fdkSession = session;
12✔
107
        const compCookieName = `${SESSION_COOKIE_NAME}_${companyId}`
12✔
108
        res.header['x-company-id'] = companyId;
12✔
109
        res.cookie(compCookieName, session.id, {
12✔
110
            secure: true,
111
            httpOnly: true,
112
            expires: session.expires,
113
            signed: true,
114
            sameSite: "None"
115
        });
116
        session.state = state;
12✔
117
        passport.use(getOAuthStrategy(req.query.company_id, req.query.application_id));
12✔
118
        const authenticator = passport.authenticate('oauth2', { scope: extensionScope, state });
12✔
119
        authenticator(req, res, next);
12✔
120
        await SessionStorage.saveSession(session);
12✔
121
    }
122
    catch (error) {
123
        console.error("Error during /fp/install route:", error.message);
×
124
        res.status(500).send({ error: `Error during /fp/install route: ${error.message}` });
×
125
    }
126
});
127

128
// OAuth callback route
129
app.get('/fp/auth', passport.authenticate('oauth2', { failureRedirect: '/' }), sessionMiddleware(false), async (req, res) => {
12✔
130
    try {
12✔
131
        if (!req.fdkSession) {
12!
UNCOV
132
            throw new Error("Can not complete oauth process as session not found");
×
133
        }
134

135
        const companyId = req.fdkSession.company_id
12✔
136
        const { token } = req;
12✔
137
        let sessionExpires = new Date(Date.now() + token.expires_in * 1000);
12✔
138

139
        req.fdkSession.expires = sessionExpires;
12✔
140
        token.access_token_validity = sessionExpires.getTime();
12✔
141
        req.fdkSession.updateToken(token);
12✔
142

143
        await SessionStorage.saveSession(req.fdkSession);
12✔
144
        const compCookieName = `${SESSION_COOKIE_NAME}_${companyId}`
12✔
145
        res.cookie(compCookieName, req.fdkSession.id, {
12✔
146
            secure: true,
147
            httpOnly: true,
148
            expires: sessionExpires,
149
            signed: true,
150
            sameSite: "None"
151
        });
152
        res.header['x-company-id'] = companyId;
12✔
153

154
        // Subscribe to a specific event
155
        await configureWebhookSubscriber(
12✔
156
            token.access_token,
157
            req.query.company_id,
158
        );
159
        // Redirect based on company or application ID
160
        const redirectUrl = req.query.application_id
12!
161
            ? `${process.env.EXTENSION_BASE_URL}/company/${req.query.company_id}/application/${req.query.application_id}`
162
            : `${process.env.EXTENSION_BASE_URL}/company/${req.query.company_id}`;
163

164
        return res.redirect(301, redirectUrl);
12✔
165
    }
166
    catch (error) {
UNCOV
167
        console.error("Error during /fp/auth route:", error.message);
×
UNCOV
168
        res.status(500).send({ error: `Error during /fp/auth route: ${error.message}` });
×
169
    }
170
});
171

172
app.post('/fp/uninstall', (req, res) => {
12✔
173
    // Write your cleanup logic to be executed after the extension is uninstalled.
174
    res.json({ success: true });
12✔
175
});
176

177
function verifySignature(req, res, next) {
178
    try {
12✔
179
        const reqSignature = req.headers['x-fp-signature'];
12✔
180
        const { body } = req;
12✔
181
        const calcSignature = hmacSHA256(JSON.stringify(body), EXTENSION_SECRET).toString();
12✔
182

183
        if (reqSignature !== calcSignature) {
12!
NEW
184
            return res.status(403).json({ "error": "Invalid signature" });
×
185
        }
186
        next();
12✔
187
    } catch (error) {
NEW
188
        return res.status(403).json({ 
×
189
            "error": "InvalidSignature",
190
            "message": "The request signature we calculated does not match the signature you provided."
191
         });
192
    }
193
}
194

195
app.post('/ext/webhook', verifySignature, (req, res) => {
12✔
196
    try {
12✔
197
        console.log(`Webhook Event: ${JSON.stringify(req.body.event)} received`)
12✔
198
        return res.status(200).json({ "success": true });
12✔
199
    } catch (err) {
200
        console.error(`Error Processing ${req.body.event} Webhook`);
×
201
        return res.status(500).json({ "success": false });
×
202
    }
203
});
204

205
function sessionMiddleware(strict) {
206
    return async (req, res, next) => {
12✔
207
      try {
12✔
208
        const companyId = req.headers['x-company-id'] || req.query['company_id'];
12✔
209
        req.fdkSession = await getSessionFromRequest(req, companyId);
12✔
210
  
211
        if (strict && !req.fdkSession) {
12!
NEW
212
          return res.status(401).json({ message: 'Unauthorized' });
×
213
        }
214
        next();
12✔
215
      } catch (error) {
NEW
216
        next(error);
×
217
      }
218
    };
219
}
220

221
// Function to configure webhook subscriber
222
// Check here for available webhook events https://partners.fynd.com/help/docs/webhooks/overview
223
async function configureWebhookSubscriber(accessToken, companyId) {
224
    try {
12✔
225
        const requestOptions = {
12✔
226
            method: 'POST',
227
            headers: {
228
                Authorization: `Bearer ${accessToken}`,
229
                'Content-Type': 'application/json',
230
            },
231
            body: JSON.stringify({
232
                name: process.env.EXTENSION_API_KEY,
233
                webhook_url: `${process.env.EXTENSION_BASE_URL}/ext/webhook`,
234
                association: {
235
                    company_id: companyId,
236
                    criteria: 'ALL',
237
                },
238
                auth_meta: {
239
                    type: 'hmac',
240
                    secret: process.env.EXTENSION_API_SECRET,
241
                },
242
                email_id: 'dev@gofynd.com',
243
                events: [
244
                    { slug: 'company/product/delete/v1' }
245
                ],
246
                status: 'active',
247
                provider: 'rest'
248
            }),
249
        };
250

251
        const response = await fetch(
12✔
252
            `${FP_API_DOMAIN}/service/platform/webhook/v2.0/company/${companyId}/subscriber`,
253
            requestOptions
254
        );
255
        return response.json(); // Return subscriber config data
12✔
256
    }
257
    catch (error) {
258
        console.error(`Error while fetching webhook subscriber configuration, Reason: ${error.message}`);
×
259
    }
260
}
261

262
// Serve static files from the 'build' directory in production
263
if (!isDev)
12✔
264
    app.use('/_next', express.static(path.join(__dirname, 'public/build')));
×
265

266
// Handle all other requests with Next.js
267
app.all('*', (req, res) => {
12✔
UNCOV
268
    return handle(req, res);
×
269
});
270

271
// Prepare and start the server
272
let server;
273
nextApp.prepare().then(() => {
12✔
274
    const PORT = process.env.FRONTEND_PORT || 3000;
12✔
275
    server = app.listen(PORT, () => {
12✔
276
        console.log(`Server started on port ${PORT}`);
12✔
277
    });
278
})
279

280
const getServerInstance = () => {
12✔
281
    return server
12✔
282
}
283

284
module.exports = { app, getServerInstance };
12✔
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