Skip to content
This repository was archived by the owner on Apr 19, 2023. It is now read-only.

Commit a4e87d5

Browse files
✨ User impersonation by superadmins
1 parent 2d173a6 commit a4e87d5

File tree

4 files changed

+35
-4
lines changed

4 files changed

+35
-4
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Staart is a Node.js backend starter for SaaS startups written in TypeScript. It
3030
- [ ] 🇪🇺 Check for location with logging in (i.e., "New location" with approved subnets)
3131
- [x] 👩‍💻 MySQL schema matching interfaces
3232
- [x] 🔐 Event logging and history (logins, settings changes, etc.)
33-
- [ ] 💳 "Magic wand" for user impersonation by superadmins
33+
- [x] 💳 "Magic wand" for user impersonation by super-admins
3434
- [x] 👩‍💻 Express middleware for token check which returns user
3535
- [ ] 🔐 Support for refresh tokens (i.e., "Keep me logged in for 30 days")
3636
- [ ] 🔐 Two-factor authentication with TOTP (and Twilio?)

src/rest/auth.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
ErrorCode,
2323
MembershipRole,
2424
Templates,
25-
Tokens
25+
Tokens,
26+
UserRole
2627
} from "../interfaces/enum";
2728
import { compare, hash } from "bcrypt";
2829
import { deleteSensitiveInfoUser } from "../helpers/utils";
@@ -174,3 +175,18 @@ export const loginWithGoogleVerify = async (code: string, locals: Locals) => {
174175
refresh: await refreshToken(user.id)
175176
};
176177
};
178+
179+
export const impersonate = async (
180+
tokenUserId: number,
181+
impersonateUserId: number
182+
) => {
183+
const tokenUser = await getUser(tokenUserId);
184+
if (tokenUser.role != UserRole.ADMIN)
185+
throw new Error(ErrorCode.INSUFFICIENT_PERMISSION);
186+
const user = await getUser(impersonateUserId);
187+
if (!user.id) throw new Error(ErrorCode.USER_NOT_FOUND);
188+
return {
189+
token: await loginToken(deleteSensitiveInfoUser(user)),
190+
refresh: await refreshToken(user.id)
191+
};
192+
};

src/routes/auth.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
register,
88
validateRefreshToken,
99
loginWithGoogleLink,
10-
loginWithGoogleVerify
10+
loginWithGoogleVerify,
11+
impersonate
1112
} from "../rest/auth";
1213
import { verifyToken } from "../helpers/jwt";
1314

@@ -99,3 +100,11 @@ export const routeAuthLoginWithGoogleVerify = async (
99100
if (!code) throw new Error(ErrorCode.MISSING_TOKEN);
100101
res.json(await loginWithGoogleVerify(code, res.locals));
101102
};
103+
104+
export const routeAuthImpersonate = async (req: Request, res: Response) => {
105+
const tokenUserId = res.locals.token.id;
106+
const impersonateUserId = req.params.id;
107+
if (!tokenUserId || !impersonateUserId)
108+
throw new Error(ErrorCode.MISSING_FIELD);
109+
res.json(await impersonate(tokenUserId, impersonateUserId));
110+
};

src/routes/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
routeAuthRegister,
2323
routeAuthRefresh,
2424
routeAuthLoginWithGoogleLink,
25-
routeAuthLoginWithGoogleVerify
25+
routeAuthLoginWithGoogleVerify,
26+
routeAuthImpersonate
2627
} from "./auth";
2728
import { routeMembershipGet, routeMembershipCreate } from "./membership";
2829

@@ -52,6 +53,11 @@ const routesAuth = (app: Application) => {
5253
);
5354
app.get("/auth/google/link", asyncHandler(routeAuthLoginWithGoogleLink));
5455
app.post("/auth/google/verify", asyncHandler(routeAuthLoginWithGoogleVerify));
56+
app.get(
57+
"/auth/impersonate/:id",
58+
authHandler,
59+
asyncHandler(routeAuthImpersonate)
60+
);
5561
};
5662

5763
const routesUser = (app: Application) => {

0 commit comments

Comments
 (0)