mirror of
https://github.com/Hessenuk/DiscordTickets.git
synced 2025-09-05 17:51:27 +03:00
feat: separate user and admin auth, redirect to settings after invite
This commit is contained in:
55
src/http.js
55
src/http.js
@@ -1,14 +1,16 @@
|
||||
const fastify = require('fastify')({ trustProxy: process.env.HTTP_TRUST_PROXY === 'true' });
|
||||
const oauth = require('@fastify/oauth2');
|
||||
const { randomBytes } = require('crypto');
|
||||
const { short } = require('leeks.js');
|
||||
const { join } = require('path');
|
||||
const { files } = require('node-dir');
|
||||
const { getPrivilegeLevel } = require('./lib/users');
|
||||
const { format } = require('util');
|
||||
|
||||
process.env.ORIGIN = process.env.HTTP_INTERNAL || process.env.HTTP_EXTERNAL;
|
||||
|
||||
module.exports = async client => {
|
||||
// for file uploads
|
||||
fastify.register(require('@fastify/multipart'), { limits: { fileSize: 2**27 } }); // 128 MiB
|
||||
|
||||
// cookies plugin, must be registered before oauth2 since oauth2@7.2.0
|
||||
fastify.register(require('@fastify/cookie'));
|
||||
|
||||
@@ -21,36 +23,6 @@ module.exports = async client => {
|
||||
secret: process.env.ENCRYPTION_KEY,
|
||||
});
|
||||
|
||||
// oauth2 plugin
|
||||
fastify.states = new Map();
|
||||
fastify.register(oauth, {
|
||||
callbackUri: `${process.env.HTTP_EXTERNAL}/auth/callback`,
|
||||
callbackUriParams: { prompt: 'none' },
|
||||
checkStateFunction: async req => {
|
||||
if (req.query.state !== req.cookies['oauth2-redirect-state']) {
|
||||
throw new Error('Invalid state');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
credentials: {
|
||||
auth: oauth.DISCORD_CONFIGURATION,
|
||||
client: {
|
||||
id: client.user.id,
|
||||
secret: process.env.DISCORD_SECRET,
|
||||
},
|
||||
},
|
||||
generateStateFunction: req => {
|
||||
const state = randomBytes(8).toString('hex');
|
||||
fastify.states.set(state, req.query.r);
|
||||
return state;
|
||||
},
|
||||
name: 'discord',
|
||||
redirectStateCookieName: 'oauth2-redirect-state',
|
||||
scope: ['applications.commands.permissions.update', 'guilds', 'identify'],
|
||||
startRedirectPath: '/auth/login',
|
||||
});
|
||||
|
||||
|
||||
// auth
|
||||
fastify.decorate('authenticate', async (req, res) => {
|
||||
try {
|
||||
@@ -111,7 +83,14 @@ module.exports = async client => {
|
||||
error: 'Unavailable For Legal Reasons',
|
||||
message: 'This guild has been banned for breaking the terms of service.',
|
||||
statusCode: 451,
|
||||
|
||||
});
|
||||
}
|
||||
if (!req.user.scopes?.includes('applications.commands.permissions.update')) {
|
||||
return res.code(401).send({
|
||||
elevate: 'admin',
|
||||
error: 'Unauthorised',
|
||||
message: 'Extra scopes required; reauthenticate.',
|
||||
statusCode: 401,
|
||||
});
|
||||
}
|
||||
const guildMember = await guild.members.fetch(userId);
|
||||
@@ -161,11 +140,15 @@ module.exports = async client => {
|
||||
: '&a') + responseTime + 'ms';
|
||||
const level = req.routeOptions.url === '/status'
|
||||
? 'debug'
|
||||
: req.routeOptions.url === '/*'
|
||||
: req.routeOptions.url === '/*'
|
||||
? 'verbose'
|
||||
: 'info';
|
||||
client.log[level].http(short(`${req.id} ${req.ip} ${req.method} ${req.routeOptions.url ?? '*'} &m-+>&r ${status}&b in ${responseTime}`));
|
||||
if (!req.routeOptions.url) client.log.verbose.http(`${req.id} ${req.method} ${req.url}`);
|
||||
client.log[level].http(
|
||||
format(
|
||||
short(`${req.id} ${req.ip} ${req.method} %s &m-+>&r ${status}&b in ${responseTime}`),
|
||||
req.url,
|
||||
),
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@@ -1 +0,0 @@
|
||||
module.exports.domain = new URL(process.env.HTTP_EXTERNAL).hostname;
|
@@ -1,7 +1,27 @@
|
||||
const { domain } = require('../../lib/http');
|
||||
|
||||
module.exports.get = () => ({
|
||||
handler: async function (req, res) { // MUST NOT use arrow function syntax
|
||||
handler: async function (req, res) {
|
||||
const cookie = req.cookies['oauth2-state'];
|
||||
if (!cookie) {
|
||||
return res.code(400).send({
|
||||
error: 'Bad Request',
|
||||
message: 'State is missing.',
|
||||
statusCode: 400,
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
const state = new URLSearchParams(cookie);
|
||||
if (state.get('secret') !== req.query.state) {
|
||||
return res.code(400).send({
|
||||
error: 'Bad Request',
|
||||
message: 'Invalid state.',
|
||||
statusCode: 400,
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: check if req.query.permissions are correct
|
||||
|
||||
const data = await (await fetch('https://discord.com/api/oauth2/token', {
|
||||
body: new URLSearchParams({
|
||||
client_id: req.routeOptions.config.client.user.id,
|
||||
@@ -14,22 +34,30 @@ module.exports.get = () => ({
|
||||
method: 'POST',
|
||||
})).json();
|
||||
|
||||
const user = await (await fetch('https://discordapp.com/api/users/@me', { headers: { 'Authorization': `Bearer ${data.access_token}` } })).json();
|
||||
const redirect = (data.guild?.id && `/settings/${data.guild?.id}`) || state.get('redirect') || '/';
|
||||
|
||||
const bearerOptions = { headers: { 'Authorization': `Bearer ${data.access_token}` } };
|
||||
const user = await (await fetch('https://discordapp.com/api/users/@me', bearerOptions)).json();
|
||||
|
||||
let scopes;
|
||||
if (data.scope) {
|
||||
scopes = data.scope.split(' ');
|
||||
} else {
|
||||
const auth = await (await fetch('https://discordapp.com/api/oauth2/@me', bearerOptions)).json();
|
||||
scopes = auth.scopes;
|
||||
}
|
||||
|
||||
const token = this.jwt.sign({
|
||||
accessToken: data.access_token,
|
||||
avatar: user.avatar,
|
||||
expiresAt: Date.now() + (data.expires_in * 1000),
|
||||
id: user.id,
|
||||
locale: user.locale,
|
||||
scopes,
|
||||
username: user.username,
|
||||
});
|
||||
|
||||
// note: if data.guild is present, guild_id and permissions should also be in req.query
|
||||
const redirect = this.states.get(req.query.state) || (data.guild?.id && `/settings/${data.guild?.id}`) || '/';
|
||||
this.states.delete(req.query.state);
|
||||
|
||||
res.setCookie('token', token, {
|
||||
domain,
|
||||
httpOnly: true,
|
||||
maxAge: data.expires_in,
|
||||
path: '/',
|
||||
|
44
src/routes/auth/login.js
Normal file
44
src/routes/auth/login.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const { randomBytes } = require('crypto');
|
||||
|
||||
module.exports.get = () => ({
|
||||
handler: async function (req, res) {
|
||||
const { client } = req.routeOptions.config;
|
||||
|
||||
const state = new URLSearchParams({
|
||||
redirect: req.query.r ?? '',
|
||||
secret: randomBytes(8).toString('hex'),
|
||||
});
|
||||
|
||||
res.setCookie('oauth2-state', state.toString(), {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
const params = {
|
||||
client_id: client.user.id,
|
||||
// ? prompt: 'none',
|
||||
redirect_uri: `${process.env.HTTP_EXTERNAL}/auth/callback`, // if not set defaults to first allowed
|
||||
response_type: 'code',
|
||||
scope: 'guilds identify',
|
||||
state: state.get('secret'),
|
||||
};
|
||||
|
||||
if (req.query.invite !== undefined) {
|
||||
params.prompt = 'consent'; // already implied by the bot scope
|
||||
params.scope = 'applications.commands applications.commands.permissions.update bot ' + params.scope;
|
||||
params.integration_type = '0';
|
||||
params.permissions = '268561488';
|
||||
if (req.query.guild) {
|
||||
params.guild_id = req.query.guild;
|
||||
params.disable_guild_select = 'true';
|
||||
}
|
||||
} else if (req.query.role === 'admin') { // invite implies admin already
|
||||
params.scope = 'applications.commands.permissions.update ' + params.scope;
|
||||
}
|
||||
|
||||
const url = new URL('https://discord.com/oauth2/authorize');
|
||||
url.search = new URLSearchParams(params);
|
||||
|
||||
res.redirect(url.toString());
|
||||
},
|
||||
});
|
@@ -1,5 +1,3 @@
|
||||
const { domain } = require('../../lib/http');
|
||||
|
||||
module.exports.get = fastify => ({
|
||||
handler: async function (req, res) {
|
||||
const { accessToken } = req.user;
|
||||
@@ -15,7 +13,6 @@ module.exports.get = fastify => ({
|
||||
});
|
||||
|
||||
res.clearCookie('token', {
|
||||
domain,
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'Strict',
|
||||
|
@@ -1,29 +1,5 @@
|
||||
const { randomBytes } = require('crypto');
|
||||
|
||||
module.exports.get = () => ({
|
||||
handler: async function (req, res) {
|
||||
const { client } = req.routeOptions.config;
|
||||
|
||||
const state = randomBytes(8).toString('hex');
|
||||
this.states.set(state, null);
|
||||
|
||||
const url = new URL('https://discord.com/oauth2/authorize');
|
||||
url.searchParams.set('response_type', 'code');
|
||||
url.searchParams.set('client_id', client.user.id);
|
||||
url.searchParams.set('prompt', 'none');
|
||||
url.searchParams.set('redirect_uri', `${process.env.HTTP_EXTERNAL}/auth/callback`); // window.location.origin
|
||||
url.searchParams.set('scope', 'applications.commands applications.commands.permissions.update bot guilds identify');
|
||||
url.searchParams.set('permissions', '268561488');
|
||||
|
||||
if (req.query.guild) {
|
||||
url.searchParams.set('guild_id', req.query.guild);
|
||||
url.searchParams.set('disable_guild_select', 'true');
|
||||
}
|
||||
|
||||
res.setCookie('oauth2-redirect-state', state, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
res.redirect(url.toString());
|
||||
res.redirect(`/auth/login?invite&guild=${req.query.guild || ''}`);
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user