Skip to content

Commit

Permalink
feat(sdk): apikeys authentication (#49)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dev-bre authored Jul 27, 2023
1 parent 8013402 commit bb72aac
Show file tree
Hide file tree
Showing 46 changed files with 1,000 additions and 271 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ packages/api/db.switchfeat.segments
db.switchfeat.flags
db.switchfeat.segments
db.switchfeat.users
db.switchfeat.sdkauths
packages/api/db.switchfeat.sdkauths
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"lint": "lerna run lint",
"start": "cd ./packages/api && npm run start",
"dev:start-api": "cd ./packages/api && npm run dev:start",
"dev:start-ui": "cd ./packages/ui && npm run start"
"dev:start-ui": "cd ./packages/ui && npm run start",
"dev:start-tw:watch": "cd ./packages/ui && npm run build:tw:watch"
}
}
27 changes: 27 additions & 0 deletions packages/api/src/helpers/responseHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ApiResponseCode, ResponseModel } from "@switchfeat/core";
import { Request, Response } from "express";

export const setErrorResponse = (resp: Response, error: ApiResponseCode) => {
console.log(error);
resp.status(error.statusCode).json({
success: false,
error: error,
data: null
} as ResponseModel<null>);
};

export const setSuccessResponse = <T extends object | null>(resp: Response, code: ApiResponseCode, data: T, req?: Request) => {
console.log(code);

const response = {
success: true,
data
} as ResponseModel<T>;

if (req) {
response.user = req.user;
response.cookies = req.cookies;
}

resp.status(code.statusCode).json(response);
};
2 changes: 2 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as passportAuth from "./managers/auth/passportAuth";
import { getDataStoreManager } from './managers/auth/dataStoreManager';
import { segmentsRoutesWrapper } from './routes/segmentsRoutes';
import { sdkRoutesWrapper } from './routes/sdkRoutes';
import { sdkAuthRoutesWrapper } from './routes/sdkAuthRoutes';

dotenv.config();
const env = process.env.NODE_ENV;
Expand Down Expand Up @@ -52,6 +53,7 @@ app.use(authRoutes);
app.use(flagRoutesWrapper(dataStoreManagerPromise));
app.use(segmentsRoutesWrapper(dataStoreManagerPromise));
app.use(sdkRoutesWrapper(dataStoreManagerPromise));
app.use(sdkAuthRoutesWrapper(dataStoreManagerPromise));

if (env !== "dev") {
app.get('*', (req, res) => {
Expand Down
70 changes: 49 additions & 21 deletions packages/api/src/managers/auth/passportAuth.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,68 @@
import passport from "passport";
import { Request, Response, NextFunction, Express } from "express";
import * as userService from "../../services/usersService";
import * as sdkAuthService from "../../services/sdkAuthService";
import { googleStrategy } from "./googleAuth";
import { keys } from "@switchfeat/core";
import { ApiResponseCodes, keys } from "@switchfeat/core";


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

app.use(passport.initialize());
app.use(passport.initialize());

// serialize the user.id to save in the cookie session
// so the browser will remember the user when login
passport.serializeUser((_req, user, done) => {
done(null, user);
});
// serialize the user.id to save in the cookie session
// so the browser will remember the user when login
passport.serializeUser((_req, user, done) => {
done(null, user);
});

// deserialize the cookieUserId to user in the database
passport.deserializeUser(async (id: string, done) => {
const currentUser = await userService.getUser({ userId: id });
done(currentUser === null ? "user not found." : null, { user: currentUser });
});
// deserialize the cookieUserId to user in the database
passport.deserializeUser(async (id: string, done) => {
const currentUser = await userService.getUser({ userId: id });
done(currentUser === null ? "user not found." : null, { user: currentUser });
});

if (keys.AUTH_PROVIDER === "google") {
console.log(" -> Google auth active");
passport.use(googleStrategy());
}
if (keys.AUTH_PROVIDER === "google") {
console.log(" -> Google auth active");
passport.use(googleStrategy());
}
};

export const isAuthenticated = (req: Request, res: Response, next: NextFunction) => {
if (!keys.AUTH_PROVIDER || req.isAuthenticated()) {
return next();
}
res.redirect("/");
};

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

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

export const isAuthenticated = (req: Request, res: Response, next: NextFunction) => {
if (!keys.AUTH_PROVIDER || req.isAuthenticated()) {
return next();
}
res.redirect("/");
return;
}
const foundInDb = await sdkAuthService.getSdkAuth({ apiKey: apiKey });

const isValid = foundInDb !== null && foundInDb.expiresOn > new Date();

if (!keys.AUTH_PROVIDER && isValid) {
return next();
}

res.status(401).json({
error: ApiResponseCodes.ApiKeyNotValid
});

return;
};


8 changes: 3 additions & 5 deletions packages/api/src/routes/authRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { NextFunction, Router } from "express";
import passport from "passport";
import { keys } from "@switchfeat/core";
import { ApiResponseCodes, keys } from "@switchfeat/core";
import { Request, Response } from "express";
import { setErrorResponse } from "../helpers/responseHelper";

export const authRoutes = Router();

Expand Down Expand Up @@ -34,10 +35,7 @@ authRoutes.get("/auth/is-auth", (req: Request, res: Response) => {
cookies: req.cookies
});
} else {
res.status(401).json({
success: false,
message: "user failed to authenticate."
});
setErrorResponse(res, ApiResponseCodes.UserAuthFailed);
}
}
});
Expand Down
65 changes: 14 additions & 51 deletions packages/api/src/routes/flagsRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import multer from 'multer';

