Skip to content

Commit 46ada27

Browse files
chore(docs): improve comments in code (#25)
* chore: document * comment some more * chore: some more comments * chore: more comments * chore: more comments * chore: more comments * chore: more comments * chore: improve readme
1 parent d2f92b5 commit 46ada27

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+978
-347
lines changed

.github/workflows/codeql-analysis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# the `language` matrix defined below to confirm you have the correct set of
1010
# supported CodeQL languages.
1111
#
12+
13+
# This is for code quality scanning by github
1214
name: "CodeQL"
1315

1416
on:

.github/workflows/deploy-image.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# This action deploys and publishes a docker image to the github docker registry.
2+
# So that the image doesn't have to be built on deploy on the server.
3+
14
name: Create and publish a Docker image
25

36
on:

.github/workflows/lint.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Auto run lint on push
2+
13
name: Lint
24
on: push
35
jobs:

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,43 @@
22

33
Backend service for [message.anothercat.me](https://message.anothercat.me)
44

5-
PRISMA_FIELD_ENCRYPTION_KEY: https://github.com/47ng/prisma-field-encryption#2-setup-your-encryption-key
5+
## Development
6+
7+
### Prerequisites
8+
9+
Node 17.5 (see .nvmrc)
10+
Postgresql 13 with user with write permissions
11+
Redis with REJSON
12+
13+
### Setup
14+
15+
Run `npm ci` to install directly from `package-lock.json` - if you use `npm install` or `npm i` instead it cannot be guaranteed that the dependencies work.
16+
17+
Set up a `.env` file with the variables from `.env.example` and fill in the values.
18+
19+
For PRISMA_FIELD_ENCRYPTION_KEY see: https://github.com/47ng/prisma-field-encryption#2-setup-your-encryption-key
20+
21+
### Running
22+
23+
Run `npm run dev` to start the development server.
24+
25+
You will need to setup a tunnel or some kind of way to expose the server to discord. I use cloudflare tunnels. The endpoint for the discord interactions is `/interactions`
26+
27+
You will need to have a gateway cache instance also running
28+
[see message-manager-discord/gateway](https://github.com/message-manager-discord/gateway) and [message-manager-discord/redis-discord-cache](https://github.com/message-manager-discord/redis-discord-cache)
29+
30+
### Migrations
31+
32+
Prisma is used for migrations - run `npm run migrate` to migrate the database.
33+
34+
### General overview of important files
35+
36+
.github/workflows - contains github actions for CI config
37+
prisma - contains prisma schema and migrations
38+
src - contains the source code
39+
.env - contains environment variables (do not use .env on production use docker env variables instead)
40+
.env.example - contains example environment variables
41+
.eslintrc.js - contains eslint config
42+
.prettierrc.json - contains prettier config
43+
.wakatime-project - contains wakatime config
44+
Dockerfile - contains docker config

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

prisma/schema.prisma

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Database schema for postgresql database
12
generator client {
23
provider = "prisma-client-js"
34
}
@@ -15,7 +16,6 @@ model Channel {
1516
guildId BigInt
1617
guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade, onUpdate: NoAction)
1718
messages Message[]
18-
1919
}
2020

2121
model Guild {
@@ -43,7 +43,6 @@ model Message {
4343
4444
embed MessageEmbed?
4545
46-
4746
@@unique([id, editedAt])
4847
}
4948

src/authRoutes.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
// This route file is separate from the other routes since auth routes are not versioned
1+
/**
2+
* This route file is separate from the other routes since auth routes are not versioned
3+
* As they are only used by the website - which is always up to date
4+
* They are routes to run the OAuth2 flow with discord
5+
*/
26
import { FastifyInstance } from "fastify";
37
import httpErrors from "http-errors";
48
const { Forbidden } = httpErrors;
@@ -8,13 +12,15 @@ import { v5 as uuidv5 } from "uuid";
812

913
import DiscordOauthRequests from "./discordOauth";
1014

15+
// Callback after authorized with discord
1116
const CallbackQuerystring = Type.Object({
1217
code: Type.String(),
1318
state: Type.Optional(Type.String()),
1419
});
1520

1621
type CallbackQuerystringType = Static<typeof CallbackQuerystring>;
1722

23+
// Route before authorized with discord - navigated too to get redirected to discord
1824
const AuthorizeQuerystring = Type.Object({
1925
redirect_to: Type.Optional(Type.String()),
2026
});
@@ -27,9 +33,14 @@ type StoredStateResponse = {
2733

2834
const rootPath = "/auth";
2935

30-
// Since this is a plugin
36+
// Since this is a plugin async should be used
3137
// eslint-disable-next-line @typescript-eslint/require-await
3238
const addPlugin = async (instance: FastifyInstance) => {
39+
/**
40+
* Authorize route, navigated too to get redirected to discord
41+
* If redirect_to is set the user will be redirected to that after navigating to /callback
42+
* This route also generates a state to be used in the oauth flow - which is used for security
43+
*/
3344
instance.get<{ Querystring: AuthorizeQuerystringType }>(
3445
`${rootPath}/authorize`,
3546
{
@@ -52,6 +63,13 @@ const addPlugin = async (instance: FastifyInstance) => {
5263
);
5364
}
5465
);
66+
/**
67+
* Callback route, navigated too after authorized with discord
68+
* This route will get the user's access token and refresh token from discord
69+
* Then it will get the user's data - and store that
70+
* Then generate a token - a way of authentication between the user and the api
71+
* Then redirect the user to the redirect_to path - if it was set
72+
*/
5573
instance.get<{ Querystring: CallbackQuerystringType }>(
5674
`${rootPath}/callback`,
5775
{
@@ -67,20 +85,27 @@ const addPlugin = async (instance: FastifyInstance) => {
6785
if (state === undefined) {
6886
return new Forbidden("Missing state");
6987
}
88+
// State must be valid, present and the same - for security
7089
const cachedState = await instance.redisCache.getState(state);
7190
if (!cachedState) {
7291
return new Forbidden("Cannot find state, please try again");
7392
}
93+
// Delete state so it cannot be used again - again for security
7494
await instance.redisCache.deleteState(state);
95+
7596
const tokenResponse = await instance.discordOauthRequests.exchangeToken(
7697
code
7798
);
99+
// If the required scopes are not set then the data required might not be accessible
78100
if (!DiscordOauthRequests.verifyScopes(tokenResponse.scope)) {
79101
return new Forbidden("Invalid scopes, please try again");
80102
}
81103
const user = await instance.discordOauthRequests.fetchUser({
82104
token: tokenResponse.access_token,
83105
});
106+
// Create user in database - the token is stored here and not on the browser
107+
// so the token does not get stolen, which could lead to actions under the bot's id being taken on
108+
// behalf of the user that we do not want to happen
84109
await instance.prisma.user.upsert({
85110
where: { id: BigInt(user.id) },
86111
create: {
@@ -96,12 +121,14 @@ const addPlugin = async (instance: FastifyInstance) => {
96121
},
97122
});
98123

124+
// Session is to authenticate the client to the api
99125
const session = uuidv5(user.id, instance.envVars.UUID_NAMESPACE);
100126
await instance.redisCache.setSession(session, user.id);
101127
const date = new Date();
102128
date.setDate(date.getDate() + 7);
103129

104130
const redirectPath = cachedState.redirectPath ?? "/";
131+
// This cookie will be used to authenticate the client to the api
105132
return reply
106133
.setCookie("_HOST-session", session, {
107134
secure: true,

src/constants.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
//const discordAPIBaseURL = "https://discord.com/api/v9";
2-
const discordAPIBaseURL = "https://discord-proxy.anothercat.workers.dev/api/v9";
1+
// Base url for making API requests to discord (other than through the discord.js client)
2+
const discordAPIBaseURL = "https://discord.com/api/v9";
3+
// Scopes to request and require in the oauth2 flow
4+
// identify to be able to identify the user
5+
// guilds to be able to get the guilds the user is in
6+
// and guilds.members.read to access the member object for the user in each guild
37
const requiredScopes = ["identify", "guilds", "guilds.members.read"];
48

9+
// Integer color numbers for embed generation
510
const embedPink = 12814273;
611
const successGreen = 3066993;
712
const failureRed = 15158332;
813

14+
// Url to invite the bot to a server - must be done by a user
915
const inviteUrl =
1016
"https://discord.com/api/oauth2/authorize?client_id=735395698278924359&permissions=515933326400&scope=bot%20applications.commands";
1117

src/consts.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
// Derived from
12
// https://github.com/detritusjs/client/blob/b27cbaa5bfb48506b059be178da0e871b83ba95e/src/constants.ts#L917
3+
// and discord.com/developers/docs/topics/permissions
24
const DiscordPermissions = Object.freeze({
35
NONE: 0n,
46
CREATE_INSTANT_INVITE: 1n << 0n,
@@ -42,6 +44,9 @@ const DiscordPermissions = Object.freeze({
4244
SEND_MESSAGES_IN_THREADS: 1n << 38n,
4345
});
4446

47+
// Various functions to get the permission names / values, this is to assist with error and message generation about permissions
48+
// For example getting the name of a permission from the value
49+
4550
const _permissionsByName: { [name: string]: bigint } = {};
4651
const _permissionsByValue: { [value: string]: string } = {};
4752
// Find the names and values of all the permissions from Permissions

src/discordOauth.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/**
2+
* Custom client for making requests to Discord's OAuth2 API
3+
*/
4+
15
import axios, { AxiosError, AxiosResponse } from "axios";
26
import {
37
RESTGetAPICurrentUserGuildsResult,
@@ -17,6 +21,8 @@ import {
1721
} from "./errors";
1822
import { UserRequestData } from "./plugins/authentication";
1923

24+
// Two different responses to differentiate between a cache and uncached response
25+
// This is because they need to be handled differently
2026
interface CachedResponse {
2127
cached: true;
2228
data: unknown;
@@ -30,6 +36,9 @@ class DiscordOauthRequests {
3036
constructor(instance: FastifyInstance) {
3137
this._instance = instance;
3238
}
39+
40+
// _makeRequest has two type overloads
41+
// This one is for when the response can from the cache (so could be either uncached or cached)
3342
private async _makeRequest({
3443
path,
3544
method,
@@ -48,6 +57,7 @@ class DiscordOauthRequests {
4857
userId: Snowflake;
4958
}): Promise<UncachedResponse | CachedResponse>;
5059

60+
// This overload if for then the response cannot be from the cache (cacheExpiry is undefined)
5161
private async _makeRequest({
5262
path,
5363
method,
@@ -66,6 +76,7 @@ class DiscordOauthRequests {
6676
userId?: undefined;
6777
}): Promise<UncachedResponse>;
6878

79+
// Function to make all requests through - this is to allow for caching
6980
private async _makeRequest({
7081
path,
7182
method,
@@ -84,9 +95,9 @@ class DiscordOauthRequests {
8495
cacheExpiry?: number;
8596
userId?: Snowflake;
8697
}): Promise<UncachedResponse | CachedResponse> {
98+
// If the cacheExpiry is defined and the request if from user, then the response can be cached
99+
// Otherwise it cannot not
87100
if (cacheExpiry !== undefined && userId !== undefined) {
88-
// TODO: Should cacheExpiry be checked / used
89-
// Requests without a token are not cached
90101
const cachedResponse = (await this._instance.redisCache.getOauthCache(
91102
path,
92103
userId

0 commit comments

Comments
 (0)