diff --git a/.github/workflows/monkey-ci.yml b/.github/workflows/monkey-ci.yml index fc136e9dd2ad..0757a783d640 100644 --- a/.github/workflows/monkey-ci.yml +++ b/.github/workflows/monkey-ci.yml @@ -23,7 +23,7 @@ concurrency: jobs: pre-ci: - if: github.event.pull_request.draft == false + if: github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'force-ci') || contains(github.event.pull_request.labels.*.name, 'force-full-ci') name: pre-ci runs-on: ubuntu-latest outputs: @@ -72,7 +72,7 @@ jobs: name: prime-cache runs-on: ubuntu-latest needs: [pre-ci] - if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || needs.pre-ci.outputs.assets-json == 'true' + if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || needs.pre-ci.outputs.assets-json == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci') steps: - name: Checkout pnpm-lock @@ -119,7 +119,7 @@ jobs: name: check-pretty needs: [pre-ci, prime-cache] runs-on: ubuntu-latest - if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || needs.pre-ci.outputs.assets-json == 'true' + if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || needs.pre-ci.outputs.assets-json == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci') steps: - uses: actions/checkout@v4 @@ -169,7 +169,7 @@ jobs: name: ci-be needs: [pre-ci, prime-cache, check-pretty] runs-on: ubuntu-latest - if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' + if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci') steps: - uses: actions/checkout@v4 with: @@ -217,7 +217,7 @@ jobs: name: ci-fe needs: [pre-ci, prime-cache, check-pretty] runs-on: ubuntu-latest - if: needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' + if: needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci') steps: - uses: actions/checkout@v4 @@ -270,7 +270,7 @@ jobs: name: ci-assets needs: [pre-ci, prime-cache, check-pretty] runs-on: ubuntu-latest - if: needs.pre-ci.outputs.assets-json == 'true' + if: needs.pre-ci.outputs.assets-json == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci') steps: - uses: actions/checkout@v4 with: @@ -339,7 +339,7 @@ jobs: name: ci-pkg needs: [pre-ci, prime-cache,check-pretty] runs-on: ubuntu-latest - if: needs.pre-ci.outputs.should-build-pkg == 'true' + if: needs.pre-ci.outputs.should-build-pkg == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci') steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/semantic-pr-title.yml b/.github/workflows/semantic-pr-title.yml index 62e1bccbd1f8..d98760a4dd1f 100644 --- a/.github/workflows/semantic-pr-title.yml +++ b/.github/workflows/semantic-pr-title.yml @@ -15,6 +15,7 @@ jobs: main: name: check runs-on: ubuntu-latest + if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }} steps: - name: Lint and verify PR title uses: amannn/action-semantic-pull-request@v5 @@ -49,7 +50,7 @@ jobs: - uses: marocchino/sticky-pull-request-comment@v2 # When the previous steps fails, the workflow would stop. By adding this # condition you can continue the execution with the populated error message. - if: always() && (steps.lint_pr_title.outputs.error_message != null) + if: always() && (steps.lint_pr_title.outputs.error_message != null) with: header: pr-title-lint-error message: | diff --git a/backend/__tests__/api/controllers/ape-key.spec.ts b/backend/__tests__/api/controllers/ape-key.spec.ts index 5f33d32fea25..6061a4e99d88 100644 --- a/backend/__tests__/api/controllers/ape-key.spec.ts +++ b/backend/__tests__/api/controllers/ape-key.spec.ts @@ -11,7 +11,7 @@ const configuration = Configuration.getCachedConfiguration(); const uid = new ObjectId().toHexString(); describe("ApeKeyController", () => { - const getUserMock = vi.spyOn(UserDal, "getUser"); + const getUserMock = vi.spyOn(UserDal, "getPartialUser"); beforeEach(async () => { await enableApeKeysEndpoints(true); diff --git a/backend/__tests__/global-setup.ts b/backend/__tests__/global-setup.ts index 573bff62c9d2..64c96042aa24 100644 --- a/backend/__tests__/global-setup.ts +++ b/backend/__tests__/global-setup.ts @@ -1,15 +1,17 @@ import * as MongoDbMock from "vitest-mongodb"; export async function setup(): Promise { process.env.TZ = "UTC"; - await MongoDbMock.setup({ - serverOptions: { - binary: { - version: "6.0.12", - }, - }, - }); + await MongoDbMock.setup(MongoDbMockConfig); } export async function teardown(): Promise { await MongoDbMock.teardown(); } + +export const MongoDbMockConfig = { + serverOptions: { + binary: { + version: "6.0.12", + }, + }, +}; diff --git a/backend/__tests__/setup-tests.ts b/backend/__tests__/setup-tests.ts index 5d267700c3f4..0a1f16757848 100644 --- a/backend/__tests__/setup-tests.ts +++ b/backend/__tests__/setup-tests.ts @@ -1,6 +1,7 @@ import { Collection, Db, MongoClient, WithId } from "mongodb"; import { afterAll, beforeAll, afterEach } from "vitest"; import * as MongoDbMock from "vitest-mongodb"; +import { MongoDbMockConfig } from "./global-setup"; process.env["MODE"] = "dev"; //process.env["MONGOMS_DISTRO"] = "ubuntu-22.04"; @@ -15,9 +16,8 @@ let client: MongoClient; const collectionsForCleanUp = ["users"]; beforeAll(async () => { - await MongoDbMock.setup({ - //don't add any configuration here, add to global-setup.ts instead. - }); + //don't add any configuration here, add to global-setup.ts instead. + await MongoDbMock.setup(MongoDbMockConfig); client = new MongoClient(globalThis.__MONGO_URI__); await client.connect(); diff --git a/backend/package.json b/backend/package.json index 1e65bd1504e2..091849ca7c9a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -33,7 +33,7 @@ "cors": "2.8.5", "cron": "2.3.0", "date-fns": "3.6.0", - "dotenv": "10.0.0", + "dotenv": "16.4.5", "express": "4.19.2", "express-rate-limit": "6.2.1", "firebase-admin": "12.0.0", @@ -46,15 +46,15 @@ "mongodb": "6.3.0", "mustache": "4.2.0", "node-fetch": "2.6.7", - "nodemailer": "6.9.9", + "nodemailer": "6.9.14", "nodemon": "3.1.4", "object-hash": "3.0.0", "path": "0.12.7", - "prom-client": "14.0.1", - "rate-limiter-flexible": "2.3.7", + "prom-client": "15.1.3", + "rate-limiter-flexible": "5.0.3", "simple-git": "3.16.0", "string-similarity": "4.0.4", - "swagger-stats": "0.99.5", + "swagger-stats": "0.99.7", "swagger-ui-express": "4.3.0", "ua-parser-js": "0.7.33", "uuid": "10.0.0", @@ -76,10 +76,10 @@ "@types/mustache": "4.2.2", "@types/node": "20.14.11", "@types/node-fetch": "2.6.1", - "@types/nodemailer": "6.4.7", - "@types/object-hash": "2.2.1", + "@types/nodemailer": "6.4.15", + "@types/object-hash": "3.0.6", "@types/readline-sync": "1.4.8", - "@types/string-similarity": "4.0.0", + "@types/string-similarity": "4.0.2", "@types/supertest": "2.0.12", "@types/swagger-stats": "0.95.11", "@types/swagger-ui-express": "4.1.3", diff --git a/backend/src/api/routes/ape-keys.ts b/backend/src/api/routes/ape-keys.ts index 2038c887a0a1..56cb0a46519e 100644 --- a/backend/src/api/routes/ape-keys.ts +++ b/backend/src/api/routes/ape-keys.ts @@ -13,7 +13,7 @@ const commonMiddleware = [ }, invalidMessage: "ApeKeys are currently disabled.", }), - checkUserPermissions({ + checkUserPermissions(["canManageApeKeys"], { criteria: (user) => { return user.canManageApeKeys ?? true; }, diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts index 86eec0bc533e..287b53f61316 100644 --- a/backend/src/api/routes/index.ts +++ b/backend/src/api/routes/index.ts @@ -77,7 +77,7 @@ export function addApiRoutes(app: Application): void { function applyTsRestApiRoutes(app: IRouter): void { createExpressEndpoints(contract, router, app, { jsonQuery: true, - requestValidationErrorHandler(err, _req, res, next) { + requestValidationErrorHandler(err, req, res, _next) { let message: string | undefined = undefined; let validationErrors: string[] | undefined = undefined; @@ -90,16 +90,24 @@ function applyTsRestApiRoutes(app: IRouter): void { } else if (err.body?.issues !== undefined) { message = "Invalid request data schema"; validationErrors = err.body.issues.map(prettyErrorMessage); - } - - if (message !== undefined) { - res - .status(422) - .json({ message, validationErrors } as MonkeyValidationError); + } else if (err.headers?.issues !== undefined) { + message = "Invalid header schema"; + validationErrors = err.headers.issues.map(prettyErrorMessage); } else { - next(); + Logger.error( + `Unknown validation error for ${req.method} ${ + req.path + }: ${JSON.stringify(err)}` + ); + res + .status(500) + .json({ message: "Unknown validation error. Contact support." }); return; } + + res + .status(422) + .json({ message, validationErrors } as MonkeyValidationError); }, globalMiddleware: [authenticateTsRestRequest()], }); diff --git a/backend/src/api/routes/quotes.ts b/backend/src/api/routes/quotes.ts index 19a4f760461d..79784b5aff8e 100644 --- a/backend/src/api/routes/quotes.ts +++ b/backend/src/api/routes/quotes.ts @@ -10,7 +10,7 @@ import { validateRequest } from "../../middlewares/validation"; const router = Router(); -const checkIfUserIsQuoteMod = checkUserPermissions({ +const checkIfUserIsQuoteMod = checkUserPermissions(["quoteMod"], { criteria: (user) => { return ( user.quoteMod === true || @@ -171,7 +171,7 @@ router.post( captcha: withCustomMessages.regex(/[\w-_]+/).required(), }, }), - checkUserPermissions({ + checkUserPermissions(["canReport"], { criteria: (user) => { return user.canReport !== false; }, diff --git a/backend/src/api/routes/users.ts b/backend/src/api/routes/users.ts index 0b6d81ec2037..1febadf8d6fb 100644 --- a/backend/src/api/routes/users.ts +++ b/backend/src/api/routes/users.ts @@ -638,7 +638,7 @@ router.post( captcha: withCustomMessages.regex(/[\w-_]+/).required(), }, }), - checkUserPermissions({ + checkUserPermissions(["canReport"], { criteria: (user) => { return user.canReport !== false; }, diff --git a/backend/src/middlewares/permission.ts b/backend/src/middlewares/permission.ts index 7be634cee088..cd10afad8bdb 100644 --- a/backend/src/middlewares/permission.ts +++ b/backend/src/middlewares/permission.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import MonkeyError from "../utils/error"; import type { Response, NextFunction, RequestHandler } from "express"; -import { getUser } from "../dal/user"; +import { getPartialUser } from "../dal/user"; import { isAdmin } from "../dal/admin-uids"; import type { ValidationOptions } from "./configuration"; @@ -34,8 +34,9 @@ export function checkIfUserIsAdmin(): RequestHandler { * Check user permissions before handling request. * Note that this middleware must be used after authentication in the middleware stack. */ -export function checkUserPermissions( - options: ValidationOptions +export function checkUserPermissions( + fields: K[], + options: ValidationOptions> ): RequestHandler { const { criteria, invalidMessage = "You don't have permission to do this." } = options; @@ -48,7 +49,11 @@ export function checkUserPermissions( try { const { uid } = req.ctx.decodedToken; - const userData = await getUser(uid, "check user permissions"); + const userData = await getPartialUser( + uid, + "check user permissions", + fields + ); const hasPermission = criteria(userData); if (!hasPermission) { diff --git a/backend/vitest.config.js b/backend/vitest.config.js index 641f8f5a983e..ecea6a5bbbb1 100644 --- a/backend/vitest.config.js +++ b/backend/vitest.config.js @@ -6,6 +6,7 @@ export default defineConfig({ environment: "node", globalSetup: "__tests__/global-setup.ts", setupFiles: ["__tests__/setup-tests.ts"], + pool: "forks", //this should be the default value, however the CI fails without this set. coverage: { include: ["**/*.ts"], diff --git a/frontend/__tests__/root/config.spec.ts b/frontend/__tests__/root/config.spec.ts index f208ede55862..9d9704907e80 100644 --- a/frontend/__tests__/root/config.spec.ts +++ b/frontend/__tests__/root/config.spec.ts @@ -35,10 +35,6 @@ describe("Config", () => { false ); }); - it("setAccountChartResults", () => { - expect(Config.setAccountChartResults(true)).toBe(true); - expect(Config.setAccountChartResults("on" as any)).toBe(false); - }); it("setStopOnError", () => { expect(Config.setStopOnError("off")).toBe(true); expect(Config.setStopOnError("word")).toBe(true); @@ -254,17 +250,12 @@ describe("Config", () => { it("setBlindMode", () => { testBoolean(Config.setBlindMode); }); - it("setAccountChartResults", () => { - testBoolean(Config.setAccountChartResults); - }); - it("setAccountChartAccuracy", () => { - testBoolean(Config.setAccountChartAccuracy); - }); - it("setAccountChartAvg10", () => { - testBoolean(Config.setAccountChartAvg10); - }); - it("setAccountChartAvg100", () => { - testBoolean(Config.setAccountChartAvg100); + it("setAccountChart", () => { + expect(Config.setAccountChart(["on", "off", "off", "on"])).toBe(true); + expect(Config.setAccountChart(["on", "off"] as any)).toBe(true); + expect(Config.setAccountChart(["on", "off", "on", "true"] as any)).toBe( + false + ); }); it("setAlwaysShowDecimalPlaces", () => { testBoolean(Config.setAlwaysShowDecimalPlaces); diff --git a/frontend/__tests__/utils/config.spec.ts b/frontend/__tests__/utils/config.spec.ts index c7877e1029e7..763cb1c78d28 100644 --- a/frontend/__tests__/utils/config.spec.ts +++ b/frontend/__tests__/utils/config.spec.ts @@ -1,12 +1,13 @@ -import { mergeWithDefaultConfig } from "../../src/ts/utils/config"; +import { migrateConfig } from "../../src/ts/utils/config"; import DefaultConfig from "../../src/ts/constants/default-config"; +import { PartialConfig } from "@monkeytype/contracts/schemas/configs"; describe("config.ts", () => { - describe("mergeWithDefaultConfig", () => { + describe("migrateConfig", () => { it("should carry over properties from the default config", () => { - const partialConfig = {} as Partial; + const partialConfig = {} as PartialConfig; - const result = mergeWithDefaultConfig(partialConfig); + const result = migrateConfig(partialConfig); expect(result).toEqual(expect.objectContaining(DefaultConfig)); for (const [key, value] of Object.entries(DefaultConfig)) { expect(result).toHaveProperty(key, value); @@ -15,9 +16,9 @@ describe("config.ts", () => { it("should not merge properties which are not in the default config (legacy properties)", () => { const partialConfig = { legacy: true, - } as Partial; + } as PartialConfig; - const result = mergeWithDefaultConfig(partialConfig); + const result = migrateConfig(partialConfig); expect(result).toEqual(expect.objectContaining(DefaultConfig)); expect(result).not.toHaveProperty("legacy"); }); @@ -27,13 +28,131 @@ describe("config.ts", () => { hideExtraLetters: true, time: 120, accountChart: ["off", "off", "off", "off"], - } as Partial; + } as PartialConfig; - const result = mergeWithDefaultConfig(partialConfig); + const result = migrateConfig(partialConfig); expect(result.mode).toEqual("quote"); expect(result.hideExtraLetters).toEqual(true); expect(result.time).toEqual(120); expect(result.accountChart).toEqual(["off", "off", "off", "off"]); }); + it("should not convert legacy values if current values are already present", () => { + const testCases = [ + { + given: { showLiveAcc: true, timerStyle: "mini", liveAccStyle: "off" }, + expected: { liveAccStyle: "off" }, + }, + { + given: { + showLiveBurst: true, + timerStyle: "mini", + liveBurstStyle: "off", + }, + expected: { liveBurstStyle: "off" }, + }, + { + given: { quickTab: true, quickRestart: "enter" }, + expected: { quickRestart: "enter" }, + }, + { + given: { swapEscAndTab: true, quickRestart: "enter" }, + expected: { quickRestart: "enter" }, + }, + { + given: { alwaysShowCPM: true, typingSpeedUnit: "wpm" }, + expected: { typingSpeedUnit: "wpm" }, + }, + { + given: { showTimerProgress: true, timerStyle: "mini" }, + expected: { timerStyle: "mini" }, + }, + ]; + + //WHEN + testCases.forEach((test) => { + const description = `given: ${JSON.stringify( + test.given + )}, expected: ${JSON.stringify(test.expected)} `; + + const result = migrateConfig(test.given); + expect(result, description).toEqual( + expect.objectContaining(test.expected) + ); + }); + }); + it("should convert legacy values", () => { + const testCases = [ + { given: { quickTab: true }, expected: { quickRestart: "tab" } }, + { given: { smoothCaret: true }, expected: { smoothCaret: "medium" } }, + { given: { smoothCaret: false }, expected: { smoothCaret: "off" } }, + { given: { swapEscAndTab: true }, expected: { quickRestart: "esc" } }, + { + given: { alwaysShowCPM: true }, + expected: { typingSpeedUnit: "cpm" }, + }, + { given: { showAverage: "wpm" }, expected: { showAverage: "speed" } }, + { + given: { playSoundOnError: true }, + expected: { playSoundOnError: "1" }, + }, + { + given: { playSoundOnError: false }, + expected: { playSoundOnError: "off" }, + }, + { + given: { showTimerProgress: false }, + expected: { timerStyle: "off" }, + }, + { + given: { showLiveWpm: true, timerStyle: "text" }, + expected: { liveSpeedStyle: "text" }, + }, + { + given: { showLiveWpm: true, timerStyle: "bar" }, + expected: { liveSpeedStyle: "mini" }, + }, + { + given: { showLiveWpm: true, timerStyle: "off" }, + expected: { liveSpeedStyle: "mini" }, + }, + { + given: { showLiveBurst: true, timerStyle: "text" }, + expected: { liveBurstStyle: "text" }, + }, + { + given: { showLiveBurst: true, timerStyle: "bar" }, + expected: { liveBurstStyle: "mini" }, + }, + { + given: { showLiveBurst: true, timerStyle: "off" }, + expected: { liveBurstStyle: "mini" }, + }, + { + given: { showLiveAcc: true, timerStyle: "text" }, + expected: { liveAccStyle: "text" }, + }, + { + given: { showLiveAcc: true, timerStyle: "bar" }, + expected: { liveAccStyle: "mini" }, + }, + { + given: { showLiveAcc: true, timerStyle: "off" }, + expected: { liveAccStyle: "mini" }, + }, + { given: { soundVolume: "0.5" }, expected: { soundVolume: 0.5 } }, + ]; + + //WHEN + testCases.forEach((test) => { + const description = `given: ${JSON.stringify( + test.given + )}, expected: ${JSON.stringify(test.expected)} `; + + const result = migrateConfig(test.given); + expect(result, description).toEqual( + expect.objectContaining(test.expected) + ); + }); + }); }); }); diff --git a/frontend/__tests__/utils/local-storage-with-schema.spec.ts b/frontend/__tests__/utils/local-storage-with-schema.spec.ts index 75db29071dcf..7a79229d3acc 100644 --- a/frontend/__tests__/utils/local-storage-with-schema.spec.ts +++ b/frontend/__tests__/utils/local-storage-with-schema.spec.ts @@ -64,6 +64,7 @@ describe("local-storage-with-schema.ts", () => { const res = ls.get(); expect(localStorage.getItem).toHaveBeenCalledWith("config"); + expect(localStorage.setItem).not.toHaveBeenCalled(); expect(res).toEqual(defaultObject); }); @@ -74,6 +75,7 @@ describe("local-storage-with-schema.ts", () => { expect(localStorage.getItem).toHaveBeenCalledWith("config"); expect(localStorage.removeItem).toHaveBeenCalledWith("config"); + expect(localStorage.setItem).not.toHaveBeenCalled(); expect(res).toEqual(defaultObject); }); @@ -83,6 +85,7 @@ describe("local-storage-with-schema.ts", () => { const res = ls.get(); expect(localStorage.getItem).toHaveBeenCalledWith("config"); + expect(localStorage.setItem).not.toHaveBeenCalled(); expect(res).toEqual(defaultObject); }); @@ -97,30 +100,79 @@ describe("local-storage-with-schema.ts", () => { const res = ls.get(); expect(localStorage.getItem).toHaveBeenCalledWith("config"); + expect(localStorage.setItem).toHaveBeenCalledWith( + "config", + JSON.stringify(defaultObject) + ); expect(res).toEqual(defaultObject); }); it("should migrate (when function is provided) if schema failed", () => { - getItemMock.mockReturnValue(JSON.stringify({ hi: "hello" })); + const existingValue = { hi: "hello" }; + + getItemMock.mockReturnValue(JSON.stringify(existingValue)); const migrated = { punctuation: false, mode: "time", fontSize: 1, }; + + const migrateFnMock = vi.fn(() => migrated as any); + const ls = new LocalStorageWithSchema({ key: "config", schema: objectSchema, fallback: defaultObject, - migrate: () => { - return migrated; - }, + migrate: migrateFnMock, }); const res = ls.get(); expect(localStorage.getItem).toHaveBeenCalledWith("config"); + expect(migrateFnMock).toHaveBeenCalledWith( + existingValue, + expect.any(Array) + ); + expect(localStorage.setItem).toHaveBeenCalledWith( + "config", + JSON.stringify(migrated) + ); expect(res).toEqual(migrated); }); + + it("should revert to fallback if migration ran but schema still failed", () => { + const existingValue = { hi: "hello" }; + + getItemMock.mockReturnValue(JSON.stringify(existingValue)); + + const invalidMigrated = { + punctuation: 1, + mode: "time", + fontSize: 1, + }; + + const migrateFnMock = vi.fn(() => invalidMigrated as any); + + const ls = new LocalStorageWithSchema({ + key: "config", + schema: objectSchema, + fallback: defaultObject, + migrate: migrateFnMock, + }); + + const res = ls.get(); + + expect(localStorage.getItem).toHaveBeenCalledWith("config"); + expect(migrateFnMock).toHaveBeenCalledWith( + existingValue, + expect.any(Array) + ); + expect(localStorage.setItem).toHaveBeenCalledWith( + "config", + JSON.stringify(defaultObject) + ); + expect(res).toEqual(defaultObject); + }); }); }); diff --git a/frontend/package.json b/frontend/package.json index ec993b2e643a..a2c1f367e5ee 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,7 +39,7 @@ "@types/howler": "2.2.7", "@types/jquery": "3.5.14", "@types/node": "20.14.11", - "@types/object-hash": "2.2.1", + "@types/object-hash": "3.0.6", "@types/subset-font": "1.4.3", "@types/throttle-debounce": "2.1.0", "@vitest/coverage-v8": "2.0.5", @@ -48,12 +48,12 @@ "concurrently": "8.2.2", "dotenv": "16.4.5", "eslint": "8.57.0", - "firebase-tools": "13.13.3", + "firebase-tools": "13.15.1", "fontawesome-subset": "4.4.0", "gulp": "4.0.2", "gulp-eslint-new": "1.9.1", "happy-dom": "13.4.1", - "madge": "6.1.0", + "madge": "8.0.0", "normalize.css": "8.0.1", "postcss": "8.4.31", "sass": "1.70.0", @@ -61,7 +61,7 @@ "typescript": "5.5.4", "vite": "5.1.7", "vite-bundle-visualizer": "1.0.1", - "vite-plugin-checker": "0.6.4", + "vite-plugin-checker": "0.7.2", "vite-plugin-filter-replace": "0.1.13", "vite-plugin-html-inject": "1.1.2", "vite-plugin-inspect": "0.8.3", @@ -72,11 +72,11 @@ "@date-fns/utc": "1.2.0", "@monkeytype/contracts": "workspace:*", "@ts-rest/core": "3.45.2", - "axios": "1.6.4", + "axios": "1.7.4", "canvas-confetti": "1.5.1", "chart.js": "3.7.1", - "chartjs-adapter-date-fns": "2.0.0", - "chartjs-plugin-annotation": "1.4.0", + "chartjs-adapter-date-fns": "3.0.0", + "chartjs-plugin-annotation": "2.2.1", "chartjs-plugin-trendline": "1.0.2", "color-blend": "4.0.0", "damerau-levenshtein": "1.0.8", diff --git a/frontend/src/html/header.html b/frontend/src/html/header.html index fe9b71aed597..dede891da9ec 100644 --- a/frontend/src/html/header.html +++ b/frontend/src/html/header.html @@ -99,39 +99,75 @@

- - + - + diff --git a/frontend/src/html/pages/account-settings.html b/frontend/src/html/pages/account-settings.html new file mode 100644 index 000000000000..0790cfcd53e6 --- /dev/null +++ b/frontend/src/html/pages/account-settings.html @@ -0,0 +1,278 @@ + diff --git a/frontend/src/html/pages/account.html b/frontend/src/html/pages/account.html index 545e188f4800..f5ddf3c16069 100644 --- a/frontend/src/html/pages/account.html +++ b/frontend/src/html/pages/account.html @@ -14,11 +14,6 @@
-
- -
-
-
@@ -555,8 +550,8 @@
- +
+ + -