import * as flagsService from "../services/flagsService";
import * as auth from "../managers/auth/passportAuth";
import { dateHelper, dbManager, entityHelper } from "@switchfeat/core";
import { ApiResponseCodes, dateHelper, dbManager, entityHelper } from "@switchfeat/core";
import { setErrorResponse, setSuccessResponse } from "../helpers/responseHelper";

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

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

const flags = await flagsService.getFlags("");

res.json({
success: true,
user: req.user,
cookies: req.cookies,
flags: flags
});
setSuccessResponse(res, ApiResponseCodes.Success, flags, req);
} catch (error) {
console.log(error);
res.status(500).json({
success: false,
message: "unable to retrieve flags"
});
setErrorResponse(res, ApiResponseCodes.GenericError);
}
});

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

if (!flagName) {
res.status(401).json({
success: false,
errorCode: "error_input"
});
setErrorResponse(res, ApiResponseCodes.InputMissing);
return;
}

const flagKey = entityHelper.generateKey(flagName);
const flagKey = entityHelper.generateSlug(flagName);
const alreadyInDb = await flagsService.getFlag({ key: flagKey });

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

res.json({
success: true,
errorCode: ""
});
setSuccessResponse(res, ApiResponseCodes.Success, null, req);
} else {
res.json({
success: false,
errorCode: "error_flag_alreadysaved"
});
setErrorResponse(res, ApiResponseCodes.GenericError);
}
});

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

if (!flagKey) {
res.status(401).json({
success: false,
errorCode: "error_input"
});
setErrorResponse(res, ApiResponseCodes.InputMissing);
return;
}

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

res.json({
success: true,
errorCode: ""
});
setSuccessResponse(res, ApiResponseCodes.Success, null, req);
} else {
res.json({
success: false,
errorCode: "error_flag_notfound"
});
setErrorResponse(res, ApiResponseCodes.FlagNotFound);
}
});

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

if (!flagKey) {
res.status(401).json({
success: false,
errorCode: "error_input"
});
setErrorResponse(res, ApiResponseCodes.InputMissing);
return;
}

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

if (alreadyInDb) {
await flagsService.deleteFlag(alreadyInDb);

res.json({
success: true,
errorCode: ""
});
setSuccessResponse(res, ApiResponseCodes.Success, null, req);
} else {
res.json({
success: false,
errorCode: "error_flag_notfound"
});
setErrorResponse(res, ApiResponseCodes.FlagNotFound);
}
});

Expand Down
Loading

0 comments on commit bb72aac

Please sign in to comment.