1
0

oauth.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. const crypto = require('crypto');
  2. const cheerio = require('cheerio');
  3. const {defaultPermissions} = require('../util/default.json');
  4. const {db, 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 && settingsData.has(state) ) {
  20. if ( !action ) {
  21. res.writeHead(302, {Location: '/'});
  22. return res.end();
  23. }
  24. settingsData.delete(state);
  25. }
  26. var $ = cheerio.load(file);
  27. let responseCode = 200;
  28. let prompt = 'none';
  29. if ( process.env.READONLY ) createNotice($, 'readonly');
  30. if ( action ) createNotice($, action);
  31. if ( action === 'unauthorized' ) $('head').append(
  32. $('<script>').text('history.replaceState(null, null, "/login");')
  33. );
  34. if ( action === 'logout' ) prompt = 'consent';
  35. if ( action === 'loginfail' ) responseCode = 400;
  36. state = crypto.randomBytes(16).toString("hex");
  37. while ( settingsData.has(state) ) {
  38. state = crypto.randomBytes(16).toString("hex");
  39. }
  40. let invite = oauth.generateAuthUrl( {
  41. scope: ['identify', 'guilds', 'bot'],
  42. permissions: defaultPermissions, state
  43. } );
  44. $('.guild#invite a, .channel#invite-wikibot').attr('href', invite);
  45. let url = oauth.generateAuthUrl( {
  46. scope: ['identify', 'guilds'],
  47. prompt, state
  48. } );
  49. $('.channel#login, #login-button').attr('href', url);
  50. let body = $.html();
  51. res.writeHead(responseCode, {
  52. 'Set-Cookie': [
  53. ...( res.getHeader('Set-Cookie') || [] ),
  54. `wikibot="${state}"; HttpOnly; Path=/`
  55. ],
  56. 'Content-Length': body.length
  57. });
  58. res.write( body );
  59. return res.end();
  60. }
  61. /**
  62. * Load oauth data of a user
  63. * @param {import('http').ServerResponse} res - The server response
  64. * @param {String} state - The user state
  65. * @param {URLSearchParams} searchParams - The url parameters
  66. * @param {String} [lastGuild] - The guild to return to
  67. */
  68. function dashboard_oauth(res, state, searchParams, lastGuild) {
  69. if ( searchParams.get('error') === 'access_denied' && state === searchParams.get('state') && settingsData.has(state) ) {
  70. res.writeHead(302, {Location: '/'});
  71. return res.end();
  72. }
  73. if ( state !== searchParams.get('state') || !searchParams.get('code') ) {
  74. res.writeHead(302, {Location: '/login?action=failed'});
  75. return res.end();
  76. }
  77. settingsData.delete(state);
  78. return oauth.tokenRequest( {
  79. scope: ['identify', 'guilds'],
  80. code: searchParams.get('code'),
  81. grantType: 'authorization_code'
  82. } ).then( ({access_token}) => {
  83. return Promise.all([
  84. oauth.getUser(access_token),
  85. oauth.getUserGuilds(access_token)
  86. ]).then( ([user, guilds]) => {
  87. guilds = guilds.filter( guild => {
  88. return ( guild.owner || hasPerm(guild.permissions, 'MANAGE_GUILD') );
  89. } ).map( guild => {
  90. return {
  91. id: guild.id,
  92. name: guild.name,
  93. acronym: guild.name.replace( /'s /g, ' ' ).replace( /\w+/g, e => e[0] ).replace( /\s/g, '' ),
  94. icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
  95. + ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) : null ),
  96. userPermissions: guild.permissions
  97. };
  98. } );
  99. sendMsg( {
  100. type: 'getGuilds',
  101. member: user.id,
  102. guilds: guilds.map( guild => guild.id )
  103. } ).then( response => {
  104. var settings = {
  105. state: `${state}-${user.id}`,
  106. access_token,
  107. user: {
  108. id: user.id,
  109. username: user.username,
  110. discriminator: user.discriminator,
  111. avatar: 'https://cdn.discordapp.com/' + ( user.avatar ?
  112. `avatars/${user.id}/${user.avatar}.` +
  113. ( user.avatar.startsWith( 'a_' ) ? 'gif' : 'png' ) :
  114. `embed/avatars/${user.discriminator % 5}.png` ) + '?size=64',
  115. locale: user.locale
  116. },
  117. guilds: {
  118. count: guilds.length,
  119. isMember: new Map(),
  120. notMember: new Map()
  121. }
  122. };
  123. response.forEach( (guild, i) => {
  124. if ( guild ) {
  125. if ( guild === 'noMember' ) return;
  126. settings.guilds.isMember.set(guilds[i].id, Object.assign(guilds[i], guild));
  127. }
  128. else settings.guilds.notMember.set(guilds[i].id, guilds[i]);
  129. } );
  130. settingsData.set(settings.state, settings);
  131. res.writeHead(302, {
  132. Location: ( lastGuild && /^\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?$/.test(lastGuild) ? `/guild/${lastGuild}` : '/' ),
  133. 'Set-Cookie': [`wikibot="${settings.state}"; HttpOnly; Path=/`]
  134. });
  135. return res.end();
  136. }, error => {
  137. console.log( '- Dashboard: Error while getting the guilds:', error );
  138. res.writeHead(302, {Location: '/login?action=failed'});
  139. return res.end();
  140. } );
  141. }, error => {
  142. console.log( '- Dashboard: Error while getting user and guilds: ' + error );
  143. res.writeHead(302, {Location: '/login?action=failed'});
  144. return res.end();
  145. } );
  146. }, error => {
  147. console.log( '- Dashboard: Error while getting the token: ' + error );
  148. res.writeHead(302, {Location: '/login?action=failed'});
  149. return res.end();
  150. } );
  151. }
  152. /**
  153. * Reload the guild of a user
  154. * @param {import('http').ServerResponse} res - The server response
  155. * @param {String} state - The user state
  156. * @param {String} [returnLocation] - The return location
  157. */
  158. function dashboard_refresh(res, state, returnLocation = '/') {
  159. var settings = settingsData.get(state);
  160. return oauth.getUserGuilds(settings.access_token).then( guilds => {
  161. guilds = guilds.filter( guild => {
  162. return ( guild.owner || hasPerm(guild.permissions, 'MANAGE_GUILD') );
  163. } ).map( guild => {
  164. return {
  165. id: guild.id,
  166. name: guild.name,
  167. acronym: guild.name.replace( /'s /g, ' ' ).replace( /\w+/g, e => e[0] ).replace( /\s/g, '' ),
  168. icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
  169. + ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) : null ),
  170. userPermissions: guild.permissions
  171. };
  172. } );
  173. sendMsg( {
  174. type: 'getGuilds',
  175. member: settings.user.id,
  176. guilds: guilds.map( guild => guild.id )
  177. } ).then( response => {
  178. let isMember = new Map();
  179. let notMember = new Map();
  180. response.forEach( (guild, i) => {
  181. if ( guild ) {
  182. if ( guild === 'noMember' ) return;
  183. isMember.set(guilds[i].id, Object.assign(guilds[i], guild));
  184. }
  185. else notMember.set(guilds[i].id, guilds[i]);
  186. } );
  187. settings.guilds = {count: guilds.length, isMember, notMember};
  188. res.writeHead(302, {Location: returnLocation + '?refresh=success'});
  189. return res.end();
  190. }, error => {
  191. console.log( '- Dashboard: Error while getting the refreshed guilds:', error );
  192. res.writeHead(302, {Location: returnLocation + '?refresh=failed'});
  193. return res.end();
  194. } );
  195. }, error => {
  196. console.log( '- Dashboard: Error while refreshing guilds: ' + error );
  197. res.writeHead(302, {Location: returnLocation + '?refresh=failed'});
  198. return res.end();
  199. } );
  200. }
  201. module.exports = {
  202. login: dashboard_login,
  203. oauth: dashboard_oauth,
  204. refresh: dashboard_refresh
  205. };