Skip to content

Commit cd078cc

Browse files
Merge branch 'main' into reports
2 parents 0dfcdfa + 46ada27 commit cd078cc

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

+970
-312
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: 21 additions & 6 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 & 0 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
}

src/authRoutes.ts

Lines changed: 32 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;
@@ -10,13 +14,15 @@ import { v4 as uuidv4 } from "uuid";
1014

1115
import DiscordOauthRequests from "./discordOauth";
1216

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

1823
type CallbackQuerystringType = Static<typeof CallbackQuerystring>;
1924

25+
// Route before authorized with discord - navigated too to get redirected to discord
2026
const AuthorizeQuerystring = Type.Object({
2127
redirect_url: Type.Optional(Type.String()),
2228
});
@@ -29,7 +35,7 @@ type StoredStateResponse = {
2935

3036
const rootPath = "/auth";
3137

32-
// Since this is a plugin
38+
// Since this is a plugin async should be used
3339
// eslint-disable-next-line @typescript-eslint/require-await
3440
const addPlugin = async (instance: FastifyInstance) => {
3541
await instance.register(fastifyRateLimit, {
@@ -57,6 +63,12 @@ const addPlugin = async (instance: FastifyInstance) => {
5763
enableDraftSpec: true,
5864
});
5965
// Must be registered a second time as v1 and auth routes are separate
66+
67+
/**
68+
* Authorize route, navigated too to get redirected to discord
69+
* If redirect_to is set the user will be redirected to that after navigating to /callback
70+
* This route also generates a state to be used in the oauth flow - which is used for security
71+
*/
6072
instance.get<{ Querystring: AuthorizeQuerystringType }>(
6173
`${rootPath}/authorize`,
6274
{
@@ -89,6 +101,13 @@ const addPlugin = async (instance: FastifyInstance) => {
89101
return reply.send({ redirectUrl });
90102
}
91103
);
104+
/**
105+
* Callback route, navigated too after authorized with discord
106+
* This route will get the user's access token and refresh token from discord
107+
* Then it will get the user's data - and store that
108+
* Then generate a token - a way of authentication between the user and the api
109+
* Then redirect the user to the redirect_to path - if it was set
110+
*/
92111
instance.get<{ Querystring: CallbackQuerystringType }>(
93112
`${rootPath}/callback`,
94113
{
@@ -122,20 +141,25 @@ const addPlugin = async (instance: FastifyInstance) => {
122141
if (state === undefined) {
123142
return new Forbidden("Missing state");
124143
}
144+
// State must be valid, present and the same - for security
125145
const cachedState = await instance.redisCache.getState(state);
126146
if (!cachedState) {
127147
return new Forbidden("Cannot find state, please try again");
128148
}
149+
// Delete state so it cannot be used again - again for security
129150
await instance.redisCache.deleteState(state);
151+
130152
const tokenResponse = await instance.discordOauthRequests.exchangeToken(
131153
code
132154
);
155+
// If the required scopes are not set then the data required might not be accessible
133156
if (!DiscordOauthRequests.verifyScopes(tokenResponse.scope)) {
134157
return new Forbidden("Invalid scopes, please try again");
135158
}
136159
const user = await instance.discordOauthRequests.fetchUser({
137160
token: tokenResponse.access_token,
138161
});
162+
139163
await instance.redisCache.setUserData(user.id, {
140164
avatar:
141165
user.avatar !== null
@@ -144,6 +168,11 @@ const addPlugin = async (instance: FastifyInstance) => {
144168
discriminator: user.discriminator,
145169
username: user.username,
146170
});
171+
172+
// Create user in database - the token is stored here and not on the browser
173+
// so the token does not get stolen, which could lead to actions under the bot's id being taken on
174+
// behalf of the user that we do not want to happen
175+
147176
await instance.prisma.user.upsert({
148177
where: { id: BigInt(user.id) },
149178
create: {
@@ -159,6 +188,7 @@ const addPlugin = async (instance: FastifyInstance) => {
159188
},
160189
});
161190

191+
// Session is to authenticate the client to the api
162192
const sessionToken = `browser.${uuidv4()}.${user.id}.${Date.now()}`;
163193
await instance.redisCache.setSession(sessionToken, user.id);
164194
const date = new Date();

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)