Skip to content

Commit 6b1daee

Browse files
authored
Merge branch 'master' into feature/track-user-email-verification
2 parents e9b8ae6 + bd35c46 commit 6b1daee

28 files changed

+835
-259
lines changed

backend/__tests__/api/controllers/leaderboard.spec.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -139,28 +139,29 @@ describe("Loaderboard Controller", () => {
139139
);
140140
});
141141

142-
it("should get for mode", async () => {
143-
getLeaderboardMock.mockResolvedValue([]);
144-
for (const mode of ["time", "words", "quote", "zen", "custom"]) {
145-
const response = await mockApp
146-
.get("/leaderboards")
147-
.query({ language: "english", mode, mode2: "custom" });
148-
expect(response.status, "for mode " + mode).toEqual(200);
149-
}
150-
});
151-
152-
it("should get for mode2", async () => {
153-
getLeaderboardMock.mockResolvedValue([]);
154-
for (const mode2 of allModes) {
155-
const response = await mockApp.get("/leaderboards").query({
156-
language: "english",
157-
mode: "words",
158-
mode2,
159-
});
142+
describe("should get for modes", async () => {
143+
beforeEach(() => {
144+
getLeaderboardMock.mockResolvedValue([]);
145+
});
160146

161-
expect(response.status, "for mode2 " + mode2).toEqual(200);
162-
}
147+
const testCases = [
148+
{ mode: "time", mode2: "15", language: "english", expectStatus: 200 },
149+
{ mode: "time", mode2: "60", language: "english", expectStatus: 200 },
150+
{ mode: "time", mode2: "30", language: "english", expectStatus: 404 },
151+
{ mode: "words", mode2: "15", language: "english", expectStatus: 404 },
152+
{ mode: "time", mode2: "15", language: "spanish", expectStatus: 404 },
153+
];
154+
it.for(testCases)(
155+
`expect $expectStatus for mode $mode, mode2 $mode2, lang $language`,
156+
async ({ mode, mode2, language, expectStatus }) => {
157+
await mockApp
158+
.get("/leaderboards")
159+
.query({ language, mode, mode2 })
160+
.expect(expectStatus);
161+
}
162+
);
163163
});
164+
164165
it("fails for missing query", async () => {
165166
const { body } = await mockApp.get("/leaderboards").expect(422);
166167

backend/src/api/controllers/leaderboard.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ export async function getLeaderboard(
3232
): Promise<GetLeaderboardResponse> {
3333
const { language, mode, mode2, page, pageSize } = req.query;
3434

35+
if (
36+
mode !== "time" ||
37+
(mode2 !== "15" && mode2 !== "60") ||
38+
language !== "english"
39+
) {
40+
throw new MonkeyError(404, "There is no leaderboard for this mode");
41+
}
42+
3543
const leaderboard = await LeaderboardsDAL.get(
3644
mode,
3745
mode2,

backend/src/api/controllers/user.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { MonkeyResponse } from "../../utils/monkey-response";
88
import * as DiscordUtils from "../../utils/discord";
99
import {
1010
buildAgentLog,
11-
isDevEnvironment,
11+
getFrontendUrl,
1212
replaceObjectId,
1313
replaceObjectIds,
1414
sanitizeString,
@@ -180,11 +180,7 @@ export async function sendVerificationEmail(
180180
const { data: link, error } = await tryCatch(
181181
FirebaseAdmin()
182182
.auth()
183-
.generateEmailVerificationLink(email, {
184-
url: isDevEnvironment()
185-
? "http://localhost:3000"
186-
: "https://monkeytype.com",
187-
})
183+
.generateEmailVerificationLink(email, { url: getFrontendUrl() })
188184
);
189185

190186
if (error) {

backend/src/init/email-client.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const templates: Record<EmailType, EmailMetadata> = {
2828

2929
let transportInitialized = false;
3030
let transporter: nodemailer.Transporter;
31+
let emailFrom = "Monkeytype <[email protected]>";
3132

3233
export function isInitialized(): boolean {
3334
return transportInitialized;
@@ -38,7 +39,12 @@ export async function init(): Promise<void> {
3839
return;
3940
}
4041

41-
const { EMAIL_HOST, EMAIL_USER, EMAIL_PASS, EMAIL_PORT } = process.env;
42+
const { EMAIL_HOST, EMAIL_USER, EMAIL_PASS, EMAIL_PORT, EMAIL_FROM } =
43+
process.env;
44+
45+
if (EMAIL_FROM !== undefined) {
46+
emailFrom = EMAIL_FROM;
47+
}
4248

4349
if (!(EMAIL_HOST ?? "") || !(EMAIL_USER ?? "") || !(EMAIL_PASS ?? "")) {
4450
if (isDevEnvironment()) {
@@ -102,7 +108,7 @@ export async function sendEmail(
102108
const template = await fillTemplate<typeof templateName>(templateName, data);
103109

104110
const mailOptions = {
105-
from: "Monkeytype <[email protected]>",
111+
from: emailFrom,
106112
to,
107113
subject: templates[templateName].subject,
108114
html: template,

backend/src/utils/auth.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
setTokenCacheSize,
77
} from "./prometheus";
88
import { type DecodedIdToken, UserRecord } from "firebase-admin/auth";
9-
import { isDevEnvironment } from "./misc";
9+
import { getFrontendUrl } from "./misc";
1010
import emailQueue from "../queues/email-queue";
1111
import * as UserDAL from "../dal/user";
1212
import { isFirebaseError } from "./error";
@@ -98,11 +98,7 @@ export async function sendForgotPasswordEmail(email: string): Promise<void> {
9898

9999
const link = await FirebaseAdmin()
100100
.auth()
101-
.generatePasswordResetLink(email, {
102-
url: isDevEnvironment()
103-
? "http://localhost:3000"
104-
: "https://monkeytype.com",
105-
});
101+
.generatePasswordResetLink(email, { url: getFrontendUrl() });
106102

107103
await emailQueue.sendForgotPasswordEmail(email, name, link);
108104
} catch (err) {

backend/src/utils/misc.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,14 @@ export function isDevEnvironment(): boolean {
193193
return process.env["MODE"] === "dev";
194194
}
195195

196+
export function getFrontendUrl(): string {
197+
return isDevEnvironment()
198+
? "http://localhost:3000"
199+
: process.env["FRONTEND_URL"] !== undefined
200+
? process.env["FRONTEND_URL"]
201+
: "https://monkeytype.com";
202+
}
203+
196204
/**
197205
* convert database object into api object
198206
* @param data database object with `_id: ObjectId`

docker/docker-compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ services:
3535
- REDIS_URI=redis://monkeytype-redis:6379
3636
- FRONTEND_URL=${MONKEYTYPE_FRONTENDURL}
3737
- RECAPTCHA_SECRET=${RECAPTCHA_SECRET:-}
38+
- EMAIL_HOST=${EMAIL_HOST:-}
39+
- EMAIL_PORT=${EMAIL_PORT:-}
40+
- EMAIL_USER=${EMAIL_USER:-}
41+
- EMAIL_PASS=${EMAIL_PASS:-}
42+
- EMAIL_FROM=${EMAIL_FROM:-}
3843
volumes:
3944
#uncomment to enable the account system, check the SELF_HOSTING.md file
4045
#- type: bind

docker/example.env

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,24 @@ MONKEYTYPE_FRONTENDURL=http://myserver:8080
44
#url of the backend server, this must be accessible by your clients browser
55
MONKEYTYPE_BACKENDURL=http://myserver:5005
66

7-
# uncomment below config if you need user accounts
87
# firebase config
8+
# uncomment below config if you need user accounts
99
#FIREBASE_APIKEY=
1010
#FIREBASE_AUTHDOMAIN=
1111
#FIREBASE_PROJECTID=
1212
#FIREBASE_STORAGEBUCKET=
1313
#FIREBASE_MESSAGINGSENDERID=
1414
#FIREBASE_APPID=
1515

16+
17+
# email server config
18+
# uncomment below if you want to send emails for e.g. password reset
19+
#EMAIL_HOST=mail.myserver
20+
#EMAIL_USER=mailuser
21+
#EMAIL_PASS=mailpass
22+
#EMAIL_PORT=465
23+
#EMAIL_FROM="Support <noreply@myserver>"
24+
1625
# google recaptcha
1726
# uncomment below config if you need user accounts
1827
# you can use these defaults if you host this privately

docs/SELF_HOSTING.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- [Setup Firebase](#setup-firebase)
1515
- [Update backend configuration](#update-backend-configuration)
1616
- [Setup Recaptcha](#setup-recaptcha)
17+
- [Setup email optional](#setup-email-optional)
1718
- [Enable daily leaderboards](#enable-daily-leaderboards)
1819
- [Configuration files](#configuration-files)
1920
- [env file](#env-file)
@@ -126,6 +127,20 @@ RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
126127
RECAPTCHA_SECRET=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
127128
```
128129

130+
### Setup email (optional)
131+
132+
To enable emails for password reset and email verification update the following config in `.env` file:
133+
134+
```
135+
# email server config
136+
# uncomment below if you want to send emails for e.g. password reset
137+
EMAIL_HOST=mail.myserver # your mailserver domain
138+
EMAIL_USER=mailuser # username to authenticate with your mailserver
139+
EMAIL_PASS=mailpass # password for the user
140+
EMAIL_PORT=465 # port, likely 465 or 578
141+
EMAIL_FROM="Support <noreply@myserver>"
142+
```
143+
129144
## Enable daily leaderboards
130145

131146
To enable daily leaderboards update the `backend-configuration.json` file and add/modify

frontend/src/html/pages/leaderboards.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<div class="title timer">Updates in: -</div>
2424

2525
<div class="jumpButtons">
26-
<div class="updating hidden">
26+
<div class="updating">
2727
<i class="fas fa-circle-notch fa-spin"></i>
2828
</div>
2929
<button data-action="firstPage"><i class="fas fa-crown"></i></button>
@@ -55,7 +55,7 @@
5555
wpm
5656
<div class="sub">accuracy</div>
5757
</td>
58-
<td class="stat narrow">
58+
<td class="stat narrow rawAndConsistency">
5959
raw
6060
<div class="sub">consistency</div>
6161
</td>
@@ -156,7 +156,7 @@
156156
</div>
157157
</div>
158158
</div>
159-
<div class="buttons">
159+
<div class="sideButtons">
160160
<div class="buttonGroup typeButtons">
161161
<button data-type="allTime">
162162
<i class="fas fa-globe-americas"></i>
@@ -172,7 +172,7 @@
172172
</button>
173173
</div>
174174
<div class="divider hidden"></div>
175-
<div class="buttonGroup hidden secondary modeButtons">
175+
<div class="buttonGroup hidden modeButtons">
176176
<button data-mode="15">
177177
<i class="fas fa-clock"></i>
178178
time 15
@@ -182,8 +182,8 @@
182182
time 60
183183
</button>
184184
</div>
185-
<div class="divider divider2 hidden"></div>
186-
<div class="buttonGroup hidden secondary languageButtons">
185+
<div class="divider2 hidden"></div>
186+
<div class="buttonGroup hidden languageButtons">
187187
<button data-language="english">
188188
<i class="fas fa-globe"></i>
189189
english

0 commit comments

Comments
 (0)