Skip to content

Commit bb72aac

Browse files
authored
feat(sdk): apikeys authentication (#49)
* feat(api): setup data layer and services * chore: renamed to sdk auth * feat(api): add isSdkAuthenticated middleware * chore(core): update sdk error codes * chore(api): added response code for missing key * chore(api): add helper methods to set response * feat(ui): reflect api changes * feat(ui): added api keys panel * feat(ui): added create and revoke apikey * chore(ui): fix navigation * chore(ui): fix table items visibility on mobile * chore(ui): minor ui changes
1 parent 8013402 commit bb72aac

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1000
-271
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ packages/api/db.switchfeat.segments
1111
db.switchfeat.flags
1212
db.switchfeat.segments
1313
db.switchfeat.users
14+
db.switchfeat.sdkauths
15+
packages/api/db.switchfeat.sdkauths

package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"lint": "lerna run lint",
2727
"start": "cd ./packages/api && npm run start",
2828
"dev:start-api": "cd ./packages/api && npm run dev:start",
29-
"dev:start-ui": "cd ./packages/ui && npm run start"
29+
"dev:start-ui": "cd ./packages/ui && npm run start",
30+
"dev:start-tw:watch": "cd ./packages/ui && npm run build:tw:watch"
3031
}
3132
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ApiResponseCode, ResponseModel } from "@switchfeat/core";
2+
import { Request, Response } from "express";
3+
4+
export const setErrorResponse = (resp: Response, error: ApiResponseCode) => {
5+
console.log(error);
6+
resp.status(error.statusCode).json({
7+
success: false,
8+
error: error,
9+
data: null
10+
} as ResponseModel<null>);
11+
};
12+
13+
export const setSuccessResponse = <T extends object | null>(resp: Response, code: ApiResponseCode, data: T, req?: Request) => {
14+
console.log(code);
15+
16+
const response = {
17+
success: true,
18+
data
19+
} as ResponseModel<T>;
20+
21+
if (req) {
22+
response.user = req.user;
23+
response.cookies = req.cookies;
24+
}
25+
26+
resp.status(code.statusCode).json(response);
27+
};

packages/api/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as passportAuth from "./managers/auth/passportAuth";
1212
import { getDataStoreManager } from './managers/auth/dataStoreManager';
1313
import { segmentsRoutesWrapper } from './routes/segmentsRoutes';
1414
import { sdkRoutesWrapper } from './routes/sdkRoutes';
15+
import { sdkAuthRoutesWrapper } from './routes/sdkAuthRoutes';
1516

1617
dotenv.config();
1718
const env = process.env.NODE_ENV;
@@ -52,6 +53,7 @@ app.use(authRoutes);
5253
app.use(flagRoutesWrapper(dataStoreManagerPromise));
5354
app.use(segmentsRoutesWrapper(dataStoreManagerPromise));
5455
app.use(sdkRoutesWrapper(dataStoreManagerPromise));
56+
app.use(sdkAuthRoutesWrapper(dataStoreManagerPromise));
5557

