oauth2 because i am fucking insane
This commit is contained in:
parent
0cd2b02282
commit
c5d9954fcf
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"token": "token",
|
"token": "token",
|
||||||
"sitePort": 15385,
|
"sitePort": 15385,
|
||||||
"siteURL": "https://localhost:15385"
|
"siteURL": "http://localhost:15385",
|
||||||
|
"clientId": "",
|
||||||
|
"clientSecret": ""
|
||||||
}
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema
|
||||||
|
.createTable('sessions', table => {
|
||||||
|
table.string('id').notNullable();
|
||||||
|
table.string('tokenType').notNullable();
|
||||||
|
table.string('accessToken').notNullable();
|
||||||
|
table.string('refreshToken').notNullable();
|
||||||
|
table.timestamp('expiresAt').notNullable();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema
|
||||||
|
.dropTable('sessions');
|
||||||
|
};
|
|
@ -12,6 +12,8 @@
|
||||||
"author": "oatmealine",
|
"author": "oatmealine",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@discordjs/core": "^1.1.1",
|
||||||
|
"@discordjs/rest": "^2.2.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"d3-array": "^2.12.1",
|
"d3-array": "^2.12.1",
|
||||||
"discord.js": "^14.14.1",
|
"discord.js": "^14.14.1",
|
||||||
|
@ -22,12 +24,16 @@
|
||||||
"parse-color": "^1.0.0",
|
"parse-color": "^1.0.0",
|
||||||
"pretty-bytes": "^5.6.0",
|
"pretty-bytes": "^5.6.0",
|
||||||
"random-seed": "^0.3.0",
|
"random-seed": "^0.3.0",
|
||||||
"sqlite3": "^5.1.6"
|
"sqlite3": "^5.1.6",
|
||||||
|
"tough-cookie": "^4.1.3",
|
||||||
|
"uid-safe": "^2.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/d3-array": "^3.2.1",
|
"@types/d3-array": "^3.2.1",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/parse-color": "^1.0.3",
|
"@types/parse-color": "^1.0.3",
|
||||||
|
"@types/tough-cookie": "^4.0.5",
|
||||||
|
"@types/uid-safe": "^2.1.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||||
"@typescript-eslint/parser": "^6.11.0",
|
"@typescript-eslint/parser": "^6.11.0",
|
||||||
"discord-api-types": "^0.37.63",
|
"discord-api-types": "^0.37.63",
|
||||||
|
|
1941
pnpm-lock.yaml
1941
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
import { Client, GatewayIntentBits, Events, Collection, CommandInteraction, CommandInteractionOption, ApplicationCommandOptionType } from 'discord.js';
|
import { Client, GatewayIntentBits, Events, Collection, CommandInteraction, CommandInteractionOption, ApplicationCommandOptionType } from 'discord.js';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
const { token, sitePort, siteURL } = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
|
const { token, sitePort, siteURL, clientId, clientSecret } = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { initializeAnnouncements } from './lib/subscriptions';
|
import { initializeAnnouncements } from './lib/subscriptions';
|
||||||
import * as log from './lib/log';
|
import * as log from './lib/log';
|
||||||
|
@ -22,7 +22,7 @@ const bot = new Client({
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.config = {
|
bot.config = {
|
||||||
token, sitePort, siteURL
|
token, sitePort, siteURL, clientId, clientSecret
|
||||||
};
|
};
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
|
|
@ -79,4 +79,11 @@ export interface CustomCraftingRecipeItem {
|
||||||
item: number,
|
item: number,
|
||||||
quantity: number,
|
quantity: number,
|
||||||
type: 'input' | 'output' | 'requirement'
|
type: 'input' | 'output' | 'requirement'
|
||||||
|
}
|
||||||
|
export interface Session {
|
||||||
|
id: string,
|
||||||
|
tokenType: string,
|
||||||
|
accessToken: string,
|
||||||
|
refreshToken: string,
|
||||||
|
expiresAt: number,
|
||||||
}
|
}
|
|
@ -11,7 +11,9 @@ export interface Command {
|
||||||
export interface Config {
|
export interface Config {
|
||||||
token: string,
|
token: string,
|
||||||
sitePort: number,
|
sitePort: number,
|
||||||
siteURL: string
|
siteURL: string,
|
||||||
|
clientId: string,
|
||||||
|
clientSecret: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'discord.js' {
|
declare module 'discord.js' {
|
||||||
|
|
141
src/web.ts
141
src/web.ts
|
@ -1,14 +1,74 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import * as log from './lib/log';
|
import * as log from './lib/log';
|
||||||
import { CustomItem, db } from './lib/db';
|
import { CustomItem, Session, db } from './lib/db';
|
||||||
import { defaultItems } from './lib/rpg/items';
|
import { defaultItems } from './lib/rpg/items';
|
||||||
import { Client } from 'discord.js';
|
import { Client, RESTPostOAuth2AccessTokenResult, RESTPostOAuth2AccessTokenURLEncodedData, RESTPostOAuth2RefreshTokenURLEncodedData, Routes } from 'discord.js';
|
||||||
|
import got from 'got';
|
||||||
|
import uid from 'uid-safe';
|
||||||
|
import { Cookie, parse } from 'tough-cookie';
|
||||||
|
import { IncomingHttpHeaders } from 'http';
|
||||||
|
|
||||||
|
const DISCORD_ENDPOINT = 'https://discord.com/api/v10';
|
||||||
|
const UID_BYTE_LENGTH = 18;
|
||||||
|
const COOKIE_KEY = 'PHPSESSID';
|
||||||
|
const COOKIE_EXPIRES = 1_000 * 60 * 60 * 24 * 365;
|
||||||
|
|
||||||
|
async function getSessionString(cookieStr: string) {
|
||||||
|
const cookies = cookieStr.split('; ').map(s => parse(s)!).filter(c => c !== null);
|
||||||
|
const sessionCookie = cookies.find(c => c.key === COOKIE_KEY);
|
||||||
|
|
||||||
|
if (!sessionCookie) {
|
||||||
|
return await uid(UID_BYTE_LENGTH);
|
||||||
|
} else {
|
||||||
|
return sessionCookie.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSession(bot: Client, headers: IncomingHttpHeaders) {
|
||||||
|
const cookie = headers['cookie'];
|
||||||
|
if (!cookie) return;
|
||||||
|
|
||||||
|
const sessionStr = await getSessionString(cookie);
|
||||||
|
|
||||||
|
const session = await db<Session>('sessions')
|
||||||
|
.where('id', sessionStr)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!session) return;
|
||||||
|
|
||||||
|
if (Date.now() < session.expiresAt) return session;
|
||||||
|
|
||||||
|
let resp;
|
||||||
|
try {
|
||||||
|
resp = await got.post(DISCORD_ENDPOINT + Routes.oauth2TokenExchange(), {
|
||||||
|
form: {
|
||||||
|
client_id: bot.config.clientId,
|
||||||
|
client_secret: bot.config.clientSecret,
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: session.refreshToken,
|
||||||
|
} satisfies RESTPostOAuth2RefreshTokenURLEncodedData
|
||||||
|
}).json() as RESTPostOAuth2AccessTokenResult;
|
||||||
|
} catch(err) {
|
||||||
|
log.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionData = {
|
||||||
|
tokenType: resp.token_type,
|
||||||
|
accessToken: resp.access_token,
|
||||||
|
refreshToken: resp.refresh_token,
|
||||||
|
expiresAt: Date.now() + resp.expires_in * 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (await db<Session>('sessions')
|
||||||
|
.where('id', sessionStr)
|
||||||
|
.update(sessionData)
|
||||||
|
.returning('*'))[0];
|
||||||
|
}
|
||||||
|
|
||||||
export async function startServer(bot: Client, port: number) {
|
export async function startServer(bot: Client, port: number) {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(express.static('static/'));
|
|
||||||
|
|
||||||
app.get('/api/items', async (req, res) => {
|
app.get('/api/items', async (req, res) => {
|
||||||
const guildID = req.query.guild;
|
const guildID = req.query.guild;
|
||||||
|
|
||||||
|
@ -32,5 +92,78 @@ export async function startServer(bot: Client, port: number) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/', async (req, res) => {
|
||||||
|
const code = req.query.code as string;
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
try {
|
||||||
|
const resp = await got.post(DISCORD_ENDPOINT + Routes.oauth2TokenExchange(), {
|
||||||
|
form: {
|
||||||
|
client_id: bot.config.clientId,
|
||||||
|
client_secret: bot.config.clientSecret,
|
||||||
|
code,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri: bot.config.siteURL,
|
||||||
|
} satisfies RESTPostOAuth2AccessTokenURLEncodedData
|
||||||
|
// if you're looking to change this then you are blissfully unaware of the past
|
||||||
|
// and have learnt 0 lessons
|
||||||
|
}).json() as RESTPostOAuth2AccessTokenResult;
|
||||||
|
|
||||||
|
const sessionId = await getSessionString(decodeURIComponent(req.headers.cookie || ''));
|
||||||
|
|
||||||
|
const session = await db<Session>('sessions')
|
||||||
|
.where('id', sessionId)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const sessionData = {
|
||||||
|
tokenType: resp.token_type,
|
||||||
|
accessToken: resp.access_token,
|
||||||
|
refreshToken: resp.refresh_token,
|
||||||
|
expiresAt: Date.now() + resp.expires_in * 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
await db<Session>('sessions')
|
||||||
|
.where('id', sessionId)
|
||||||
|
.update(sessionData);
|
||||||
|
} else {
|
||||||
|
await db<Session>('sessions')
|
||||||
|
.insert({id: sessionId, ...sessionData} satisfies Session);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookie = new Cookie({
|
||||||
|
key: COOKIE_KEY,
|
||||||
|
value: sessionId,
|
||||||
|
expires: new Date(Date.now() + COOKIE_EXPIRES),
|
||||||
|
sameSite: 'strict'
|
||||||
|
});
|
||||||
|
res.setHeader('Set-Cookie', cookie.toString());
|
||||||
|
|
||||||
|
return res.redirect('/profile');
|
||||||
|
} catch (err) {
|
||||||
|
log.error(err);
|
||||||
|
return res.status(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendFile('index.html', { root: 'static/' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/profile', async (req, res) => {
|
||||||
|
const session = await getSession(bot, req.headers);
|
||||||
|
if (!session) return res.redirect(`https://discord.com/api/oauth2/authorize?client_id=${bot.config.clientId}&redirect_uri=${encodeURIComponent(bot.config.siteURL)}&response_type=code&scope=identify%20guilds`);
|
||||||
|
|
||||||
|
const user = await got('https://discord.com/api/users/@me', {
|
||||||
|
headers: {
|
||||||
|
authorization: `${session.tokenType} ${session.accessToken}`
|
||||||
|
}
|
||||||
|
}).json();
|
||||||
|
|
||||||
|
//res.sendFile('profile/index.html', { root: 'static/' });
|
||||||
|
res.json(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(express.static('static/'));
|
||||||
|
|
||||||
app.listen(port, () => log.info(`web interface listening on ${port}`));
|
app.listen(port, () => log.info(`web interface listening on ${port}`));
|
||||||
}
|
}
|
Loading…
Reference in New Issue