Skip to content

Commit e87eba0

Browse files
feat: login and guild API controllers
1 parent 3833367 commit e87eba0

File tree

12 files changed

+327
-32
lines changed

12 files changed

+327
-32
lines changed

.vscode/settings.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,14 @@
8383
"blazebuild": "Project"
8484
},
8585
"typescript.preferences.importModuleSpecifier": "non-relative",
86-
"eslint.experimental.useFlatConfig": true
86+
"eslint.experimental.useFlatConfig": true,
87+
"editor.inlayHints.enabled": "on",
88+
"typescript.inlayHints.enumMemberValues.enabled": true,
89+
"typescript.inlayHints.parameterNames.enabled": "all",
90+
"typescript.inlayHints.functionLikeReturnTypes.enabled": true,
91+
"typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": true,
92+
"typescript.inlayHints.parameterTypes.enabled": true,
93+
"typescript.inlayHints.variableTypes.enabled": true,
94+
"typescript.inlayHints.propertyDeclarationTypes.enabled": true,
95+
"typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName": true,
8796
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
"googleapis": "^126.0.1",
8989
"jpeg-js": "^0.4.4",
9090
"json5": "^2.2.3",
91-
"jsonwebtoken": "^9.0.1",
91+
"jsonwebtoken": "^9.0.2",
9292
"module-alias": "^2.2.3",
9393
"nsfwjs": "^3.0.0",
9494
"pm2": "^5.3.1",

prisma/schema.prisma

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,25 @@ model AfkEntry {
157157
@@map("afk_entries")
158158
}
159159

160+
model User {
161+
id Int @id @default(autoincrement())
162+
name String?
163+
username String
164+
discordId String
165+
guilds String[] @default([])
166+
password String
167+
token String?
168+
recoveryToken String?
169+
recoveryCode String?
170+
recoveryAttempts Int @default(0)
171+
recoveryTokenExpiresAt DateTime?
172+
createdAt DateTime @default(now())
173+
tokenExpiresAt DateTime?
174+
updatedAt DateTime @default(now()) @updatedAt
175+
176+
@@map("users")
177+
}
178+
160179
// The following models were used in the previous version of the bot and are no longer used.
161180
// They are kept here for reference and will be removed in the future.
162181

@@ -210,25 +229,6 @@ model ReactionRole {
210229
@@map("reaction_roles")
211230
}
212231