5658
if (env !== "dev") {
5759
app.get('*', (req, res) => {
Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,68 @@
11
import passport from "passport";
22
import { Request, Response, NextFunction, Express } from "express";
33
import * as userService from "../../services/usersService";
4+
import * as sdkAuthService from "../../services/sdkAuthService";
45
import { googleStrategy } from "./googleAuth";
5-
import { keys } from "@switchfeat/core";
6+
import { ApiResponseCodes, keys } from "@switchfeat/core";
67

78

89
export const initialise = (app: Express) => {
910

10-
app.use(passport.initialize());
11+
app.use(passport.initialize());
1112

12-
// serialize the user.id to save in the cookie session
13-
// so the browser will remember the user when login
14-
passport.serializeUser((_req, user, done) => {
15-
done(null, user);
16-
});
13+
// serialize the user.id to save in the cookie session
14+
// so the browser will remember the user when login
15+
passport.serializeUser((_req, user, done) => {
16+
done(null, user);
17+
});
1718

18-
// deserialize the cookieUserId to user in the database
19-
passport.deserializeUser(async (id: string, done) => {
20-
const currentUser = await userService.getUser({ userId: id });
21-
done(currentUser === null ? "user not found." : null, { user: currentUser });
22-
});
19+
// deserialize the cookieUserId to user in the database
20+
passport.deserializeUser(async (id: string, done) => {
21+
const currentUser = await userService.getUser({ userId: id });
22+
done(currentUser === null ? "user not found." : null, { user: currentUser });
23+
});
2324

24-
if (keys.AUTH_PROVIDER === "google") {
25-
console.log(" -> Google auth active");
26-
passport.use(googleStrategy());
27-
}
25+
if (keys.AUTH_PROVIDER === "google") {
26+
console.log(" -> Google auth active");
27+
passport.use(googleStrategy());
28+
}
2829
};
2930

31+
export const isAuthenticated = (req: Request, res: Response, next: NextFunction) => {
32+
if (!keys.AUTH_PROVIDER || req.isAuthenticated()) {
33+
return next();
34+
}
35+
res.redirect("/");
36+
};
3037

38+
/*
39+
** - Get the apikey from the sdk request
40+
** - Lookup of the key in db
41+
** - Ensure it is not expired
42+
*/
43+
export const isSdkAuthenticated = async (req: Request, res: Response, next: NextFunction) => {
3144

45+
const apiKey = req.headers["sf-api-key"] as string;
46+
if (!apiKey) {
47+
res.status(401).json({
48+
error: ApiResponseCodes.ApiKeyNotFound,
49+
});
3250

33-
export const isAuthenticated = (req: Request, res: Response, next: NextFunction) => {
34-
if (!keys.AUTH_PROVIDER || req.isAuthenticated()) {
35-
return next();
36-
}
37-
res.redirect("/");
51+
return;
52+
}
53+
const foundInDb = await sdkAuthService.getSdkAuth({ apiKey: apiKey });
54+
55+
const isValid = foundInDb !== null && foundInDb.expiresOn > new Date();
56+
57+
if (!keys.AUTH_PROVIDER && isValid) {
58+
return next();
59+
}
60+
61+
res.status(401).json({
62+
error: ApiResponseCodes.ApiKeyNotValid
63+
});
64+
65+
return;
3866
};
3967

4068

packages/api/src/routes/authRoutes.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { NextFunction, Router } from "express";
22
import passport from "passport";
3-
import { keys } from "@switchfeat/core";
3+
import { ApiResponseCodes, keys } from "@switchfeat/core";
44
import { Request, Response } from "express";
5+
import { setErrorResponse } from "../helpers/responseHelper";
56

67
export const authRoutes = Router();
78

@@ -34,10 +35,7 @@ authRoutes.get("/auth/is-auth", (req: Request, res: Response) => {
3435
cookies: req.cookies
3536
});
3637
} else {
37-
res.status(401).json({
38-
success: false,
39-
message: "user failed to authenticate."
40-
});
38+
setErrorResponse(res, ApiResponseCodes.UserAuthFailed);
4139
}
4240
}
4341
});

packages/api/src/routes/flagsRoutes.ts

Lines changed: 14 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import multer from 'multer';
44

55
import * as flagsService from "../services/flagsService";
66
import * as auth from "../managers/auth/passportAuth";
7-
import { dateHelper, dbManager, entityHelper } from "@switchfeat/core";
7+
import { ApiResponseCodes, dateHelper, dbManager, entityHelper } from "@switchfeat/core";
8+
import { setErrorResponse, setSuccessResponse } from "../helpers/responseHelper";
89

