-
Notifications
You must be signed in to change notification settings - Fork 2
feat: migrate session store to Redis (#79) #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
b0db186
a46bb54
e75e6f6
c1339b5
b2747ab
9c9364d
dfcb843
761c22a
27ba358
250dd29
89ee18b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,25 +34,34 @@ function getSecret() { | |
| * Verify a JWT token and validate the associated server-side session. | ||
| * | ||
| * @param {string} token - The JWT Bearer token to verify | ||
| * @returns {{ user: Object } | { error: string, status: number }} | ||
| * @returns {Promise<{ user: Object } | { error: string, status: number }>} | ||
| * On success: `{ user }` with the decoded JWT payload. | ||
| * On failure: `{ error, status }` with an error message and HTTP status code. | ||
| */ | ||
| export function verifyJwtToken(token) { | ||
| export async function verifyJwtToken(token) { | ||
| let secret; | ||
| try { | ||
| secret = getSecret(); | ||
| } catch { | ||
| return { error: 'Session not configured', status: 500 }; | ||
| } | ||
|
|
||
| let decoded; | ||
| try { | ||
| const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] }); | ||
| if (!getSessionToken(decoded.userId)) { | ||
| return { error: 'Session expired or revoked', status: 401 }; | ||
| } | ||
| return { user: decoded }; | ||
| decoded = jwt.verify(token, secret, { algorithms: ['HS256'] }); | ||
| } catch { | ||
| return { error: 'Invalid or expired token', status: 401 }; | ||
| } | ||
|
|
||
| let sessionToken; | ||
| try { | ||
| sessionToken = await getSessionToken(decoded.userId); | ||
| } catch { | ||
| return { error: 'Session lookup failed', status: 503 }; | ||
| } | ||
BillChirico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if (!sessionToken) { | ||
| return { error: 'Session expired or revoked', status: 401 }; | ||
| } | ||
| return { user: decoded }; | ||
BillChirico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
Comment on lines
+42
to
69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for jwt.sign calls to see what payloads are being signed
rg -n "jwt\.sign\(" --type js -A 3 -B 1Repository: VolvoxLLC/volvox-bot Length of output: 1739 🏁 Script executed: #!/bin/bash
# Search for jwt.verify calls to understand usage patterns
rg -n "jwt\.verify\(" --type js -B 2 -A 5Repository: VolvoxLLC/volvox-bot Length of output: 1739 🏁 Script executed: #!/bin/bash
# Check the file structure and identify jwt import statements
rg -n "import.*jwt|require.*jwt" --type jsRepository: VolvoxLLC/volvox-bot Length of output: 1739 🌐 Web query:
💡 Result: In jsonwebtoken (
Citations:
Guard decoded JWT payload shape before accessing According to jsonwebtoken's specification, Suggested patch export async function verifyJwtToken(token) {
@@
let decoded;
try {
decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
} catch {
return { error: 'Invalid or expired token', status: 401 };
}
+ if (!decoded || typeof decoded !== 'object' || typeof decoded.userId !== 'string') {
+ return { error: 'Invalid or expired token', status: 401 };
+ }
+ const userId = decoded.userId;
+
let sessionToken;
try {
- sessionToken = await getSessionToken(decoded.userId);
+ sessionToken = await getSessionToken(userId);
} catch (err) {
- logError('Session lookup failed', { error: err.message, userId: decoded.userId });
+ const errMsg = err instanceof Error ? err.message : String(err);
+ logError('Session lookup failed', { error: errMsg, userId });
return { error: 'Session lookup failed', status: 503 };
}
@@
if (!sessionToken) {
return { error: 'Session expired or revoked', status: 401 };
}
return { user: decoded };
}🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -172,14 +172,23 @@ function formatBucketLabel(bucket, interval) { | |
| * @returns {boolean} `true` if the user has any of the specified permission flags on the guild, `false` otherwise. | ||
| */ | ||
| async function hasOAuthGuildPermission(user, guildId, anyOfFlags) { | ||
| const accessToken = getSessionToken(user?.userId); | ||
| if (!accessToken) return false; | ||
| const guilds = await fetchUserGuilds(user.userId, accessToken); | ||
| const guild = guilds.find((g) => g.id === guildId); | ||
| if (!guild) return false; | ||
| const permissions = Number(guild.permissions); | ||
| if (Number.isNaN(permissions)) return false; | ||
| return (permissions & anyOfFlags) !== 0; | ||
| try { | ||
| const accessToken = await getSessionToken(user?.userId); | ||
| if (!accessToken) return false; | ||
| const guilds = await fetchUserGuilds(user.userId, accessToken); | ||
| const guild = guilds.find((g) => g.id === guildId); | ||
| if (!guild) return false; | ||
| const permissions = Number(guild.permissions); | ||
| if (Number.isNaN(permissions)) return false; | ||
| return (permissions & anyOfFlags) !== 0; | ||
| } catch (err) { | ||
| error('Redis error in hasOAuthGuildPermission', { | ||
| error: err.message, | ||
| userId: user?.userId, | ||
| guildId, | ||
| }); | ||
| throw err; | ||
| } | ||
BillChirico marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
174
to
+191
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Harden error logging: don’t assume Both catch blocks log Suggested patch async function hasOAuthGuildPermission(user, guildId, anyOfFlags) {
try {
const accessToken = await getSessionToken(user?.userId);
if (!accessToken) return false;
@@
return (permissions & anyOfFlags) !== 0;
} catch (err) {
+ const errMsg = err instanceof Error ? err.message : String(err);
error('Error in hasOAuthGuildPermission (session lookup or guild fetch)', {
- error: err.message,
+ error: errMsg,
userId: user?.userId,
guildId,
});
throw err;
}
}
@@
try {
accessToken = await getSessionToken(req.user?.userId);
} catch (err) {
+ const errMsg = err instanceof Error ? err.message : String(err);
- error('Redis error fetching session token in GET /guilds', {
- error: err.message,
+ error('Session store error fetching session token in GET /guilds', {
+ error: errMsg,
userId: req.user?.userId,
});
return res.status(503).json({ error: 'Session store unavailable' });
}Also applies to: 319-328 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -307,7 +316,16 @@ router.get('/', async (req, res) => { | |
| return res.json(ownerGuilds); | ||
| } | ||
|
|
||
| const accessToken = getSessionToken(req.user?.userId); | ||
| let accessToken; | ||
| try { | ||
| accessToken = await getSessionToken(req.user?.userId); | ||
| } catch (err) { | ||
| error('Redis error fetching session token in GET /guilds', { | ||
| error: err.message, | ||
| userId: req.user?.userId, | ||
| }); | ||
| return res.status(503).json({ error: 'Session store unavailable' }); | ||
| } | ||
| if (!accessToken) { | ||
| return res.status(401).json({ error: 'Missing access token' }); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.