Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# VitNode

- Always write and run unit tests in vitest for all new features and bug fixes.
- Don't use `React.FC` for defining React components. Instead, use the arrow function syntax.
- Don't use `any` type in TypeScript and use `unknown` as less as possible.
- Use `AutoForm` for forms instead of manually creating form components.
- After create/edit/delete operations, always refresh the data in the table to reflect the changes with notification using toast `sonner`.
- Use `React.lazy` and `Suspense` for code splitting and lazy loading for content-heavy dialogs like dialogs in forms.
42 changes: 37 additions & 5 deletions apps/api/src/locales/@vitnode/core/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"@vitnode/core:debug": "Debug Panel",
"@vitnode/core:debug:can_view": "View debug panel",
"@vitnode/core:debug:can_clear_cache": "Clear cache",
"@vitnode/core:system": "System",
"@vitnode/core:system:can_view": "View integrations",
"@vitnode/core:system:can_send_test_email": "Send test email",
"@vitnode/core:staff_moderators": "Staff: Moderators",
"@vitnode/core:staff_moderators:can_view": "View moderators list",
"@vitnode/core:staff_moderators:can_create": "Create moderators",
Expand Down Expand Up @@ -265,6 +268,10 @@
"debug": "Debug Panel",
"log_out": "Log Out"
},
"system": {
"title": "System",
"integrations": "Integrations"
},
"advanced": {
"title": "Advanced",
"cron": "Cron Jobs"
Expand Down Expand Up @@ -477,9 +484,7 @@
}
}
},
"debug": {
"title": "Debug Panel",
"desc": "Check logs, errors, and other debug information.",
"system": {
"integrations": {
"title": "Integrations",
"desc": "Status of the core services that power this instance.",
Expand All @@ -500,7 +505,30 @@
},
"email": {
"title": "Email",
"desc": "Transactional emails such as sign-up confirmations and password resets."
"desc": "Transactional emails such as sign-up confirmations and password resets.",
"test": {
"label": "Test email",
"title": "Send a test email",
"desc": "Send an email to any address to confirm your email adapter is working.",
"info": {
"title": "How to verify your configuration",
"desc": "If the message arrives in the inbox, your email adapter is configured correctly. If it doesn't show up, double-check your adapter settings and the system logs."
},
"to": {
"label": "Recipient",
"invalid": "Please enter a valid email address."
},
"subject": {
"label": "Subject",
"default": "VitNode test email"
},
"content": {
"label": "Content",
"default": "This is a test email sent from the VitNode admin panel."
},
"submit": "Send test email",
"success": "Test email sent. Check the recipient's inbox to confirm your configuration."
}
},
"captcha": {
"title": "Captcha",
Expand All @@ -517,7 +545,11 @@
"not_configured": "No cron adapter configured — jobs won't run automatically.",
"jobs": "{count, plural, =0 {No jobs scheduled} one {# job scheduled} other {# jobs scheduled}}"
}
},
}
},
"debug": {
"title": "Debug Panel",
"desc": "Check logs, errors, and other debug information.",
"actions": {
"clear_cache": {
"label": "Clear Cache",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ import { DataTableSkeleton } from "@vitnode/core/components/table/data-table";
import { HeaderContent } from "@vitnode/core/components/ui/header-content";
import { checkAdminPermissionApi } from "@vitnode/core/lib/api/get-session-admin-api";
import { ClearCacheAction } from "@vitnode/core/views/admin/views/core/debug/actions/clear-cache/clear-cache";
import {
IntegrationsView,
IntegrationsViewSkeleton,
} from "@vitnode/core/views/admin/views/core/debug/integrations/integrations-view";

const SystemLogsView = dynamic(async () =>
import("@vitnode/core/views/admin/views/core/debug/system-logs/system-logs-view").then(
Expand Down Expand Up @@ -50,14 +46,6 @@ export default async function Page(
{canClearCache && <ClearCacheAction />}
</HeaderContent>

<HeaderContent
desc={t("integrations.desc")}
h2={t("integrations.title")}
/>
<React.Suspense fallback={<IntegrationsViewSkeleton />}>
<IntegrationsView />
</React.Suspense>

<HeaderContent className="mt-8" h2={t("logs.title")} />
<React.Suspense fallback={<DataTableSkeleton columns={4} />}>
<SystemLogsView {...props} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getTranslations } from "next-intl/server";
import { notFound } from "next/navigation";
import React from "react";

import { I18nProvider } from "@vitnode/core/components/i18n-provider";
import { HeaderContent } from "@vitnode/core/components/ui/header-content";
import { checkAdminPermissionApi } from "@vitnode/core/lib/api/get-session-admin-api";
import {
IntegrationsView,
IntegrationsViewSkeleton,
} from "@vitnode/core/views/admin/views/core/system/integrations/integrations-view";

export const generateMetadata = async () => {
const t = await getTranslations("admin.system.integrations");

return {
title: t("title"),
description: t("desc"),
};
};

export default async function Page() {
const [t, canView] = await Promise.all([
getTranslations("admin.system.integrations"),
checkAdminPermissionApi({ module: "system", permission: "can_view" }),
]);

if (!canView) {
notFound();
}

return (
<I18nProvider namespaces="admin.system.integrations">
<div className="p-4">
<HeaderContent desc={t("desc")} h1={t("title")} />

<React.Suspense fallback={<IntegrationsViewSkeleton />}>
<IntegrationsView />
</React.Suspense>
</div>
</I18nProvider>
);
}
42 changes: 37 additions & 5 deletions apps/docs/src/locales/@vitnode/core/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"@vitnode/core:debug": "Debug Panel",
"@vitnode/core:debug:can_view": "View debug panel",
"@vitnode/core:debug:can_clear_cache": "Clear cache",
"@vitnode/core:system": "System",
"@vitnode/core:system:can_view": "View integrations",
"@vitnode/core:system:can_send_test_email": "Send test email",
"@vitnode/core:staff_moderators": "Staff: Moderators",
"@vitnode/core:staff_moderators:can_view": "View moderators list",
"@vitnode/core:staff_moderators:can_create": "Create moderators",
Expand Down Expand Up @@ -265,6 +268,10 @@
"debug": "Debug Panel",
"log_out": "Log Out"
},
"system": {
"title": "System",
"integrations": "Integrations"
},
"advanced": {
"title": "Advanced",
"cron": "Cron Jobs"
Expand Down Expand Up @@ -477,9 +484,7 @@
}
}
},
"debug": {
"title": "Debug Panel",
"desc": "Check logs, errors, and other debug information.",
"system": {
"integrations": {
"title": "Integrations",
"desc": "Status of the core services that power this instance.",
Expand All @@ -500,7 +505,30 @@
},
"email": {
"title": "Email",
"desc": "Transactional emails such as sign-up confirmations and password resets."
"desc": "Transactional emails such as sign-up confirmations and password resets.",
"test": {
"label": "Test email",
"title": "Send a test email",
"desc": "Send an email to any address to confirm your email adapter is working.",
"info": {
"title": "How to verify your configuration",
"desc": "If the message arrives in the inbox, your email adapter is configured correctly. If it doesn't show up, double-check your adapter settings and the system logs."
},
"to": {
"label": "Recipient",
"invalid": "Please enter a valid email address."
},
"subject": {
"label": "Subject",
"default": "VitNode test email"
},
"content": {
"label": "Content",
"default": "This is a test email sent from the VitNode admin panel."
},
"submit": "Send test email",
"success": "Test email sent. Check the recipient's inbox to confirm your configuration."
}
},
"captcha": {
"title": "Captcha",
Expand All @@ -517,7 +545,11 @@
"not_configured": "No cron adapter configured — jobs won't run automatically.",
"jobs": "{count, plural, =0 {No jobs scheduled} one {# job scheduled} other {# jobs scheduled}}"
}
},
}
},
"debug": {
"title": "Debug Panel",
"desc": "Check logs, errors, and other debug information.",
"actions": {
"clear_cache": {
"label": "Clear Cache",
Expand Down
3 changes: 0 additions & 3 deletions apps/docs/src/vitnode.api.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ export const POSTGRES_URL =

export const vitNodeApiConfig = buildApiConfig({
pathToMessages: async path => await import(`./locales/${path}`),
// Redis is opt-in: only enabled when REDIS_URL is set. Without it the cache
// is a no-op, the rate limiter uses in-memory storage, and WebSockets run in
// single-instance mode.
redis: process.env.REDIS_URL
? { url: process.env.REDIS_URL, password: process.env.REDIS_PASSWORD }
: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { CONFIG_PLUGIN } from "../../../../config";
import { buildModule } from "../../../lib/module";
import { integrationsDebugAdminRoute } from "./routes/integrations.route";
import { logsDebugAdminRoute } from "./routes/logs.route";
import { sendTestEmailDebugAdminRoute } from "./routes/send-test-email.route";

export const debugAdminModule = buildModule({
pluginId: CONFIG_PLUGIN.pluginId,
name: "debug",
routes: [logsDebugAdminRoute, integrationsDebugAdminRoute],
routes: [
logsDebugAdminRoute,
integrationsDebugAdminRoute,
sendTestEmailDebugAdminRoute,
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { buildRoute } from "../../../../lib/route";

export const integrationsDebugAdminRoute = buildRoute({
pluginId: CONFIG_PLUGIN.pluginId,
adminStaffPermission: { module: "debug", permission: "can_view" },
adminStaffPermission: { module: "system", permission: "can_view" },
route: {
method: "get",
description:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { HTTPException } from "hono/http-exception";
import { z } from "zod";

import { buildRoute } from "@/api/lib/route";
import { CONFIG_PLUGIN } from "@/config";
import TestEmailTemplate from "@/emails/test-email";

export const zodSendTestEmailSchema = z.object({
content: z.string().min(1).max(5000),
subject: z.string().min(1).max(200),
to: z.email(),
});

export const sendTestEmailDebugAdminRoute = buildRoute({
pluginId: CONFIG_PLUGIN.pluginId,
adminStaffPermission: { module: "system", permission: "can_send_test_email" },
route: {
method: "post",
description:
"Send a test email to verify that the email adapter is configured correctly.",
path: "/send-test-email",
request: {
body: {
required: true,
content: {
"application/json": {
schema: zodSendTestEmailSchema,
},
},
},
},
responses: {
200: {
content: {
"application/json": {
schema: z.object({ success: z.boolean() }),
},
},
description: "Test email sent",
},
400: {
description: "Email adapter not configured",
},
},
},
handler: async c => {
// The adapter check gives a clear error instead of the generic 500 the
// email model would otherwise throw when no provider is configured.
if (!c.get("core").email?.adapter) {
throw new HTTPException(400, {
message: "Email provider not configured",
});
}

const { to, subject, content } = c.req.valid("json");

await c.get("email").send({
to,
locale: "en",
subject,
content: props => TestEmailTemplate({ ...props, content }),
});

return c.json({ success: true });
},
Comment thread
aXenDeveloper marked this conversation as resolved.
});
4 changes: 4 additions & 0 deletions packages/vitnode/src/api/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const newBuildPluginApiCore = buildApiPlugin({
"can_view",
{ permission: "can_clear_cache", dependsOn: ["can_view"] },
],
system: [
"can_view",
{ permission: "can_send_test_email", dependsOn: ["can_view"] },
],
staff_moderators: [
"can_view",
{ permission: "can_create", dependsOn: ["can_view"] },
Expand Down
21 changes: 21 additions & 0 deletions packages/vitnode/src/emails/test-email.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Text } from "@react-email/components";

import DefaultTemplateEmail, {
type DefaultTemplateEmailProps,
} from "./default-template";

export default function TestEmailTemplate({
content,
...props
}: DefaultTemplateEmailProps & { content: string }) {
return (
<DefaultTemplateEmail {...props}>
<Text className="whitespace-pre-line">{content}</Text>
</DefaultTemplateEmail>
);
}

TestEmailTemplate.PreviewProps = {
...DefaultTemplateEmail.PreviewProps,
content: "This is a test email sent from the VitNode admin panel.",
} satisfies DefaultTemplateEmailProps & { content: string };
4 changes: 2 additions & 2 deletions packages/vitnode/src/lib/api/get-next-cron-run-date.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import cronParser from "cron-parser";
import { CronExpressionParser } from "cron-parser";

export const getNextCronRunDate = (
schedule: string,
Expand All @@ -9,7 +9,7 @@ export const getNextCronRunDate = (
currentDate: lastRun ?? new Date(0),
};

const interval = cronParser.parse(schedule, options);
const interval = CronExpressionParser.parse(schedule, options);

return interval.next().toDate();
} catch (err) {
Expand Down
4 changes: 2 additions & 2 deletions packages/vitnode/src/lib/api/should-cron-job-run.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import cronParser from "cron-parser";
import { CronExpressionParser } from "cron-parser";

export const shouldCronJobRun = (
schedule: string,
Expand All @@ -10,7 +10,7 @@ export const shouldCronJobRun = (
currentDate: lastRun ?? new Date(0),
};

const interval = cronParser.parse(schedule, options);
const interval = CronExpressionParser.parse(schedule, options);
const nextScheduledRun = interval.next().toDate();

return nextScheduledRun <= now;
Expand Down
Loading
Loading