910
export const flagRoutesWrapper = (storeManager: Promise<dbManager.DataStoreManager>) : Router => {
1011

@@ -17,19 +18,9 @@ export const flagRoutesWrapper = (storeManager: Promise<dbManager.DataStoreManag
1718
try {
1819

1920
const flags = await flagsService.getFlags("");
20-
21-
res.json({
22-
success: true,
23-
user: req.user,
24-
cookies: req.cookies,
25-
flags: flags
26-
});
21+
setSuccessResponse(res, ApiResponseCodes.Success, flags, req);
2722
} catch (error) {
28-
console.log(error);
29-
res.status(500).json({
30-
success: false,
31-
message: "unable to retrieve flags"
32-
});
23+
setErrorResponse(res, ApiResponseCodes.GenericError);
3324
}
3425
});
3526

@@ -43,14 +34,11 @@ export const flagRoutesWrapper = (storeManager: Promise<dbManager.DataStoreManag
4334
const flagRules = req.body.flagRules;
4435

4536
if (!flagName) {
46-
res.status(401).json({
47-
success: false,
48-
errorCode: "error_input"
49-
});
37+
setErrorResponse(res, ApiResponseCodes.InputMissing);
5038
return;
5139
}
5240

53-
const flagKey = entityHelper.generateKey(flagName);
41+
const flagKey = entityHelper.generateSlug(flagName);
5442
const alreadyInDb = await flagsService.getFlag({ key: flagKey });
5543

5644
if (!alreadyInDb) {
@@ -64,15 +52,9 @@ export const flagRoutesWrapper = (storeManager: Promise<dbManager.DataStoreManag
6452
rules: JSON.parse(flagRules)
6553
});
6654

67-
res.json({
68-
success: true,
69-
errorCode: ""
70-
});
55+
setSuccessResponse(res, ApiResponseCodes.Success, null, req);
7156
} else {
72-
res.json({
73-
success: false,
74-
errorCode: "error_flag_alreadysaved"
75-
});
57+
setErrorResponse(res, ApiResponseCodes.GenericError);
7658
}
7759
});
7860

@@ -87,10 +69,7 @@ export const flagRoutesWrapper = (storeManager: Promise<dbManager.DataStoreManag
8769
const flagRules = req.body.flagRules;
8870

8971
if (!flagKey) {
90-
res.status(401).json({
91-
success: false,
92-
errorCode: "error_input"
93-
});
72+
setErrorResponse(res, ApiResponseCodes.InputMissing);
9473
return;
9574
}
9675

@@ -104,15 +83,9 @@ export const flagRoutesWrapper = (storeManager: Promise<dbManager.DataStoreManag
10483
alreadyInDb.rules = flagRules ? JSON.parse(flagRules) : alreadyInDb.rules;
10584
await flagsService.updateFlag(alreadyInDb);
10685

107-
res.json({
108-
success: true,
109-
errorCode: ""
110-
});
86+
setSuccessResponse(res, ApiResponseCodes.Success, null, req);
11187
} else {
112-
res.json({
113-
success: false,
114-
errorCode: "error_flag_notfound"
115-
});
88+
setErrorResponse(res, ApiResponseCodes.FlagNotFound);
11689
}
11790
});
11891

@@ -123,27 +96,17 @@ export const flagRoutesWrapper = (storeManager: Promise<dbManager.DataStoreManag
12396
const flagKey = req.body.flagKey;
12497

12598
if (!flagKey) {
126-
res.status(401).json({
127-
success: false,
128-
errorCode: "error_input"
129-
});
99+
setErrorResponse(res, ApiResponseCodes.InputMissing);
130100
return;
131101
}
132102

133103
const alreadyInDb = await flagsService.getFlag({ key: flagKey });
134104

135105
if (alreadyInDb) {
136106
await flagsService.deleteFlag(alreadyInDb);
137-
138-
res.json({
139-
success: true,
140-
errorCode: ""
141-
});
107+
setSuccessResponse(res, ApiResponseCodes.Success, null, req);
142108
} else {
143-
res.json({
144-
success: false,
145-
errorCode: "error_flag_notfound"
146-
});
109+
setErrorResponse(res, ApiResponseCodes.FlagNotFound);
147110
}
148111
});
149112

0 commit comments

Comments
 (0)