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
2 changes: 2 additions & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"@fastify/helmet": "^13.0.2",
"@fastify/rate-limit": "^10.3.0",
"@fastify/static": "^9.0.0",
"@fastify/swagger": "^9.8.0",
"@fastify/swagger-ui": "^6.1.0",
"@fluxcore/config": "workspace:*",
"@fluxcore/database": "workspace:*",
"@fluxcore/i18n": "workspace:*",
Expand Down
229 changes: 218 additions & 11 deletions apps/dashboard/src/server/features/actions/routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FastifyInstance } from "fastify";
import { withDocs } from "../../shared/openapi-schemas.js";
import { requireAuth, requireGuildAdmin, requirePermission } from "../../shared/middleware.js";
import {
createRule,
Expand Down Expand Up @@ -115,11 +116,68 @@ function validateRuleBody(
return null;
}

const ruleRequestBodySchema = {
type: "object",
properties: {
name: { type: "string", maxLength: 50 },
eventType: { type: "string" },
actions: {
type: "array",
items: {
type: "object",
properties: {
type: { type: "string" },
webhook: {
type: "object",
properties: { url: { type: "string" } },
},
},
additionalProperties: true,
},
},
steps: {
type: "array",
maxItems: 10,
items: {
type: "object",
properties: {
id: { type: "string" },
type: { type: "string" },
},
additionalProperties: true,
},
},
entryStepId: { type: "string" },
conditions: { type: "object", additionalProperties: true },
priority: { type: "integer" },
enabled: { type: "boolean" },
},
additionalProperties: true,
} as const;

export function registerActionRoutes(app: FastifyInstance): void {
// --- Constants (for frontend dropdowns) ---
app.get(
"/api/actions/constants",
{ preHandler: [requireAuth] },
{
preHandler: [requireAuth],
schema: withDocs(undefined, {
tag: "Actions",
response: {
200: {
type: "object",
properties: {
eventTypes: { type: "object", additionalProperties: true },
actionTypes: { type: "object", additionalProperties: true },
maxActionsPerRule: { type: "integer" },
actionTypeFields: { type: "object", additionalProperties: true },
eventTypeVariables: { type: "object", additionalProperties: true },
templateVariables: { type: "object", additionalProperties: true },
},
},
},
}),
},
async (_request, reply) => {
reply.send({
eventTypes: EVENT_TYPES,
Expand All @@ -135,7 +193,13 @@ export function registerActionRoutes(app: FastifyInstance): void {
// --- Rules CRUD ---
app.get(
"/api/guilds/:guildId/actions/rules",
{ preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.view")] },
{
preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.view")],
schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, {
tag: "Actions",
response: { 200: { type: "array", items: { type: "object", additionalProperties: true } } },
}),
},
async (request, reply) => {
const { guildId } = request.params as { guildId: string };
const [rules, lastFiredMap] = await Promise.all([
Expand All @@ -152,7 +216,19 @@ export function registerActionRoutes(app: FastifyInstance): void {

app.post(
"/api/guilds/:guildId/actions/rules",
{ preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.manage")] },
{
preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.manage")],
schema: withDocs(
{
params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] },
body: ruleRequestBodySchema,
},
{
tag: "Actions",
response: { 201: { type: "object", additionalProperties: true } },
},
),
},
async (request, reply) => {
const { guildId } = request.params as { guildId: string };
const body = request.body as RuleRequestBody;
Expand Down Expand Up @@ -194,7 +270,19 @@ export function registerActionRoutes(app: FastifyInstance): void {

app.put(
"/api/guilds/:guildId/actions/rules/:ruleId",
{ preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.manage")] },
{
preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.manage")],
schema: withDocs(
{
params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] },
body: ruleRequestBodySchema,
},
{
tag: "Actions",
response: { 200: { type: "object", additionalProperties: true } },
},
),
},
async (request, reply) => {
const { guildId, ruleId } = request.params as {
guildId: string;
Expand Down Expand Up @@ -246,7 +334,13 @@ export function registerActionRoutes(app: FastifyInstance): void {

app.delete(
"/api/guilds/:guildId/actions/rules/:ruleId",
{ preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.manage")] },
{
preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.manage")],
schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, {
tag: "Actions",
response: { 200: { type: "object", properties: { success: { type: "boolean" } } } },
}),
},
async (request, reply) => {
const { guildId, ruleId } = request.params as {
guildId: string;
Expand All @@ -263,7 +357,38 @@ export function registerActionRoutes(app: FastifyInstance): void {
// --- Bulk Operations ---
app.patch(
"/api/guilds/:guildId/actions/rules/bulk",
{ preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.execute")] },
{
preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.rules.execute")],
schema: withDocs(
{
params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] },
body: {
type: "object",
required: ["ruleIds", "action"],
properties: {
ruleIds: {
type: "array",
maxItems: 50,
items: { type: "integer" },
},
action: { type: "string", enum: ["enable", "disable", "delete"] },
},
},
},
{
tag: "Actions",
response: {
200: {
type: "object",
properties: {
success: { type: "boolean" },
count: { type: "integer" },
},
},
},
},
),
},
async (request, reply) => {
const { guildId } = request.params as { guildId: string };
const body = request.body as {
Expand Down Expand Up @@ -307,7 +432,22 @@ export function registerActionRoutes(app: FastifyInstance): void {
// --- Per-Rule Analytics ---
app.get(
"/api/guilds/:guildId/actions/rules/:ruleId/analytics",
{ preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.analytics.view")] },
{
preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.analytics.view")],
schema: withDocs(
{
params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] },
querystring: {
type: "object",
properties: { days: { type: "string" } },
},
},
{
tag: "Actions",
response: { 200: { type: "object", additionalProperties: true } },
},
),
},
async (request, reply) => {
const { guildId, ruleId } = request.params as {
guildId: string;
Expand All @@ -323,7 +463,22 @@ export function registerActionRoutes(app: FastifyInstance): void {
// --- Settings ---
app.get(
"/api/guilds/:guildId/actions/settings",
{ preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.settings.manage")] },
{
preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.settings.manage")],
schema: withDocs({ params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] } }, {
tag: "Actions",
response: {
200: {
type: "object",
properties: {
maxRules: { type: "integer" },
globalEnabled: { type: "boolean" },
logChannelId: { type: ["string", "null"] },
},
},
},
}),
},
async (request, reply) => {
const { guildId } = request.params as { guildId: string };
reply.send(getGuildSettingsOrDefault(guildId));
Expand All @@ -332,7 +487,26 @@ export function registerActionRoutes(app: FastifyInstance): void {

app.put(
"/api/guilds/:guildId/actions/settings",
{ preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.settings.manage")] },
{
preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.settings.manage")],
schema: withDocs(
{
params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] },
body: {
type: "object",
properties: {
maxRules: { type: "integer" },
globalEnabled: { type: "boolean" },
logChannelId: { type: ["string", "null"] },
},
},
},
{
tag: "Actions",
response: { 200: { type: "object", properties: { success: { type: "boolean" } } } },
},
),
},
async (request, reply) => {
const { guildId } = request.params as { guildId: string };
const body = request.body as {
Expand Down Expand Up @@ -373,7 +547,22 @@ export function registerActionRoutes(app: FastifyInstance): void {
// --- Analytics ---
app.get(
"/api/guilds/:guildId/actions/analytics",
{ preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.analytics.view")] },
{
preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.analytics.view")],
schema: withDocs(
{
params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] },
querystring: {
type: "object",
properties: { days: { type: "string" } },
},
},
{
tag: "Actions",
response: { 200: { type: "object", additionalProperties: true } },
},
),
},
async (request, reply) => {
const { guildId } = request.params as { guildId: string };
const query = request.query as { days?: string };
Expand All @@ -386,7 +575,25 @@ export function registerActionRoutes(app: FastifyInstance): void {
// --- Logs ---
app.get(
"/api/guilds/:guildId/actions/logs",
{ preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.analytics.view")] },
{
preHandler: [requireAuth, requireGuildAdmin, requirePermission("actions.analytics.view")],
schema: withDocs(
{
params: { type: "object", properties: { guildId: { type: "string" } }, required: ["guildId"] },
querystring: {
type: "object",
properties: {
ruleName: { type: "string" },
limit: { type: "string" },
},
},
},
{
tag: "Actions",
response: { 200: { type: "array", items: { type: "object", additionalProperties: true } } },
},
),
},
async (request, reply) => {
const { guildId } = request.params as { guildId: string };
const query = request.query as { ruleName?: string; limit?: string };
Expand Down
11 changes: 6 additions & 5 deletions apps/dashboard/src/server/features/auth/routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FastifyInstance } from "fastify";
import { withDocs } from "../../shared/openapi-schemas.js";
import { config } from "@fluxcore/config";
import {
buildCallbackUrl,
Expand All @@ -20,7 +21,7 @@ const authRateLimit = {
};

export function registerAuthRoutes(app: FastifyInstance): void {
app.get("/auth/login", { ...authRateLimit }, async (_request, reply) => {
app.get("/auth/login", { ...authRateLimit, schema: withDocs(undefined, { tag: "Auth", secure: false }) }, async (_request, reply) => {
const callbackUrl = buildCallbackUrl(config.dashboardPublicUrl);
const { url, state } = getAuthorizationUrl(callbackUrl);
reply
Expand All @@ -35,7 +36,7 @@ export function registerAuthRoutes(app: FastifyInstance): void {
.redirect(url);
});

app.get("/auth/callback", { ...authRateLimit }, async (request, reply) => {
app.get("/auth/callback", { ...authRateLimit, schema: withDocs(undefined, { tag: "Auth", secure: false }) }, async (request, reply) => {
const { code, state } = request.query as {
code?: string;
state?: string;
Expand Down Expand Up @@ -100,7 +101,7 @@ export function registerAuthRoutes(app: FastifyInstance): void {
}
});

app.get("/auth/csrf", async (_request, reply) => {
app.get("/auth/csrf", { schema: withDocs(undefined, { tag: "Auth", secure: false, response: { 200: { type: "object", properties: { token: { type: "string" } } } } }) }, async (_request, reply) => {
const token = generateCsrfToken();
reply
.setCookie("csrf_token", token, {
Expand All @@ -113,7 +114,7 @@ export function registerAuthRoutes(app: FastifyInstance): void {
.send({ token });
});

app.get("/auth/logout", async (request, reply) => {
app.get("/auth/logout", { schema: withDocs(undefined, { tag: "Auth", secure: false }) }, async (request, reply) => {
const sessionCookie = request.cookies?.session;
if (sessionCookie) {
const unsigned = request.unsignCookie(sessionCookie);
Expand All @@ -124,7 +125,7 @@ export function registerAuthRoutes(app: FastifyInstance): void {
reply.clearCookie("session", { path: "/" }).redirect("/");
});

app.get("/auth/me", async (request, reply) => {
app.get("/auth/me", { schema: withDocs(undefined, { tag: "Auth", secure: false, response: { 200: { type: "object", properties: { userId: { type: "string" }, username: { type: "string" }, avatar: { type: ["string", "null"] } } } } }) }, async (request, reply) => {
const sessionCookie = request.cookies?.session;
if (!sessionCookie) {
reply.code(401).send({ error: "Not authenticated" });
Expand Down
Loading
Loading