Skip to content

Commit

Permalink
feat(auth): enhance applications entitlements scope handling and erro…
Browse files Browse the repository at this point in the history
…r messaging
  • Loading branch information
chimpdev committed Feb 17, 2025
1 parent 1ec2fa2 commit 0e2d09e
Show file tree
Hide file tree
Showing 11 changed files with 58 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export default function MyBots() {
const grantScopeIntervalRef = useRef(null);

function grantScope() {
const authorizeUrl = new URL(config.applicationsEntitlementsScopeURL());
console.log('grantScope', user);
const authorizeUrl = new URL(config.applicationsEntitlementsScopeURL(user.id));

window.open(authorizeUrl, '_blank');

Expand All @@ -51,6 +52,12 @@ export default function MyBots() {

toast.success(t('accountPage.tabs.myBots.sections.newBot.toast.permissionGranted'));
}

if (applicationsEntitlementsScopeGranted === 'error') {
clearInterval(grantScopeIntervalRef.current);

toast.error(t('accountPage.tabs.myBots.sections.newBot.toast.permissionGrantError'));
}
} catch (error) {
clearInterval(grantScopeIntervalRef.current);
toast.error(error.message);
Expand Down
4 changes: 2 additions & 2 deletions client/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ const config = {
(process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : 'https://discord.place') + pathname
)}`;
},
applicationsEntitlementsScopeURL() {
return `${this.api.url}/auth/applicationsEntitlementsScope`;
applicationsEntitlementsScopeURL(userId) {
return `${this.api.url}/auth/applicationsEntitlementsScope?userId=${userId}`;
},
validateSlug: function slugValidation(value) {
return /^(?!-)(?!.*--)(?!.*-$)[a-zA-Z0-9-]{3,32}$/.test(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default cache(() => {

try {
const response = await axios.get(url, { withCredentials: true });
resolve(response.data.granted === true);
resolve(response.data.granted);
} catch (error) {
reject(error instanceof axios.AxiosError ? (error.response?.data?.error || error.message) : error.message);
}
Expand Down
3 changes: 2 additions & 1 deletion client/locales/az.json
Original file line number Diff line number Diff line change
Expand Up @@ -1913,7 +1913,8 @@
},
"toast": {
"grantingPermissionTimeout": "İcazə verilməsi tələbi zaman aşımına uğradı. Zəhmət olmasa yenidən cəhd edin.",
"permissionGranted": "İcazə uğurla verildi!"
"permissionGranted": "İcazə uğurla verildi!",
"permissionGrantError": "Siz icazə verməyi razılaşmadınız və ya bir xəta baş verdi. Yenidən cəhd edin."
}
},
"addBot": {
Expand Down
3 changes: 2 additions & 1 deletion client/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1914,7 +1914,8 @@
},
"toast": {
"grantingPermissionTimeout": "Granting permission timed out. Please try again.",
"permissionGranted": "Permission granted successfully!"
"permissionGranted": "Permission granted successfully!",
"permissionGrantError": "You did not agree to give permission or an error occurred. Try again."
}
},
"addBot": {
Expand Down
3 changes: 2 additions & 1 deletion client/locales/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1914,7 +1914,8 @@
},
"toast": {
"grantingPermissionTimeout": "İzin verme isteği zaman aşımına uğradı. Lütfen tekrar deneyin.",
"permissionGranted": "İzin başarıyla verildi!"
"permissionGranted": "İzin başarıyla verildi!",
"permissionGrantError": "İzin vermeyi kabul etmediniz ya da bir hata oluştu. Tekrar deneyin."
}
},
"addBot": {
Expand Down
1 change: 1 addition & 0 deletions server/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ module.exports = class Client {
this.client.blockedIps = new Discord.Collection();
this.client.currentlyUploadingEmojiPack = new Discord.Collection();
this.client.languageCache = new Discord.Collection();
this.client.applicationsEntitlementsScopeCallbackError = new Discord.Collection();

return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ module.exports = {
const user = await User.findOne({ id: request.user.id });
if (!user) return response.sendError('User not found.', 404);

// Remove the error flag if it exists
const applicationsEntitlementsScopeCallbackError = client.applicationsEntitlementsScopeCallbackError.get(user.id);
if (applicationsEntitlementsScopeCallbackError) client.applicationsEntitlementsScopeCallbackError.delete(user.id);

return response.json({
granted: user.applicationsEntitlementsScopeGranted === true
granted: applicationsEntitlementsScopeCallbackError ? 'error' : user.applicationsEntitlementsScopeGranted === true
});
}
]
Expand Down
17 changes: 15 additions & 2 deletions server/src/routes/auth/applicationsEntitlementsScope/callback.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
const validateRequest = require('@/utils/middlewares/validateRequest');
const { query, matchedData } = require('express-validator');
const { query, cookie, matchedData } = require('express-validator');
const useRateLimiter = require('@/utils/useRateLimiter');
const getAccessToken = require('@/utils/getAccessToken');
const authCallback = require('@/utils/authCallback');

module.exports = {
get: [
query('code')
.optional()
.isString().withMessage('Code must be a string.')
.matches(/^[a-zA-Z0-9]{30}$/).withMessage('Invalid code.'),
query('error')
.optional()
.isString().withMessage('Error must be a string.'),
query('state')
.isString().withMessage('State must be a string.')
.matches(/^[a-zA-Z0-9]{32}$/).withMessage('Invalid state.'),
cookie('applicationsEntitlementsScope_userId')
.isString().withMessage('User ID must be a string.')
.matches(/^\d{17,19}$/).withMessage('Invalid user ID.'),
validateRequest,
useRateLimiter({ maxRequests: 5, perMinutes: 1 }),
async (request, response) => {
const { code, state } = matchedData(request);
const { code, error, state, applicationsEntitlementsScope_userId: userId } = matchedData(request);

if (!code || error) {
client.applicationsEntitlementsScopeCallbackError.set(userId, true);

return response.send('<script src="/scripts/closeWindow.js"></script>');
}

const storedState = request.cookies.applicationsEntitlementsScope_state;
if (!storedState) return response.sendError('State not found.', 400);
Expand Down
9 changes: 9 additions & 0 deletions server/src/routes/auth/applicationsEntitlementsScope/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
const crypto = require('node:crypto');
const { query, matchedData } = require('express-validator');
const validateRequest = require('@/utils/middlewares/validateRequest');

module.exports = {
get: [
query('userId')
.isString().withMessage('User ID must be a string.')
.matches(/^\d{17,19}$/).withMessage('Invalid user ID.'),
validateRequest,
async (request, response) => {
const { userId } = matchedData(request);

const state = crypto.randomBytes(16).toString('hex');

response.cookie('applicationsEntitlementsScope_state', state, { httpOnly: true, maxAge: 1000 * 60 * 5 });
response.cookie('applicationsEntitlementsScope_userId', userId, { httpOnly: true, maxAge: 1000 * 60 * 5 });

const redirectUrl = `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&redirect_uri=${config.backendUrl}/auth/applicationsEntitlementsScope/callback&response_type=code&scope=${encodeURIComponent(config.discordScopes.concat('applications.entitlements').join(' '))}&state=${state}&prompt=none`;

Expand Down
11 changes: 11 additions & 0 deletions server/src/utils/bots/isUserBotOwner.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ async function isUserBotOwner(userId, botId) {
} catch (error) {
if (error instanceof axios.AxiosError) {
if (error.response?.status === 403 && error.response?.data?.code === 20012) return false;
if (error.response?.status === 401) {
// User revoked the applications.entitlements scope from the bot
await User.updateOne(
{ id: userId },
{ applicationsEntitlementsScopeGranted: false }
);

logger.info(`User with ID ${userId} revoked the applications.entitlements scope.`);

return false;
}
}

logger.error(`There was an error while checking if user with ID ${user.id} is owner of bot with ID ${botId}:`, error);
Expand Down

0 comments on commit 0e2d09e

Please sign in to comment.