oauth.js 7.6 KB

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