diff --git a/app/actions/definitions/debug.tsx b/app/actions/definitions/debug.tsx deleted file mode 100644 index 5f11c5687..000000000 --- a/app/actions/definitions/debug.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ToolsIcon, TrashIcon } from "outline-icons"; -import * as React from "react"; -import stores from "~/stores"; -import { createAction } from "~/actions"; -import { DebugSection } from "~/actions/sections"; -import env from "~/env"; -import { deleteAllDatabases } from "~/utils/developer"; - -export const clearIndexedDB = createAction({ - name: ({ t }) => t("Delete IndexedDB cache"), - icon: , - keywords: "cache clear database", - section: DebugSection, - perform: async ({ t }) => { - await deleteAllDatabases(); - stores.toasts.showToast(t("IndexedDB cache deleted")); - }, -}); - -export const development = createAction({ - name: ({ t }) => t("Development"), - keywords: "debug", - icon: , - iconInContextMenu: false, - section: DebugSection, - visible: ({ event }) => - env.ENVIRONMENT === "development" || - (event instanceof KeyboardEvent && event.altKey), - children: [clearIndexedDB], -}); - -export const rootDebugActions = [development]; diff --git a/app/actions/definitions/developer.tsx b/app/actions/definitions/developer.tsx new file mode 100644 index 000000000..0d8dff773 --- /dev/null +++ b/app/actions/definitions/developer.tsx @@ -0,0 +1,50 @@ +import { ToolsIcon, TrashIcon, UserIcon } from "outline-icons"; +import * as React from "react"; +import stores from "~/stores"; +import { createAction } from "~/actions"; +import { DeveloperSection } from "~/actions/sections"; +import env from "~/env"; +import { client } from "~/utils/ApiClient"; +import { deleteAllDatabases } from "~/utils/developer"; + +export const clearIndexedDB = createAction({ + name: ({ t }) => t("Delete IndexedDB cache"), + icon: , + keywords: "cache clear database", + section: DeveloperSection, + perform: async ({ t }) => { + await deleteAllDatabases(); + stores.toasts.showToast(t("IndexedDB cache deleted")); + }, +}); + +export const createTestUsers = createAction({ + name: "Create test users", + icon: , + section: DeveloperSection, + visible: () => env.ENVIRONMENT === "development", + perform: async () => { + const count = 10; + + try { + await client.post("/developer.create_test_users", { count }); + stores.toasts.showToast(`${count} test users created`); + } catch (err) { + stores.toasts.showToast(err.message, { type: "error" }); + } + }, +}); + +export const developer = createAction({ + name: ({ t }) => t("Developer"), + keywords: "debug", + icon: , + iconInContextMenu: false, + section: DeveloperSection, + visible: ({ event }) => + env.ENVIRONMENT === "development" || + (event instanceof KeyboardEvent && event.altKey), + children: [clearIndexedDB, createTestUsers], +}); + +export const rootDeveloperActions = [developer]; diff --git a/app/actions/root.ts b/app/actions/root.ts index 9894e0d72..fb19a3868 100644 --- a/app/actions/root.ts +++ b/app/actions/root.ts @@ -1,5 +1,5 @@ import { rootCollectionActions } from "./definitions/collections"; -import { rootDebugActions } from "./definitions/debug"; +import { rootDeveloperActions } from "./definitions/developer"; import { rootDocumentActions } from "./definitions/documents"; import { rootNavigationActions } from "./definitions/navigation"; import { rootSettingsActions } from "./definitions/settings"; @@ -11,5 +11,5 @@ export default [ ...rootUserActions, ...rootNavigationActions, ...rootSettingsActions, - ...rootDebugActions, + ...rootDeveloperActions, ]; diff --git a/app/actions/sections.ts b/app/actions/sections.ts index 2963e980f..00a69c4d5 100644 --- a/app/actions/sections.ts +++ b/app/actions/sections.ts @@ -2,7 +2,7 @@ import { ActionContext } from "~/types"; export const CollectionSection = ({ t }: ActionContext) => t("Collection"); -export const DebugSection = ({ t }: ActionContext) => t("Debug"); +export const DeveloperSection = ({ t }: ActionContext) => t("Debug"); export const DocumentSection = ({ t }: ActionContext) => t("Document"); diff --git a/server/commands/userInviter.ts b/server/commands/userInviter.ts index 2d732fc8a..498b18a93 100644 --- a/server/commands/userInviter.ts +++ b/server/commands/userInviter.ts @@ -6,7 +6,7 @@ import Logger from "@server/logging/Logger"; import { User, Event, Team } from "@server/models"; import { UserFlag } from "@server/models/User"; -type Invite = { +export type Invite = { name: string; email: string; role: Role; diff --git a/server/routes/api/developer.ts b/server/routes/api/developer.ts new file mode 100644 index 000000000..bddf1b6f1 --- /dev/null +++ b/server/routes/api/developer.ts @@ -0,0 +1,58 @@ +import { Context, Next } from "koa"; +import Router from "koa-router"; +import randomstring from "randomstring"; +import userInviter, { Invite } from "@server/commands/userInviter"; +import env from "@server/env"; +import Logger from "@server/logging/Logger"; +import auth from "@server/middlewares/authentication"; +import { presentUser } from "@server/presenters"; + +const router = new Router(); + +function dev() { + return async function checkDevelopmentMiddleware(ctx: Context, next: Next) { + if (env.ENVIRONMENT !== "development") { + throw new Error("Attempted to access development route in production"); + } + + return next(); + }; +} + +router.post("developer.create_test_users", dev(), auth(), async (ctx) => { + const { count = 10 } = ctx.body; + const { user } = ctx.state; + const invites = Array(count) + .fill(0) + .map(() => { + const rando = randomstring.generate(10); + + return { + email: `${rando}@example.com`, + name: `${rando.slice(0, 5)} Tester`, + role: "member", + } as Invite; + }); + + Logger.info("utils", `Creating ${count} test users`, invites); + + // Generate a bunch of invites + const response = await userInviter({ + user, + invites, + ip: ctx.request.ip, + }); + + // Convert from invites to active users by marking as active + await Promise.all( + response.users.map((user) => user.updateActiveAt(ctx.request.ip, true)) + ); + + ctx.body = { + data: { + users: response.users.map((user) => presentUser(user)), + }, + }; +}); + +export default router; diff --git a/server/routes/api/index.ts b/server/routes/api/index.ts index 180af9da0..1e15be118 100644 --- a/server/routes/api/index.ts +++ b/server/routes/api/index.ts @@ -1,6 +1,7 @@ import Koa from "koa"; import bodyParser from "koa-body"; import Router from "koa-router"; +import env from "@server/env"; import { NotFoundError } from "@server/errors"; import errorHandling from "@server/middlewares/errorHandling"; import methodOverride from "@server/middlewares/methodOverride"; @@ -10,6 +11,7 @@ import auth from "./auth"; import authenticationProviders from "./authenticationProviders"; import collections from "./collections"; import utils from "./cron"; +import developer from "./developer"; import documents from "./documents"; import events from "./events"; import fileOperationsRoute from "./fileOperations"; @@ -70,6 +72,10 @@ router.use("/", groups.routes()); router.use("/", fileOperationsRoute.routes()); router.use("/", webhookSubscriptions.routes()); +if (env.ENVIRONMENT === "development") { + router.use("/", developer.routes()); +} + router.post("*", (ctx) => { ctx.throw(NotFoundError("Endpoint not found")); }); diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index f9e9b6711..bd028ab50 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -7,7 +7,7 @@ "Unstar": "Unstar", "Delete IndexedDB cache": "Delete IndexedDB cache", "IndexedDB cache deleted": "IndexedDB cache deleted", - "Development": "Development", + "Developer": "Developer", "Open document": "Open document", "New document": "New document", "Download": "Download",