oauth.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. const crypto = require('crypto');
  2. const cheerio = require('cheerio');
  3. const {defaultPermissions} = require('../util/default.json');
  4. const {settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
  5. const DiscordOauth2 = require('discord-oauth2');
  6. const oauth = new DiscordOauth2( {
  7. clientId: process.env.bot,
  8. clientSecret: process.env.secret,
  9. redirectUri: process.env.dashboard
  10. } );
  11. const file = require('fs').readFileSync('./dashboard/login.html');
  12. /**
  13. * Let a user login
  14. * @param {import('http').ServerResponse} res - The server response
  15. * @param {String} [state] - The user state
  16. * @param {String} [action] - The action the user made
  17. */
  18. function dashboard_login(res, state, action) {
  19. if ( state ) {
  20. if ( settingsData.has(state) ) {
  21. res.writeHead(302, {Location: '/'});
  22. return res.end();
  23. }
  24. res.setHeader('Set-Cookie', [`wikibot="${state}"; Max-Age=0; HttpOnly`]);
  25. }
  26. var $ = cheerio.load(file);
  27. $('.guild#invite a').attr('href', oauth.generateAuthUrl( {
  28. scope: ['identify', 'guilds', 'bot'],
  29. permissions: defaultPermissions, state
  30. } ));
  31. let responseCode = 200;
  32. let notice = '';
  33. if ( action === 'failed' ) {
  34. responseCode = 400;
  35. notice = createNotice($, {
  36. title: 'Login failed!',
  37. text: 'An error occurred while logging you in, please try again.'
  38. });
  39. }
  40. if ( action === 'unauthorized' ) {
  41. responseCode = 401;
  42. notice = createNotice($, {
  43. title: 'Not logged in!',
  44. text: 'Please login before you can change any settings.'
  45. });
  46. }
  47. if ( action === 'logout' ) {
  48. notice = createNotice($, {
  49. title: 'Successfully logged out!',
  50. text: 'You have been successfully logged out. To change any settings you need to login again.'
  51. });
  52. }
  53. $('replace#notice').replaceWith(notice);
  54. state = crypto.randomBytes(16).toString("hex");
  55. while ( settingsData.has(state) ) {
  56. state = crypto.randomBytes(16).toString("hex");
  57. }
  58. let url = oauth.generateAuthUrl( {
  59. scope: ['identify', 'guilds'],
  60. prompt: 'none', state
  61. } );
  62. $('a#login').attr('href', url);
  63. $('replace#text').replaceWith(`<a href="${url}">Login</a>`);
  64. let body = $.html();
  65. res.writeHead(responseCode, {
  66. 'Set-Cookie': [`wikibot="${state}"; HttpOnly`],
  67. 'Content-Length': body.length
  68. });
  69. res.write( body );
  70. return res.end();
  71. }
  72. /**
  73. * Load oauth data of a user
  74. * @param {import('http').ServerResponse} res - The server response
  75. * @param {String} state - The user state
  76. * @param {URLSearchParams} searchParams - The url parameters
  77. * @param {String} [lastGuild] - The guild to return to
  78. */
  79. function dashboard_oauth(res, state, searchParams, lastGuild) {
  80. if ( settingsData.has(state) ) {
  81. res.writeHead(302, {Location: '/'});
  82. return res.end();
  83. }
  84. if ( state !== searchParams.get('state') || !searchParams.get('code') ) {
  85. res.writeHead(302, {Location: '/login?action=unauthorized'});
  86. return res.end();
  87. }
  88. return oauth.tokenRequest( {
  89. scope: ['identify', 'guilds'],
  90. code: searchParams.get('code'),
  91. grantType: 'authorization_code'
  92. } ).then( ({access_token}) => {
  93. return Promise.all([
  94. oauth.getUser(access_token),
  95. oauth.getUserGuilds(access_token)
  96. ]).then( ([user, guilds]) => {
  97. guilds = guilds.filter( guild => {
  98. return ( guild.owner || hasPerm(guild.permissions, 'MANAGE_GUILD') );
  99. } ).map( guild => {
  100. return {
  101. id: guild.id,
  102. name: guild.name,
  103. acronym: guild.name.replace( /'s /g, ' ' ).replace( /\w+/g, e => e[0] ).replace( /\s/g, '' ),
  104. icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
  105. + ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) + '?size=128' : null ),
  106. permissions: guild.permissions
  107. };
  108. } );
  109. sendMsg( {
  110. type: 'isMemberAll',
  111. guilds: guilds.map( guild => guild.id )
  112. } ).then( response => {
  113. let isMember = new Map();
  114. let notMember = new Map();
  115. response.forEach( (guild, i) => {
  116. if ( guild ) isMember.set(guilds[i].id, guilds[i]);
  117. else notMember.set(guilds[i].id, guilds[i]);
  118. } );
  119. settingsData.set(`${state}-${user.id}`, {
  120. state: `${state}-${user.id}`,
  121. access_token,
  122. user: {
  123. id: user.id,
  124. username: user.username,
  125. discriminator: user.discriminator,
  126. avatar: 'https://cdn.discordapp.com/' + ( user.avatar ?
  127. `avatars/${user.id}/${user.avatar}.` +
  128. ( user.avatar.startsWith( 'a_' ) ? 'gif' : 'png' ) :
  129. `embed/avatars/${user.discriminator % 5}.png` ) + '?size=64',
  130. locale: user.locale
  131. },
  132. guilds: {isMember, notMember}
  133. });
  134. res.writeHead(302, {
  135. Location: ( lastGuild ? '/guild/' + lastGuild : '/' ),
  136. 'Set-Cookie': [
  137. `wikibot="${state}"; Max-Age=0; HttpOnly`,
  138. `wikibot="${state}-${user.id}"; HttpOnly`
  139. ]
  140. });
  141. return res.end();
  142. }, error => {
  143. console.log( '- Dashboard: Error while checking the guilds:', error );
  144. res.writeHead(302, {Location: '/login?action=failed'});
  145. return res.end();
  146. } );
  147. }, error => {
  148. console.log( '- Dashboard: Error while getting user and guilds: ' + error );
  149. res.writeHead(302, {Location: '/login?action=failed'});
  150. return res.end();
  151. } );
  152. }, error => {
  153. console.log( '- Dashboard: Error while getting the token: ' + error );
  154. res.writeHead(302, {Location: '/login?action=failed'});
  155. return res.end();
  156. } );
  157. }
  158. /**
  159. * Reload the guild of a user
  160. * @param {import('http').ServerResponse} res - The server response
  161. * @param {String} state - The user state
  162. * @param {String} [returnLocation] - The return location
  163. */
  164. function dashboard_refresh(res, state, returnLocation = '/') {
  165. var settings = settingsData.get(state);
  166. return oauth.getUserGuilds(settings.access_token).then( guilds => {
  167. guilds = guilds.filter( guild => {
  168. return ( guild.owner || hasPerm(guild.permissions, 'MANAGE_GUILD') );
  169. } ).map( guild => {
  170. return {
  171. id: guild.id,
  172. name: guild.name,
  173. acronym: guild.name.replace( /'s /g, ' ' ).replace( /\w+/g, e => e[0] ).replace( /\s/g, '' ),
  174. icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
  175. + ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) + '?size=128' : null ),
  176. permissions: guild.permissions
  177. };
  178. } );
  179. sendMsg( {
  180. type: 'isMemberAll',
  181. guilds: guilds.map( guild => guild.id )
  182. } ).then( response => {
  183. let isMember = new Map();
  184. let notMember = new Map();
  185. response.forEach( (guild, i) => {
  186. if ( guild ) isMember.set(guilds[i].id, guilds[i]);
  187. else notMember.set(guilds[i].id, guilds[i]);
  188. } );
  189. settings.guilds = {isMember, notMember};
  190. res.writeHead(302, {Location: returnLocation});
  191. return res.end();
  192. }, error => {
  193. console.log( '- Dashboard: Error while checking refreshed guilds:', error );
  194. res.writeHead(302, {Location: '/login?action=failed'});
  195. return res.end();
  196. } );
  197. }, error => {
  198. console.log( '- Dashboard: Error while refreshing guilds: ' + error );
  199. res.writeHead(302, {Location: '/login?action=failed'});
  200. return res.end();
  201. } );
  202. }
  203. module.exports = {
  204. login: dashboard_login,
  205. oauth: dashboard_oauth,
  206. refresh: dashboard_refresh
  207. };