Skip to content

Commit

Permalink
feat: Add basic user registration with Discord
Browse files Browse the repository at this point in the history
  • Loading branch information
Pieloaf committed Oct 24, 2023
1 parent 9bb5f6c commit 2d3291b
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 43 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
.env
package-lock.json
*.code-workspace
*.code-workspace
build/
4 changes: 2 additions & 2 deletions data/accounts.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[
{
"username": "test",
"user_id": "123456"
"user_id": 123456
},
{
"username": "Pieloaf",
"user_id": "293850"
"user_id": 293850
}
]
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@
"license": "ISC",
"dependencies": {
"async-mqtt": "^2.6.3",
"dotenv": "^16.0.3",
"express": "^4.18.2"
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0"
},
"devDependencies": {
"@types/express": "^4.17.14",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^18.11.0",
"@types/ws": "^8.5.3",
"ts-node-dev": "^2.0.0",
"tsc": "^2.0.4",
"typescript": "^4.8.4"
},
"engines": {
"node": ">=18.18.2"
}
}
58 changes: 58 additions & 0 deletions src/auth/discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { config } from "dotenv";

config();

const DISCORD_REDIRECT_URI =
"https://bsf.pieloaf.com/auth/discord-oauth-callback";
const DISCORD_CLIENT_ID = "1122976027140956221";
const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET as string;
const DISCORD_API_BASE_URL = "https://discord.com/api";

export const getDiscordOAuthURL = () => {
let url = new URL(`https://discord.com/api/oauth2/authorize`);
url.searchParams.set("client_id", DISCORD_CLIENT_ID);
url.searchParams.set("redirect_uri", DISCORD_REDIRECT_URI);
url.searchParams.set("response_type", "code");
url.searchParams.set("scope", "identify guilds");
return url.toString();
};

export const getDiscorOauthToken = async (grant_code: string) => {
let url = new URL(`${DISCORD_API_BASE_URL}/oauth2/token`);
let body = new URLSearchParams({
client_id: DISCORD_CLIENT_ID,
client_secret: DISCORD_CLIENT_SECRET,
grant_type: "authorization_code",
code: grant_code,
redirect_uri: DISCORD_REDIRECT_URI,
});
let requestData = {
method: "POST",
body: body,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
};

let response = await fetch(url.toString(), requestData);
if (response.status === 200) {
return await response.json();
} else {
throw new Error(
`Error fetching OAuth tokens: [${response.status}] ${response.statusText}`
);
}
};

export const getDiscordUser = async (access_token: string) => {
let url = new URL(`${DISCORD_API_BASE_URL}/users/@me`);
let requestData = {
method: "GET",
headers: {
Authorization: `Bearer ${access_token}`,
"Content-Type": "application/x-www-form-urlencoded",
},
};
let response = await fetch(url.toString(), requestData);
return await response.json();
};
43 changes: 30 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { AuthRouter, Session, sessionHandler } from "./sessions";
import { ChatRouter } from "./chat";
import { BattleRouter } from "./battle/Battle";
import { QueueRouter } from "./queue";
import { config } from "dotenv";

config();

const app = express();

