Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add basic user registration with Discord #6

Merged
merged 2 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": false
"singleQuote": false,
"printWidth": 120
}
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
}
]
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@
"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",
"discord-api-types": "^0.37.61",
"ts-node-dev": "^2.0.0",
"tsc": "^2.0.4",
"typescript": "^4.8.4"
},
"engines": {
"node": ">=18.18.2"
}
}
60 changes: 60 additions & 0 deletions src/auth/discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
RESTPostOAuth2AccessTokenResult,
RESTGetAPICurrentUserResult,
OAuth2Routes,
Routes,
} from "discord-api-types/rest/v10";
import { config } from "dotenv";
// TODO: provide env variables in docker compose and remove this dependency
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;

export const getDiscordOAuthURL = () => {
let url = new URL(OAuth2Routes.authorizationURL);
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): Promise<RESTPostOAuth2AccessTokenResult> => {
let url = new URL(OAuth2Routes.tokenURL);
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()) as RESTPostOAuth2AccessTokenResult;
} else {
throw new Error(`Error fetching OAuth tokens: [${response.status}] ${response.statusText}`);
}
};

export const getDiscordUser = async (access_token: string): Promise<RESTGetAPICurrentUserResult> => {
let url = new URL(Routes.user());
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()) as RESTGetAPICurrentUserResult;
};
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);
});
84 changes: 59 additions & 25 deletions src/sessions.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,46 @@

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 +54,7 @@ export class Session {
this.user_id = user_id;
this.session_key = generateKey();
this.data = getInitialData();
};
}

asJson() {
return {
Expand All @@ -59,18 +63,18 @@ 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 +83,51 @@ 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;
let res_params = new URLSearchParams();

if (req.query?.error || !req.query?.code) {
res_params.set("error", req.query.error?.toString() || "missing_access_code");
} else {
try {
const tokens = await d_auth.getDiscorOauthToken(req.query.code as string);
const discord_user = await d_auth.getDiscordUser(tokens.access_token);
jwt_res = sign({ discord_id: discord_user.id }, JWT_SECRET);
} catch (e) {
console.log(e); // TODO: Should probably log this somewhere persistent
res_params.set("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}`);
});
Loading