213-
model User {
214-
id Int @id @default(autoincrement())
215-
name String?
216-
username String
217-
discordId String
218-
guilds String[] @default([])
219-
password String
220-
token String?
221-
recoveryToken String?
222-
recoveryCode String?
223-
recoveryAttempts Int @default(0)
224-
recoveryTokenExpiresAt DateTime?
225-
createdAt DateTime @default(now())
226-
tokenExpiresAt DateTime?
227-
updatedAt DateTime @default(now()) @updatedAt
228-
229-
@@map("users")
230-
}
231-
232232
model BoostRoleEntries {
233233
id Int @id @default(autoincrement())
234234
role_id String

src/framework/typescript/api/http/Controller.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,19 @@
1717
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
1818
*/
1919

20+
import Response from "@framework/api/http/Response";
2021
import Application from "../../app/Application";
21-
import CanBind from "../../container/CanBind";
2222

23-
@CanBind
2423
export default abstract class Controller {
25-
public constructor(protected readonly application: Application) {}
24+
public constructor(
25+
@((..._: unknown[]) => undefined) protected readonly application: Application
26+
) {}
27+
28+
protected error(status: number, body?: unknown, headers?: Record<string, string>) {
29+
return new Response({
30+
status,
31+
body,
32+
headers
33+
});
34+
}
2635
}

src/framework/typescript/api/middleware/GuildAccessControl.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
1818
*/
1919

20+
import { APIErrorCode } from "@main/types/APIErrorCode";
2021
import type { NextFunction, Response } from "express";
2122
import type Request from "../http/Request";
2223

@@ -27,15 +28,17 @@ export default async function GuildAccessControl(
2728
) {
2829
if (!request.params.guild) {
2930
response.status(401).send({
30-
error: "Cannot authorize access without a Guild ID."
31+
error: "Cannot authorize access without a Guild ID.",
32+
code: APIErrorCode.RestrictedGuildAccess
3133
});
3234

3335
return;
3436
}
3537

3638
if (!request.user?.guilds.includes(request.params.guild)) {
3739
response.status(403).send({
38-
error: "Access denied."
40+
error: "Access denied.",
41+
code: APIErrorCode.RestrictedGuildAccess
3942
});
4043

4144
return;

src/framework/typescript/api/middleware/RequireAuthMiddleware.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
1818
*/
1919

20+
import { APIErrorCode } from "@main/types/APIErrorCode";
2021
import type { NextFunction, Response } from "express";
2122
import jwt from "jsonwebtoken";
2223
import type Application from "../../app/Application";
@@ -31,7 +32,8 @@ export default async function RequireAuthMiddleware(
3132
) {
3233
if (!request.headers.authorization) {
3334
response.status(401).json({
34-
error: "No authorization header found in the request"
35+
error: "No authorization header found in the request",
36+
code: APIErrorCode.Unauthorized
3537
});
3638

3739
return;
@@ -41,7 +43,8 @@ export default async function RequireAuthMiddleware(
4143

4244
if (type.toLowerCase() !== "bearer") {
4345
response.status(401).json({
44-
error: "Only bearer tokens are supported"
46+
error: "Only bearer tokens are supported",
47+
code: APIErrorCode.Unauthorized
4548
});
4649

4750
return;
@@ -88,7 +91,8 @@ export default async function RequireAuthMiddleware(
8891
application.logger.debug(e);
8992

9093
response.status(401).json({
91-
error: "Invalid API token"
94+
error: "Invalid API token",
95+
code: APIErrorCode.Unauthorized
9296
});
9397

9498
return;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* This file is part of SudoBot.
3+
*
4+
* Copyright (C) 2021-2023 OSN Developers.
5+
*
6+
* SudoBot is free software; you can redistribute it and/or modify it
7+
* under the terms of the GNU Affero General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* SudoBot is distributed in the hope that it will be useful, but
12+
* WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
import { Action } from "@framework/api/decorators/Action";
21+
import { Validate } from "@framework/api/decorators/Validate";
22+
import Controller from "@framework/api/http/Controller";
23+
import type Request from "@framework/api/http/Request";
24+
import Response from "@framework/api/http/Response";
25+
import { Inject } from "@framework/container/Inject";
26+
import { fetchUser } from "@framework/utils/entities";
27+
import AuthService from "@main/services/AuthService";
28+
import { APIErrorCode } from "@main/types/APIErrorCode";
29+
import { APIGuild, User } from "discord.js";
30+
import { z } from "zod";
31+
32+
class AuthController extends Controller {
33+
@Inject()
34+
private readonly authService!: AuthService;
35+
36+
private readonly linkedUserCache = new Map<number, User>();
37+
private linkedUserCacheTimeout: ReturnType<typeof setTimeout> | null = null;
38+
39+
@Action("POST", "/login")
40+
@Validate(
41+
z.object({
42+
username: z.string().min(3).max(32),
43+
password: z.string().min(1).max(64)
44+
})
45+
)
46+
public async login(request: Request) {
47+
const { username, password } = request.body;
48+
49+
const result = await this.authService.authenticate({
50+
username,
51+
password
52+
});
53+
54+
if (!result.success) {
55+
return new Response({
56+
status: 400,
57+
body: {
58+
success: false,
59+
message: "Invalid credentials",
60+
code: APIErrorCode.InvalidCredentials
61+
}
62+
});
63+
}
64+
65+
const { user } = result;
66+
const discordUser =
67+
this.linkedUserCache.get(user.id) ??
68+
(await fetchUser(this.application.client, user.discordId));
69+
70+
if (!discordUser) {
71+
return new Response({
72+
status: 403,
73+
body: {
74+
success: false,
75+
message: "This account is disabled. Please contact an administrator.",
76+
code: APIErrorCode.AccountDisabled
77+
}
78+
});
79+
}
80+
81+
this.linkedUserCache.set(user.id, discordUser);
82+
83+
if (!this.linkedUserCacheTimeout) {
84+
this.linkedUserCacheTimeout = setTimeout(
85+
() => {
86+
this.linkedUserCache.clear();
87+
this.linkedUserCacheTimeout = null;
88+
},
89+
1000 * 60 * 2
90+
);
91+
}
92+
93+
const guilds: APIGuild[] = [];
94+
95+
for (const guild of this.application.client.guilds.cache.values()) {
96+
if (user.guilds.includes(guild.id)) {
97+
guilds.push(guild.toJSON() as APIGuild);
98+
}
99+
}
100+
101+
return {
102+
success: true,
103+
user: {
104+
id: user.id,
105+
name: user.name ?? undefined,
106+
username: user.username,
107+
discordId: user.discordId,
108+
avatar: discordUser.displayAvatarURL()
109+
},
110+
token: user.token,
111+
expires: user.tokenExpiresAt?.getTime(),
112+
guilds
113+
};
114+
}
115+
}
116+
117+
export default AuthController;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* This file is part of SudoBot.
3+
*
4+
* Copyright (C) 2021-2023 OSN Developers.
5+
*
6+
* SudoBot is free software; you can redistribute it and/or modify it
7+
* under the terms of the GNU Affero General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* SudoBot is distributed in the hope that it will be useful, but
12+
* WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
import { Action } from "@framework/api/decorators/Action";
21+
import { EnableGuildAccessControl } from "@framework/api/decorators/EnableGuildAccessControl";
22+
import { RequireAuth } from "@framework/api/decorators/RequireAuth";
23+
import Controller from "@framework/api/http/Controller";
24+
import type Request from "@framework/api/http/Request";
25+
import { APIErrorCode } from "@main/types/APIErrorCode";
26+
27+
class GuildController extends Controller {
28+
@Action("GET", "/guilds/:guild")
29+
@RequireAuth()
30+
@EnableGuildAccessControl()
31+
public async view(request: Request) {
32+
const { id } = request.params;
33+
const guild = this.application.client.guilds.cache.get(id);
34+
35+
if (!guild) {
36+
return this.error(404, {
37+
message: "Guild not found.",
38+
code: APIErrorCode.None
39+
});
40+
}
41+
42+
return {
43+
id: guild.id,
44+
name: guild.name,
45+
icon: guild.icon
46+
};
47+
}
48+
49+
@Action("GET", "/guilds")
50+
@RequireAuth()
51+
public async index(request: Request) {
52+
const guilds = [];
53+
54+
for (const guild of this.application.client.guilds.cache.values()) {
55+
if (request.user?.guilds.includes(guild.id)) {
56+
guilds.push({
57+
id: guild.id,
58+
name: guild.name,
59+
icon: guild.icon
60+
});
61+
}
62+
}
63+
64+
return guilds;
65+
}
66+
}
67+
68+
export default GuildController;

src/main/typescript/api/controllers/MainController.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@
1919

2020
import { Action } from "@framework/api/decorators/Action";
2121
import Controller from "@framework/api/http/Controller";
22-
import CanBind from "@framework/container/CanBind";
2322
import { Inject } from "@framework/container/Inject";
2423
import ConfigurationManager from "../../services/ConfigurationManager";
2524

26-
@CanBind
27-
export default class MainController extends Controller {
25+
class MainController extends Controller {
2826
@Inject()
2927
protected readonly configManager!: ConfigurationManager;
3028

@@ -47,3 +45,5 @@ export default class MainController extends Controller {
4745
};
4846
}
4947
}
48+
49+
export default MainController;

src/main/typescript/core/DiscordKernel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class DiscordKernel extends Kernel {
7878
"@services/ChannelLockManager",
7979
"@services/ReactionRoleService",
8080
"@services/AFKService",
81+
"@services/AuthService",
8182
"@services/ImageRecognitionService",
8283
"@services/DirectiveParsingService",
8384
"@root/framework/typescript/api/APIServer"

0 commit comments

Comments
 (0)