Expand All @@ -16,7 +19,7 @@ app.use(express.json()); // parse data as json unless otherwise specified
* [with the exception of steam/overlay/{session_id}/{state}]. This middleware
* extracts the session key and checks it is a valid session before continuing.
**/
app.use((req, res, next) => {
app.use("/services", (req, res, next) => {
// steam overlay requests dont end with the session key but the server
// doesnt seem to do anything with the request anyway so skip check
if (req.path.startsWith("/services/session/steam/overlay/")) {
Expand All @@ -33,33 +36,33 @@ app.use((req, res, next) => {
// the login route ends with /11 so in that case the user has no session
// and the extracted "key" will be 11, so in this case the request can continue
if (!session && session_key !== "11") {
res.sendStatus(403);
res.status(403);
return;
}

// adding the session object to the request object so
// each module doesn't need to lookup the session again
(req as any).session = session;
next();
app._router.handle(req, res, next);
});

app.use("/services/auth", AuthRouter);
app.use("/auth", AuthRouter);

app.use("/services/chat", ChatRouter);
app.use("/chat", ChatRouter);

app.use("/services/vs", QueueRouter);
app.use("/vs", QueueRouter);

app.use("/services/battle", BattleRouter);
app.use("/battle", BattleRouter);

// request leaderboard or update server of location
app.post("/services/game/leaderboards/:session_key", (req, res) => {
app.post("/game/leaderboards/:session_key", (req, res) => {
// parse board_ids and tourney from body
// and lookup database
res.json(JSON.parse(readFileSync("./data/lboard.json", "utf-8")));
});

// poll for relevant data
app.get("/services/game/:session_key", (req, res) => {
app.get("/game/:session_key", (req, res) => {
let session: Session = (req as any).session;
// send buffered data and clear
if (session.data.length > 0) {
Expand All @@ -74,23 +77,37 @@ app.get("/services/game/:session_key", (req, res) => {
* Random routes that either have temp data or idk what their purpose is
*/

const getTokenFromHeader = (req: express.Request) => {
return req.headers.authorization?.match(/Bearer (.*)/)?.[1];
};
// get account info
app.get("/services/account/info/:session_key", (req, res) => {
app.get("/account/info/:session_key?", (req, res) => {
let auth: string | undefined =
req.params.session_key || getTokenFromHeader(req);

if (!auth) {
res.status(403);
return;
}
// // look up user in database
// return user data (will require some handlers for packing data)
// TODO: implement handlers for packing acc data
res.json(JSON.parse(readFileSync("./data/acc.json", "utf-8")));
});

app.post("/services/game/location/:session_key", (req, res) => {
app.post("/game/location/:session_key", (req, res) => {
// do something here with location info maybe? idk what

// TODO: start worker for class linked with location eg meadhouse -> worker for roster
res.send();
});

// notify server if steam overlay is enabled
app.post("/services/session/steam/overlay/:session_key/:state", (req, res) => {
app.post("/session/steam/overlay/:session_key/:state", (req, res) => {
// idk what the server does with this info
res.send();
});

http.createServer(app).listen(3000);
http.createServer(app).listen(3000, () => {
console.log("Express server listening on port " + 3000);
});
92 changes: 67 additions & 25 deletions src/sessions.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,48 @@

import crypto from "crypto";
import { readFileSync } from 'fs';
import { readFileSync } from "fs";
import { getQueue } from "./queue";
import { GameModes } from "./const";
import { Router } from "express";
import jsonwebtoken from "jsonwebtoken";
const { verify, sign } = jsonwebtoken;
import * as d_auth from "./auth/discord";
import { config } from "dotenv";

config();

const build_number = readFileSync("./data/build-number", 'utf-8');
const build_number = readFileSync("./data/build-number", "utf-8");
export const AuthRouter = Router();

const JWT_SECRET = process.env.JWT_SECRET as string;

var generateKey = () => {
return crypto.randomBytes(8).toString("hex");
};

const getInitialData = (): any[] => {
// should take user_id arg to check currency data and friend data
// return initial queue data [done], tournament data, currency data, friend data
let initialData: any[] = []
let initialData: any[] = [];

for (const type of Object.values(GameModes)) {
initialData.push(getQueue(type, 0))
initialData.push(getQueue(type, 0));
}
initialData.concat(JSON.parse(readFileSync("./data/first.json", 'utf-8')));
return initialData
}

initialData.concat(JSON.parse(readFileSync("./data/first.json", "utf-8")));
return initialData;
};

const getUser = (user_id: number) => {
// look up user in database and return data
// needs some form of authentication
// maybe a user_id stored in a jwt token
// maybe a user_id stored in a jwt token
// which can be passed as the username to the game from an external client
// anyway... on with the demo
return JSON.parse(readFileSync("./data/accounts.json", 'utf-8')).find(
(acc: any) => acc.user_id === user_id);
return JSON.parse(readFileSync("./data/accounts.json", "utf-8")).find(
(acc: any) => acc.user_id === user_id
);
};

export class Session {

display_name: string;
user_id: number;
session_key: string;
Expand All @@ -50,7 +56,7 @@ export class Session {
this.user_id = user_id;
this.session_key = generateKey();
this.data = getInitialData();
};
}

asJson() {
return {
Expand All @@ -59,18 +65,22 @@ export class Session {
user_id: this.user_id,
vbb_name: null,
session_key: this.session_key,
}
};
};
}

pushData(...data: any) {
this.data.push(...data);
};
}
}

var sessions: any = {}
var sessions: any = {};

export const sessionHandler = {
getSessions: (filterFunc: (s:Session, index: number, array: Session[]) => void = _ => true): Session[] => {
getSessions: (
filterFunc: (s: Session, index: number, array: Session[]) => void = (
_
) => true
): Session[] => {
return (Object.values(sessions) as Session[]).filter(filterFunc);
},
addSession: (user_id: number) => {
Expand All @@ -79,21 +89,53 @@ export const sessionHandler = {
return session.asJson();
},
getSession: (key: string, value: any): Session => {
if (key === "session_key")
return sessions[value];
return Object.values(sessions).find(session => (session as any)[key] === value) as Session;
if (key === "session_key") return sessions[value];
return Object.values(sessions).find(
(session) => (session as any)[key] === value
) as Session;
},
removeSession: (session_key: string) => {
delete sessions[session_key];
}
},
};

AuthRouter.post('/login/:httpVersion', (req, res) => {
let userData = sessionHandler.addSession(req.body.steam_id);
AuthRouter.post("/login/:httpVersion", (req, res) => {
let data = verify(req.body.steam_id, process.env.JWT_SECRET as string);
console.log(data); // Temporary
// TODO: lookup user in database
let userData = sessionHandler.addSession(293850);
res.json(userData);
});

AuthRouter.post("/logout/:session_key", (req, res) => {
sessionHandler.removeSession(req.params.session_key);
res.send();
});

AuthRouter.get("/discord-login", (req, res) => {
res.redirect(d_auth.getDiscordOAuthURL());
});

AuthRouter.get("/discord-oauth-callback", async (req, res) => {
let jwt_res: string | undefined;
let error: string | undefined;

if (req.query?.error || !req.query?.code) {
error = req.query.error?.toString();
}
try {
const tokens = await d_auth.getDiscorOauthToken(
req.query.code as string
);
const d_user = await d_auth.getDiscordUser(tokens.access_token);
jwt_res = sign({ discord_id: d_user.id }, JWT_SECRET);
} catch (e) {
console.log(e); // TODO: Should probably log this somewhere persistent
error = "an_error_occurred_communicating_with_discord";
}
res.status(301);
if (error) {
return res.redirect(`bsf://auth?error=${error}`);
}
return res.redirect(`bsf://auth?access_token=${jwt_res}`);
});

0 comments on commit 2d3291b

Please sign in to comment.