Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 520f917723 | |||
| d1dd77bc1e | |||
| 0be45b6513 | |||
| 4b1274e34a | |||
| 2f66fff993 | |||
| bcae59626f | |||
| f851305d78 | |||
| 743b177c89 |
+15
-11
@@ -14,6 +14,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
run: npm install -g pnpm@11.18.0
|
||||||
|
|
||||||
|
- name: Security audit (fails on any prod or high/critical finding)
|
||||||
|
run: |
|
||||||
|
pnpm audit --prod
|
||||||
|
pnpm audit --audit-level high
|
||||||
|
|
||||||
- name: Install Docker CLI
|
- name: Install Docker CLI
|
||||||
run: |
|
run: |
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
@@ -30,33 +38,29 @@ jobs:
|
|||||||
FULL_SHA=$(git rev-parse HEAD)
|
FULL_SHA=$(git rev-parse HEAD)
|
||||||
IMAGE="git.kubebase.de/${{ gitea.repository }}"
|
IMAGE="git.kubebase.de/${{ gitea.repository }}"
|
||||||
DATE_STAMP=$(date -u +"%Y%m%d")
|
DATE_STAMP=$(date -u +"%Y%m%d")
|
||||||
|
VERSION="dev-$(date -u +"%Y%m%d-%H%M")"
|
||||||
|
TAGS="-t ${IMAGE}:latest"
|
||||||
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
||||||
VERSION="${{ gitea.ref_name }}"
|
VERSION="${{ gitea.ref_name }}"
|
||||||
VERSION_TAG="${{ gitea.ref_name }}"
|
TAGS="${TAGS} -t ${IMAGE}:${VERSION}"
|
||||||
else
|
|
||||||
VERSION="dev-$(date -u +"%Y%m%d-%H%M")"
|
|
||||||
VERSION_TAG="nightly-${DATE_STAMP}"
|
|
||||||
fi
|
fi
|
||||||
TAGS="-t ${IMAGE}:sha-${SHA} -t ${IMAGE}:latest -t ${IMAGE}:${VERSION_TAG}"
|
|
||||||
docker build --no-cache \
|
docker build --no-cache \
|
||||||
--build-arg COMMIT_SHA="$FULL_SHA" \
|
--build-arg COMMIT_SHA="$FULL_SHA" \
|
||||||
--build-arg BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
--build-arg BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
||||||
--build-arg VERSION="$VERSION" \
|
--build-arg VERSION="$VERSION" \
|
||||||
$TAGS .
|
$TAGS .
|
||||||
docker push "${IMAGE}:sha-${SHA}"
|
|
||||||
docker push "${IMAGE}:latest"
|
docker push "${IMAGE}:latest"
|
||||||
docker push "${IMAGE}:${VERSION_TAG}"
|
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
||||||
|
docker push "${IMAGE}:${VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Update k8s manifest in admin/apps
|
- name: Update k8s manifest in admin/apps
|
||||||
|
if: gitea.ref_type == 'tag'
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
SHA=$(git rev-parse --short HEAD)
|
SHA=$(git rev-parse --short HEAD)
|
||||||
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
|
||||||
NEWTAG="${{ gitea.ref_name }}"
|
NEWTAG="${{ gitea.ref_name }}"
|
||||||
else
|
|
||||||
NEWTAG="sha-${SHA}"
|
|
||||||
fi
|
|
||||||
git clone "https://admin:${GITEA_TOKEN}@git.kubebase.de/admin/apps.git" /tmp/apps
|
git clone "https://admin:${GITEA_TOKEN}@git.kubebase.de/admin/apps.git" /tmp/apps
|
||||||
cd /tmp/apps
|
cd /tmp/apps
|
||||||
cd apps/system/toolrate/overlays/k3s
|
cd apps/system/toolrate/overlays/k3s
|
||||||
|
|||||||
@@ -47,3 +47,6 @@ Thumbs.db
|
|||||||
# Replit
|
# Replit
|
||||||
.cache/
|
.cache/
|
||||||
.local/
|
.local/
|
||||||
|
|
||||||
|
# Generated release docs (produced by scripts/src/sync-release-docs.mjs during build)
|
||||||
|
/artifacts/toolrate/public/docs/
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
auto-install-peers=false
|
auto-install-peers=false
|
||||||
strict-peer-dependencies=false
|
strict-peer-dependencies=false
|
||||||
|
save-exact=true
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
FROM node:24-alpine AS builder
|
FROM node:24.18.1-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN npm install -g pnpm@10.26.1
|
RUN npm install -g pnpm@11.18.0
|
||||||
|
|
||||||
# 1. Alle Projektdateien in den Container bringen
|
# 1. Alle Projektdateien in den Container bringen
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -17,10 +17,10 @@ ENV PORT=8080
|
|||||||
|
|
||||||
RUN pnpm -r --if-present run build
|
RUN pnpm -r --if-present run build
|
||||||
|
|
||||||
FROM node:24-alpine AS runner
|
FROM node:24.18.1-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN npm install -g pnpm@10.26.1
|
RUN npm install -g pnpm@11.18.0
|
||||||
|
|
||||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||||
COPY lib/db/package.json lib/db/
|
COPY lib/db/package.json lib/db/
|
||||||
|
|||||||
@@ -12,30 +12,29 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@workspace/api-zod": "workspace:*",
|
"@workspace/api-zod": "workspace:*",
|
||||||
"@workspace/db": "workspace:*",
|
"@workspace/db": "workspace:*",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "3.0.3",
|
||||||
"connect-pg-simple": "^10.0.0",
|
"connect-pg-simple": "10.0.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"cors": "^2.8.6",
|
"cors": "2.8.6",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"express": "^5.2.1",
|
"express": "5.2.1",
|
||||||
"express-rate-limit": "^8.6.1",
|
"express-rate-limit": "8.6.1",
|
||||||
"express-session": "^1.19.0",
|
"express-session": "1.19.0",
|
||||||
"openid-client": "^5.7.1",
|
"openid-client": "6.8.4",
|
||||||
"pino": "^9.14.0",
|
"pino": "10.3.1",
|
||||||
"pino-http": "^10.5.0",
|
"pino-http": "11.0.0",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/connect-pg-simple": "7.0.3",
|
||||||
"@types/connect-pg-simple": "^7.0.3",
|
"@types/cookie-parser": "1.4.10",
|
||||||
"@types/cookie-parser": "^1.4.10",
|
"@types/cors": "2.8.19",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/express": "5.0.6",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express-session": "1.19.0",
|
||||||
"@types/express-session": "^1.19.0",
|
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"esbuild": "0.27.3",
|
"esbuild": "0.28.1",
|
||||||
"esbuild-plugin-pino": "^2.3.3",
|
"esbuild-plugin-pino": "2.3.3",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "13.1.3",
|
||||||
"thread-stream": "3.1.0"
|
"thread-stream": "4.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import type { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
|
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
|
||||||
|
|
||||||
|
export function getCsrfToken(req: Request): string {
|
||||||
|
if (!req.session.csrfToken) {
|
||||||
|
req.session.csrfToken = crypto.randomBytes(24).toString("hex");
|
||||||
|
}
|
||||||
|
return req.session.csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function csrfProtection(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
if (SAFE_METHODS.has(req.method.toUpperCase())) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const provided = req.headers["x-csrf-token"];
|
||||||
|
const token = getCsrfToken(req);
|
||||||
|
if (typeof provided === "string" && provided && provided === token) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(403).json({ error: "CSRF token missing or invalid" });
|
||||||
|
}
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
import { Router, type IRouter, type Request } from "express";
|
import { Router, type IRouter, type Request } from "express";
|
||||||
import { Issuer, generators, type Client } from "openid-client";
|
import {
|
||||||
|
discovery,
|
||||||
|
randomPKCECodeVerifier,
|
||||||
|
calculatePKCECodeChallenge,
|
||||||
|
randomState,
|
||||||
|
buildAuthorizationUrl,
|
||||||
|
authorizationCodeGrant,
|
||||||
|
fetchUserInfo,
|
||||||
|
buildEndSessionUrl,
|
||||||
|
skipSubjectCheck,
|
||||||
|
type Configuration,
|
||||||
|
} from "openid-client";
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { eq, and, inArray, isNull } from "drizzle-orm";
|
import { eq, and, inArray, isNull } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -8,10 +19,11 @@ import { logger } from "../lib/logger";
|
|||||||
import { writeAuditLog } from "../lib/audit";
|
import { writeAuditLog } from "../lib/audit";
|
||||||
import { loginRateLimit, passwordRateLimit } from "../lib/rate-limit";
|
import { loginRateLimit, passwordRateLimit } from "../lib/rate-limit";
|
||||||
import { getEntitlements, requireFeature } from "../middleware/feature";
|
import { getEntitlements, requireFeature } from "../middleware/feature";
|
||||||
|
import { getCsrfToken } from "../middleware/csrf";
|
||||||
|
|
||||||
const router: IRouter = Router();
|
const router: IRouter = Router();
|
||||||
|
|
||||||
let cachedClient: Client | null = null;
|
let cachedConfig: Configuration | null = null;
|
||||||
|
|
||||||
function isOidcConfigured(): boolean {
|
function isOidcConfigured(): boolean {
|
||||||
return !!(
|
return !!(
|
||||||
@@ -38,8 +50,8 @@ function isSafeReturnTo(value: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getClient(): Promise<Client | null> {
|
async function getClient(): Promise<Configuration | null> {
|
||||||
if (cachedClient) return cachedClient;
|
if (cachedConfig) return cachedConfig;
|
||||||
|
|
||||||
const keycloakUrl = process.env.KEYCLOAK_URL;
|
const keycloakUrl = process.env.KEYCLOAK_URL;
|
||||||
const realm = process.env.KEYCLOAK_REALM;
|
const realm = process.env.KEYCLOAK_REALM;
|
||||||
@@ -51,14 +63,9 @@ async function getClient(): Promise<Client | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const issuerUrl = `${keycloakUrl}/realms/${realm}`;
|
const issuerUrl = new URL(`${keycloakUrl}/realms/${realm}`);
|
||||||
const issuer = await Issuer.discover(issuerUrl);
|
cachedConfig = await discovery(issuerUrl, clientId, clientSecret);
|
||||||
cachedClient = new issuer.Client({
|
return cachedConfig;
|
||||||
client_id: clientId,
|
|
||||||
client_secret: clientSecret,
|
|
||||||
response_types: ["code"],
|
|
||||||
});
|
|
||||||
return cachedClient;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err }, "Failed to discover Keycloak issuer");
|
logger.error({ err }, "Failed to discover Keycloak issuer");
|
||||||
return null;
|
return null;
|
||||||
@@ -109,6 +116,10 @@ router.get("/auth/mode", (_req, res): void => {
|
|||||||
res.json({ mode: isOidcConfigured() ? "oidc" : "local" });
|
res.json({ mode: isOidcConfigured() ? "oidc" : "local" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/auth/csrf", (req, res): void => {
|
||||||
|
res.json({ token: getCsrfToken(req) });
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/auth/login", loginRateLimit, async (req, res): Promise<void> => {
|
router.post("/auth/login", loginRateLimit, async (req, res): Promise<void> => {
|
||||||
if (isOidcConfigured()) {
|
if (isOidcConfigured()) {
|
||||||
res.status(400).json({ error: "Use OIDC login when Keycloak is configured." });
|
res.status(400).json({ error: "Use OIDC login when Keycloak is configured." });
|
||||||
@@ -171,9 +182,9 @@ router.get("/auth/login", async (req, res): Promise<void> => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeVerifier = generators.codeVerifier();
|
const codeVerifier = randomPKCECodeVerifier();
|
||||||
const codeChallenge = generators.codeChallenge(codeVerifier);
|
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
||||||
const state = generators.state();
|
const state = randomState();
|
||||||
|
|
||||||
req.session.codeVerifier = codeVerifier;
|
req.session.codeVerifier = codeVerifier;
|
||||||
req.session.oidcState = state;
|
req.session.oidcState = state;
|
||||||
@@ -182,7 +193,7 @@ router.get("/auth/login", async (req, res): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const redirectUri = `${getBaseUrl(req)}/api/auth/callback`;
|
const redirectUri = `${getBaseUrl(req)}/api/auth/callback`;
|
||||||
const url = client.authorizationUrl({
|
const url = buildAuthorizationUrl(client, {
|
||||||
scope: "openid email profile",
|
scope: "openid email profile",
|
||||||
code_challenge: codeChallenge,
|
code_challenge: codeChallenge,
|
||||||
code_challenge_method: "S256",
|
code_challenge_method: "S256",
|
||||||
@@ -190,7 +201,7 @@ router.get("/auth/login", async (req, res): Promise<void> => {
|
|||||||
state,
|
state,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.redirect(url);
|
res.redirect(url.href);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/auth/callback", async (req, res): Promise<void> => {
|
router.get("/auth/callback", async (req, res): Promise<void> => {
|
||||||
@@ -216,13 +227,13 @@ router.get("/auth/callback", async (req, res): Promise<void> => {
|
|||||||
const redirectUri = `${getBaseUrl(req)}/api/auth/callback`;
|
const redirectUri = `${getBaseUrl(req)}/api/auth/callback`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = client.callbackParams(req);
|
const tokens = await authorizationCodeGrant(
|
||||||
const tokenSet = await client.callback(redirectUri, params, {
|
client,
|
||||||
code_verifier: codeVerifier,
|
new URL(req.originalUrl ?? "/", getBaseUrl(req)),
|
||||||
state,
|
{ pkceCodeVerifier: codeVerifier, expectedState: state },
|
||||||
});
|
);
|
||||||
|
|
||||||
const userinfo = await client.userinfo(tokenSet.access_token!);
|
const userinfo = await fetchUserInfo(client, tokens.access_token, skipSubjectCheck);
|
||||||
const dbUser = await upsertUserFromOidc(userinfo);
|
const dbUser = await upsertUserFromOidc(userinfo);
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@@ -254,9 +265,9 @@ router.get("/auth/logout", async (req, res): Promise<void> => {
|
|||||||
req.session.destroy(() => {});
|
req.session.destroy(() => {});
|
||||||
|
|
||||||
const client = await getClient();
|
const client = await getClient();
|
||||||
if (client && client.issuer.metadata.end_session_endpoint) {
|
if (client && client.serverMetadata().end_session_endpoint) {
|
||||||
const logoutUrl = client.endSessionUrl({ post_logout_redirect_uri: getBaseUrl(req) });
|
const logoutUrl = buildEndSessionUrl(client, { post_logout_redirect_uri: getBaseUrl(req) });
|
||||||
res.redirect(logoutUrl);
|
res.redirect(logoutUrl.href);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,13 +298,13 @@ router.get("/auth/password-redirect", async (req, res): Promise<void> => {
|
|||||||
res.json({ url: null });
|
res.json({ url: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const realm = client.issuer.metadata.issuer ?? "";
|
const realm = client.serverMetadata().issuer ?? "";
|
||||||
res.json({ url: `${realm}/account/password` });
|
res.json({ url: `${realm}/account/password` });
|
||||||
});
|
});
|
||||||
|
|
||||||
const ChangePasswordSchema = z.object({
|
const ChangePasswordSchema = z.object({
|
||||||
currentPassword: z.string().min(1),
|
currentPassword: z.string().min(1),
|
||||||
newPassword: z.string().min(8),
|
newPassword: z.string().min(6),
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/auth/me/password", passwordRateLimit, async (req, res): Promise<void> => {
|
router.post("/auth/me/password", passwordRateLimit, async (req, res): Promise<void> => {
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import usersRouter from "./users";
|
|||||||
import auditRouter from "./audit";
|
import auditRouter from "./audit";
|
||||||
import costsRouter from "./costs";
|
import costsRouter from "./costs";
|
||||||
import adminRouter from "./admin";
|
import adminRouter from "./admin";
|
||||||
|
import { csrfProtection } from "../middleware/csrf";
|
||||||
|
|
||||||
const router: IRouter = Router();
|
const router: IRouter = Router();
|
||||||
|
|
||||||
|
router.use(csrfProtection);
|
||||||
|
|
||||||
router.use(authRouter);
|
router.use(authRouter);
|
||||||
router.use(healthRouter);
|
router.use(healthRouter);
|
||||||
router.use(toolsRouter);
|
router.use(toolsRouter);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Router, type IRouter } from "express";
|
import { Router, type IRouter } from "express";
|
||||||
import { eq, desc, asc, sql, and, not, isNull, inArray } from "drizzle-orm";
|
import { eq, desc, asc, sql, and, not, isNull, inArray, type SQL } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, toolsTable, ratingsTable, toolRelationsTable } from "@workspace/db";
|
import { db, toolsTable, ratingsTable, toolRelationsTable } from "@workspace/db";
|
||||||
import {
|
import {
|
||||||
@@ -57,21 +57,23 @@ router.get("/tools", async (req, res): Promise<void> => {
|
|||||||
const tagList = (tags ?? "").split(",").map((t) => t.trim()).filter(Boolean);
|
const tagList = (tags ?? "").split(",").map((t) => t.trim()).filter(Boolean);
|
||||||
const featureList = (features ?? "").split(",").map((f) => f.trim()).filter(Boolean);
|
const featureList = (features ?? "").split(",").map((f) => f.trim()).filter(Boolean);
|
||||||
|
|
||||||
let query = db.select().from(toolsTable).where(isNull(toolsTable.deletedAt)).$dynamic();
|
const conditions: SQL[] = [isNull(toolsTable.deletedAt)];
|
||||||
if (category) {
|
if (category) {
|
||||||
query = query.where(eq(toolsTable.category, category));
|
conditions.push(eq(toolsTable.category, category));
|
||||||
}
|
}
|
||||||
if (search) {
|
if (search) {
|
||||||
const escaped = search.replace(/[%_\\]/g, (m) => `\\${m}`);
|
const escaped = search.replace(/[%_\\]/g, (m) => `\\${m}`);
|
||||||
query = query.where(sql`${toolsTable.name} ilike ${`%${escaped}%`} escape '\\'`);
|
conditions.push(sql`${toolsTable.name} ilike ${`%${escaped}%`} escape '\\'`);
|
||||||
}
|
}
|
||||||
if (tagList.length > 0) {
|
if (tagList.length > 0) {
|
||||||
query = query.where(sql`${toolsTable.tags} @> ARRAY[${sql.join(tagList.map((t) => sql`${t}`), sql`, `)}]::text[]`);
|
conditions.push(sql`${toolsTable.tags} @> ARRAY[${sql.join(tagList.map((t) => sql`${t}`), sql`, `)}]::text[]`);
|
||||||
}
|
}
|
||||||
if (featureList.length > 0) {
|
if (featureList.length > 0) {
|
||||||
query = query.where(sql`${toolsTable.features} @> ARRAY[${sql.join(featureList.map((f) => sql`${f}`), sql`, `)}]::text[]`);
|
conditions.push(sql`${toolsTable.features} @> ARRAY[${sql.join(featureList.map((f) => sql`${f}`), sql`, `)}]::text[]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const query = db.select().from(toolsTable).where(and(...conditions));
|
||||||
|
|
||||||
const tools = await query.orderBy(desc(toolsTable.createdAt));
|
const tools = await query.orderBy(desc(toolsTable.createdAt));
|
||||||
|
|
||||||
const toolIds = tools.map((t) => t.id);
|
const toolIds = tools.map((t) => t.id);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const UserUpdateSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const SetPasswordSchema = z.object({
|
const SetPasswordSchema = z.object({
|
||||||
password: z.string().min(8),
|
password: z.string().min(6),
|
||||||
});
|
});
|
||||||
|
|
||||||
router.patch("/users/:id/password", requireAdmin, passwordRateLimit, async (req, res): Promise<void> => {
|
router.patch("/users/:id/password", requireAdmin, passwordRateLimit, async (req, res): Promise<void> => {
|
||||||
|
|||||||
+1
@@ -14,5 +14,6 @@ declare module "express-session" {
|
|||||||
codeVerifier?: string;
|
codeVerifier?: string;
|
||||||
returnTo?: string;
|
returnTo?: string;
|
||||||
oidcState?: string;
|
oidcState?: string;
|
||||||
|
csrfToken?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,34 +10,34 @@
|
|||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "5.7.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "1.2.20",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "1.1.23",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
"@radix-ui/react-aspect-ratio": "1.1.15",
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "1.2.6",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "1.3.11",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "1.1.20",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@radix-ui/react-context-menu": "2.3.7",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "1.1.23",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "2.1.24",
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
"@radix-ui/react-hover-card": "1.1.23",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "2.1.15",
|
||||||
"@radix-ui/react-menubar": "^1.1.16",
|
"@radix-ui/react-menubar": "1.1.24",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
"@radix-ui/react-navigation-menu": "1.2.22",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "1.1.23",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "1.1.16",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@radix-ui/react-radio-group": "1.4.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "1.2.18",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "2.3.7",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "1.1.15",
|
||||||
"@radix-ui/react-slider": "^1.3.6",
|
"@radix-ui/react-slider": "1.4.7",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "1.3.3",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "1.3.7",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "1.1.21",
|
||||||
"@radix-ui/react-toast": "^1.2.15",
|
"@radix-ui/react-toast": "1.2.23",
|
||||||
"@radix-ui/react-toggle": "^1.1.10",
|
"@radix-ui/react-toggle": "1.1.18",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "1.1.19",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "1.2.16",
|
||||||
"@replit/vite-plugin-cartographer": "catalog:",
|
"@replit/vite-plugin-cartographer": "catalog:",
|
||||||
"@replit/vite-plugin-runtime-error-modal": "catalog:",
|
"@replit/vite-plugin-runtime-error-modal": "catalog:",
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
@@ -45,29 +45,30 @@
|
|||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"@types/react-dom": "catalog:",
|
"@types/react-dom": "catalog:",
|
||||||
"@vitejs/plugin-react": "catalog:",
|
"@vitejs/plugin-react": "catalog:",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "5.0.0",
|
||||||
"class-variance-authority": "catalog:",
|
"class-variance-authority": "catalog:",
|
||||||
"clsx": "catalog:",
|
"clsx": "catalog:",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "1.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "4.4.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "8.6.0",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "3.3.3",
|
||||||
"framer-motion": "catalog:",
|
"framer-motion": "catalog:",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"lucide-react": "catalog:",
|
"lucide-react": "catalog:",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-day-picker": "^9.14.0",
|
"react-day-picker": "10.0.1",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:",
|
||||||
"react-hook-form": "^7.75.0",
|
"react-hook-form": "7.84.0",
|
||||||
"react-resizable-panels": "^2.1.9",
|
"react-is": "19.2.8",
|
||||||
"recharts": "^2.15.4",
|
"react-resizable-panels": "4.12.2",
|
||||||
"sonner": "^2.0.7",
|
"recharts": "3.10.1",
|
||||||
|
"sonner": "2.0.7",
|
||||||
"tailwind-merge": "catalog:",
|
"tailwind-merge": "catalog:",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "1.0.7",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "1.4.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "1.1.2",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function Calendar({
|
|||||||
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
|
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
|
||||||
defaultClassNames.caption_label
|
defaultClassNames.caption_label
|
||||||
),
|
),
|
||||||
table: "w-full border-collapse",
|
month_grid: "w-full border-collapse",
|
||||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||||
weekday: cn(
|
weekday: cn(
|
||||||
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
|
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as RechartsPrimitive from "recharts"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
const THEMES = { light: "", dark: ".dark" } as const
|
const THEMES = { light: "", dark: ".dark" } as const
|
||||||
|
|
||||||
export type ChartConfig = {
|
export type ChartConfig = {
|
||||||
@@ -101,7 +102,7 @@ const ChartTooltip = RechartsPrimitive.Tooltip
|
|||||||
|
|
||||||
const ChartTooltipContent = React.forwardRef<
|
const ChartTooltipContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
RechartsPrimitive.TooltipContentProps &
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
hideLabel?: boolean
|
hideLabel?: boolean
|
||||||
hideIndicator?: boolean
|
hideIndicator?: boolean
|
||||||
@@ -191,7 +192,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.dataKey}
|
key={typeof item.dataKey === "string" || typeof item.dataKey === "number" ? item.dataKey : index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||||
indicator === "dot" && "items-center"
|
indicator === "dot" && "items-center"
|
||||||
@@ -260,7 +261,7 @@ const ChartLegend = RechartsPrimitive.Legend
|
|||||||
const ChartLegendContent = React.forwardRef<
|
const ChartLegendContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> &
|
React.ComponentProps<"div"> &
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
Pick<RechartsPrimitive.DefaultLegendContentProps, "payload" | "verticalAlign"> & {
|
||||||
hideIcon?: boolean
|
hideIcon?: boolean
|
||||||
nameKey?: string
|
nameKey?: string
|
||||||
}
|
}
|
||||||
@@ -317,6 +318,7 @@ const ChartLegendContent = React.forwardRef<
|
|||||||
)
|
)
|
||||||
ChartLegendContent.displayName = "ChartLegend"
|
ChartLegendContent.displayName = "ChartLegend"
|
||||||
|
|
||||||
|
// Helper to extract item config from a payload.
|
||||||
function getPayloadConfigFromPayload(
|
function getPayloadConfigFromPayload(
|
||||||
config: ChartConfig,
|
config: ChartConfig,
|
||||||
payload: unknown,
|
payload: unknown,
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import { cn } from "@/lib/utils"
|
|||||||
const ResizablePanelGroup = ({
|
const ResizablePanelGroup = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
}: React.ComponentProps<typeof ResizablePrimitive.Group>) => (
|
||||||
<ResizablePrimitive.PanelGroup
|
<ResizablePrimitive.Group
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
"flex h-full w-full data-[group-orientation=vertical]:flex-col",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -24,12 +24,12 @@ const ResizableHandle = ({
|
|||||||
withHandle,
|
withHandle,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
}: React.ComponentProps<typeof ResizablePrimitive.Separator> & {
|
||||||
withHandle?: boolean
|
withHandle?: boolean
|
||||||
}) => (
|
}) => (
|
||||||
<ResizablePrimitive.PanelResizeHandle
|
<ResizablePrimitive.Separator
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[group-orientation=vertical]:h-px data-[group-orientation=vertical]:w-full data-[group-orientation=vertical]:after:left-0 data-[group-orientation=vertical]:after:h-1 data-[group-orientation=vertical]:after:w-full data-[group-orientation=vertical]:after:-translate-y-1/2 data-[group-orientation=vertical]:after:translate-x-0 [&[data-group-orientation=vertical]>div]:rotate-90",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -39,7 +39,7 @@ const ResizableHandle = ({
|
|||||||
<GripVertical className="h-2.5 w-2.5" />
|
<GripVertical className="h-2.5 w-2.5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ResizablePrimitive.PanelResizeHandle>
|
</ResizablePrimitive.Separator>
|
||||||
)
|
)
|
||||||
|
|
||||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||||
|
|||||||
@@ -4,44 +4,44 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --config vite.config.ts --host 0.0.0.0",
|
"dev": "node ../../scripts/src/sync-release-docs.mjs && vite --config vite.config.ts --host 0.0.0.0",
|
||||||
"build": "vite build --config vite.config.ts",
|
"build": "node ../../scripts/src/sync-release-docs.mjs && vite build --config vite.config.ts",
|
||||||
"serve": "vite preview --config vite.config.ts --host 0.0.0.0",
|
"serve": "vite preview --config vite.config.ts --host 0.0.0.0",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "5.7.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.4",
|
"@radix-ui/react-accordion": "1.2.20",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
"@radix-ui/react-alert-dialog": "1.1.23",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
"@radix-ui/react-aspect-ratio": "1.1.15",
|
||||||
"@radix-ui/react-avatar": "^1.1.4",
|
"@radix-ui/react-avatar": "1.2.6",
|
||||||
"@radix-ui/react-checkbox": "^1.1.5",
|
"@radix-ui/react-checkbox": "1.3.11",
|
||||||
"@radix-ui/react-collapsible": "^1.1.4",
|
"@radix-ui/react-collapsible": "1.1.20",
|
||||||
"@radix-ui/react-context-menu": "^2.2.7",
|
"@radix-ui/react-context-menu": "2.3.7",
|
||||||
"@radix-ui/react-dialog": "^1.1.7",
|
"@radix-ui/react-dialog": "1.1.23",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.7",
|
"@radix-ui/react-dropdown-menu": "2.1.24",
|
||||||
"@radix-ui/react-hover-card": "^1.1.7",
|
"@radix-ui/react-hover-card": "1.1.23",
|
||||||
"@radix-ui/react-label": "^2.1.3",
|
"@radix-ui/react-label": "2.1.15",
|
||||||
"@radix-ui/react-menubar": "^1.1.7",
|
"@radix-ui/react-menubar": "1.1.24",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.6",
|
"@radix-ui/react-navigation-menu": "1.2.22",
|
||||||
"@radix-ui/react-popover": "^1.1.7",
|
"@radix-ui/react-popover": "1.1.23",
|
||||||
"@radix-ui/react-progress": "^1.1.3",
|
"@radix-ui/react-progress": "1.1.16",
|
||||||
"@radix-ui/react-radio-group": "^1.2.4",
|
"@radix-ui/react-radio-group": "1.4.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.4",
|
"@radix-ui/react-scroll-area": "1.2.18",
|
||||||
"@radix-ui/react-select": "^2.1.7",
|
"@radix-ui/react-select": "2.3.7",
|
||||||
"@radix-ui/react-separator": "^1.1.3",
|
"@radix-ui/react-separator": "1.1.15",
|
||||||
"@radix-ui/react-slider": "^1.2.4",
|
"@radix-ui/react-slider": "1.4.7",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "1.3.3",
|
||||||
"@radix-ui/react-switch": "^1.1.4",
|
"@radix-ui/react-switch": "1.3.7",
|
||||||
"@radix-ui/react-tabs": "^1.1.4",
|
"@radix-ui/react-tabs": "1.1.21",
|
||||||
"@radix-ui/react-toast": "^1.2.7",
|
"@radix-ui/react-toast": "1.2.23",
|
||||||
"@radix-ui/react-toggle": "^1.1.3",
|
"@radix-ui/react-toggle": "1.1.18",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.3",
|
"@radix-ui/react-toggle-group": "1.1.19",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "1.2.16",
|
||||||
"@replit/vite-plugin-cartographer": "catalog:",
|
"@replit/vite-plugin-cartographer": "catalog:",
|
||||||
"@replit/vite-plugin-dev-banner": "catalog:",
|
"@replit/vite-plugin-dev-banner": "catalog:",
|
||||||
"@replit/vite-plugin-runtime-error-modal": "catalog:",
|
"@replit/vite-plugin-runtime-error-modal": "catalog:",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "0.5.20",
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
"@tanstack/react-query": "catalog:",
|
"@tanstack/react-query": "catalog:",
|
||||||
"@tanstack/react-virtual": "catalog:",
|
"@tanstack/react-virtual": "catalog:",
|
||||||
@@ -52,29 +52,32 @@
|
|||||||
"@workspace/api-client-react": "workspace:*",
|
"@workspace/api-client-react": "workspace:*",
|
||||||
"class-variance-authority": "catalog:",
|
"class-variance-authority": "catalog:",
|
||||||
"clsx": "catalog:",
|
"clsx": "catalog:",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "1.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "4.4.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"dompurify": "catalog:",
|
||||||
|
"embla-carousel-react": "8.6.0",
|
||||||
"framer-motion": "catalog:",
|
"framer-motion": "catalog:",
|
||||||
"i18next": "^26.3.6",
|
"i18next": "26.3.6",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"lucide-react": "catalog:",
|
"lucide-react": "catalog:",
|
||||||
"next-themes": "^0.4.6",
|
"marked": "catalog:",
|
||||||
|
"next-themes": "0.4.6",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "10.0.1",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:",
|
||||||
"react-hook-form": "^7.55.0",
|
"react-hook-form": "7.84.0",
|
||||||
"react-i18next": "^17.0.11",
|
"react-i18next": "17.0.11",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "5.7.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-is": "19.2.8",
|
||||||
"recharts": "^2.15.2",
|
"react-resizable-panels": "4.12.2",
|
||||||
"sonner": "^2.0.7",
|
"recharts": "3.10.1",
|
||||||
|
"sonner": "2.0.7",
|
||||||
"tailwind-merge": "catalog:",
|
"tailwind-merge": "catalog:",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "1.4.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "1.1.2",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"wouter": "^3.3.5",
|
"wouter": "catalog:",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Switch, Route, Router as WouterRouter } from "wouter";
|
import { Switch, Route, Router as WouterRouter } from "wouter";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
|
import { loadCsrfToken } from "@/lib/csrf";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ import Trash from "@/pages/trash";
|
|||||||
import Compare from "@/pages/compare";
|
import Compare from "@/pages/compare";
|
||||||
import Watchlist from "@/pages/watchlist";
|
import Watchlist from "@/pages/watchlist";
|
||||||
import Login from "@/pages/login";
|
import Login from "@/pages/login";
|
||||||
|
import Docs from "@/pages/docs";
|
||||||
import NotFound from "@/pages/not-found";
|
import NotFound from "@/pages/not-found";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@@ -44,12 +47,17 @@ function Router() {
|
|||||||
<Route path="/admin" component={Admin} />
|
<Route path="/admin" component={Admin} />
|
||||||
<Route path="/admin/redundancy" component={Redundancy} />
|
<Route path="/admin/redundancy" component={Redundancy} />
|
||||||
<Route path="/trash" component={Trash} />
|
<Route path="/trash" component={Trash} />
|
||||||
|
<Route path="/docs" component={Docs} />
|
||||||
|
<Route path="/docs/:version" component={Docs} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
useEffect(() => {
|
||||||
|
void loadCsrfToken();
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ function buildCrumbs(location: string, t: TFunction): Crumb[] {
|
|||||||
crumbs.push({ label: t("compare.title") });
|
crumbs.push({ label: t("compare.title") });
|
||||||
} else if (location.startsWith("/analytics")) {
|
} else if (location.startsWith("/analytics")) {
|
||||||
crumbs.push({ label: t("nav.analytics") });
|
crumbs.push({ label: t("nav.analytics") });
|
||||||
|
} else if (location.startsWith("/docs")) {
|
||||||
|
crumbs.push({ href: "/docs", label: t("nav.docs") });
|
||||||
|
const match = location.match(/^\/docs\/(.+)$/);
|
||||||
|
if (match) crumbs.push({ label: match[1] });
|
||||||
} else if (location.startsWith("/login")) {
|
} else if (location.startsWith("/login")) {
|
||||||
crumbs.push({ label: t("auth.signIn") });
|
crumbs.push({ label: t("auth.signIn") });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Link, useLocation } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { LayoutDashboard, Wrench, BarChart3, LogIn, LogOut, ShieldCheck, AlertTriangle, Search } from "lucide-react";
|
import { LayoutDashboard, Wrench, BarChart3, FileText, LogIn, LogOut, ShieldCheck, AlertTriangle, Search } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
|
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
|
||||||
@@ -39,6 +39,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
{ href: "/", label: t("nav.home"), icon: LayoutDashboard },
|
{ href: "/", label: t("nav.home"), icon: LayoutDashboard },
|
||||||
{ href: "/tools", label: t("nav.browseTools"), icon: Wrench },
|
{ href: "/tools", label: t("nav.browseTools"), icon: Wrench },
|
||||||
{ href: "/analytics", label: t("nav.analytics"), icon: BarChart3 },
|
{ href: "/analytics", label: t("nav.analytics"), icon: BarChart3 },
|
||||||
|
{ href: "/docs", label: t("nav.docs"), icon: FileText },
|
||||||
];
|
];
|
||||||
|
|
||||||
const adminLinks = [
|
const adminLinks = [
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function PasswordInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: Omit<React.ComponentProps<"input">, "type">) {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
return (
|
||||||
|
<div className={cn("relative", className)}>
|
||||||
|
<Input type={visible ? "text" : "password"} className="pr-9" {...props} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setVisible((v) => !v)}
|
||||||
|
className="absolute right-0 top-0 flex h-9 w-9 items-center justify-center text-muted-foreground hover:text-foreground"
|
||||||
|
aria-label={visible ? "Hide password" : "Show password"}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{visible ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -84,7 +84,7 @@ function Calendar({
|
|||||||
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
|
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
|
||||||
defaultClassNames.caption_label
|
defaultClassNames.caption_label
|
||||||
),
|
),
|
||||||
table: "w-full border-collapse",
|
month_grid: "w-full border-collapse",
|
||||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||||
weekday: cn(
|
weekday: cn(
|
||||||
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
|
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const ChartTooltip = RechartsPrimitive.Tooltip
|
|||||||
|
|
||||||
const ChartTooltipContent = React.forwardRef<
|
const ChartTooltipContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
RechartsPrimitive.TooltipContentProps &
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
hideLabel?: boolean
|
hideLabel?: boolean
|
||||||
hideIndicator?: boolean
|
hideIndicator?: boolean
|
||||||
@@ -192,7 +192,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.dataKey}
|
key={typeof item.dataKey === "string" || typeof item.dataKey === "number" ? item.dataKey : index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||||
indicator === "dot" && "items-center"
|
indicator === "dot" && "items-center"
|
||||||
@@ -261,7 +261,7 @@ const ChartLegend = RechartsPrimitive.Legend
|
|||||||
const ChartLegendContent = React.forwardRef<
|
const ChartLegendContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> &
|
React.ComponentProps<"div"> &
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
Pick<RechartsPrimitive.DefaultLegendContentProps, "payload" | "verticalAlign"> & {
|
||||||
hideIcon?: boolean
|
hideIcon?: boolean
|
||||||
nameKey?: string
|
nameKey?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import { cn } from "@/lib/utils"
|
|||||||
const ResizablePanelGroup = ({
|
const ResizablePanelGroup = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
}: React.ComponentProps<typeof ResizablePrimitive.Group>) => (
|
||||||
<ResizablePrimitive.PanelGroup
|
<ResizablePrimitive.Group
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
"flex h-full w-full data-[group-orientation=vertical]:flex-col",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -24,12 +24,12 @@ const ResizableHandle = ({
|
|||||||
withHandle,
|
withHandle,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
}: React.ComponentProps<typeof ResizablePrimitive.Separator> & {
|
||||||
withHandle?: boolean
|
withHandle?: boolean
|
||||||
}) => (
|
}) => (
|
||||||
<ResizablePrimitive.PanelResizeHandle
|
<ResizablePrimitive.Separator
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[group-orientation=vertical]:h-px data-[group-orientation=vertical]:w-full data-[group-orientation=vertical]:after:left-0 data-[group-orientation=vertical]:after:h-1 data-[group-orientation=vertical]:after:w-full data-[group-orientation=vertical]:after:-translate-y-1/2 data-[group-orientation=vertical]:after:translate-x-0 [&[data-group-orientation=vertical]>div]:rotate-90",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -39,7 +39,7 @@ const ResizableHandle = ({
|
|||||||
<GripVertical className="h-2.5 w-2.5" />
|
<GripVertical className="h-2.5 w-2.5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ResizablePrimitive.PanelResizeHandle>
|
</ResizablePrimitive.Separator>
|
||||||
)
|
)
|
||||||
|
|
||||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { PasswordInput } from "@/components/password-input";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useChangeMyPassword, useGetPasswordRedirect, getGetPasswordRedirectQueryKey } from "@workspace/api-client-react";
|
import { useChangeMyPassword, useGetPasswordRedirect, getGetPasswordRedirectQueryKey } from "@workspace/api-client-react";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
@@ -182,8 +182,7 @@ export function UserMenu() {
|
|||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{t("auth.currentPassword")}</Label>
|
<Label>{t("auth.currentPassword")}</Label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
|
||||||
value={currentPassword}
|
value={currentPassword}
|
||||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
data-testid="input-current-password"
|
data-testid="input-current-password"
|
||||||
@@ -191,18 +190,16 @@ export function UserMenu() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{t("auth.newPassword")}</Label>
|
<Label>{t("auth.newPassword")}</Label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
placeholder="min. 8 characters"
|
placeholder="min. 6 characters"
|
||||||
data-testid="input-new-password"
|
data-testid="input-new-password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{t("auth.confirmPassword")}</Label>
|
<Label>{t("auth.confirmPassword")}</Label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
data-testid="input-confirm-password"
|
data-testid="input-confirm-password"
|
||||||
@@ -219,7 +216,7 @@ export function UserMenu() {
|
|||||||
toast({ title: t("auth.pwMismatch"), variant: "destructive" });
|
toast({ title: t("auth.pwMismatch"), variant: "destructive" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newPassword.length < 8) {
|
if (newPassword.length < 6) {
|
||||||
toast({ title: t("auth.pwTooShort"), variant: "destructive" });
|
toast({ title: t("auth.pwTooShort"), variant: "destructive" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"trash": "Papierkorb",
|
"trash": "Papierkorb",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"redundancy": "Redundanz",
|
"redundancy": "Redundanz",
|
||||||
|
"docs": "Doku",
|
||||||
"search": "Tools suchen…"
|
"search": "Tools suchen…"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -127,7 +128,12 @@
|
|||||||
"relatedTools": "Ähnliche Tools",
|
"relatedTools": "Ähnliche Tools",
|
||||||
"costs": "Kosten",
|
"costs": "Kosten",
|
||||||
"addCost": "Kosten hinzufügen",
|
"addCost": "Kosten hinzufügen",
|
||||||
"recentRatings": "Letzte Bewertungen"
|
"recentRatings": "Letzte Bewertungen",
|
||||||
|
"deleteConfirmTitle": "Dieses Tool löschen?",
|
||||||
|
"deleteToTrash": "Dies verschiebt {{name}} in den Papierkorb. Es kann später wiederhergestellt werden.",
|
||||||
|
"deletePermanent": "Dies entfernt {{name}} dauerhaft inklusive aller Bewertungen. Das kann nicht rückgängig gemacht werden.",
|
||||||
|
"deleting": "Löschen…",
|
||||||
|
"deleteAction": "Löschen"
|
||||||
},
|
},
|
||||||
"compare": {
|
"compare": {
|
||||||
"title": "Tools vergleichen",
|
"title": "Tools vergleichen",
|
||||||
@@ -176,6 +182,12 @@
|
|||||||
"text": "Diese Seite existiert nicht.",
|
"text": "Diese Seite existiert nicht.",
|
||||||
"backHome": "Zurück zur Startseite"
|
"backHome": "Zurück zur Startseite"
|
||||||
},
|
},
|
||||||
|
"docs": {
|
||||||
|
"title": "Versionsdokumentation",
|
||||||
|
"subtitle": "Version-gebundene Dokumentation je Release — was ist neu, was hat sich geändert und was beim Upgrade zu beachten ist.",
|
||||||
|
"backToIndex": "Alle Releases",
|
||||||
|
"noDocs": "Noch keine Release-Dokumentation verfügbar."
|
||||||
|
},
|
||||||
"command": {
|
"command": {
|
||||||
"navigate": "Navigation",
|
"navigate": "Navigation",
|
||||||
"recent": "Zuletzt besucht",
|
"recent": "Zuletzt besucht",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"trash": "Trash",
|
"trash": "Trash",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"redundancy": "Redundancy",
|
"redundancy": "Redundancy",
|
||||||
|
"docs": "Docs",
|
||||||
"search": "Search tools…"
|
"search": "Search tools…"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -127,7 +128,12 @@
|
|||||||
"relatedTools": "Related Tools",
|
"relatedTools": "Related Tools",
|
||||||
"costs": "Costs",
|
"costs": "Costs",
|
||||||
"addCost": "Add Cost",
|
"addCost": "Add Cost",
|
||||||
"recentRatings": "Recent Ratings"
|
"recentRatings": "Recent Ratings",
|
||||||
|
"deleteConfirmTitle": "Delete this tool?",
|
||||||
|
"deleteToTrash": "This will move {{name}} to the trash. It can be restored later.",
|
||||||
|
"deletePermanent": "This will permanently remove {{name}} and all its ratings. This cannot be undone.",
|
||||||
|
"deleting": "Deleting…",
|
||||||
|
"deleteAction": "Delete"
|
||||||
},
|
},
|
||||||
"compare": {
|
"compare": {
|
||||||
"title": "Compare Tools",
|
"title": "Compare Tools",
|
||||||
@@ -176,6 +182,12 @@
|
|||||||
"text": "This page doesn't exist.",
|
"text": "This page doesn't exist.",
|
||||||
"backHome": "Back to Home"
|
"backHome": "Back to Home"
|
||||||
},
|
},
|
||||||
|
"docs": {
|
||||||
|
"title": "Release Documentation",
|
||||||
|
"subtitle": "Version-bound documentation for each release — what's new, what changed, and what to know when upgrading.",
|
||||||
|
"backToIndex": "All releases",
|
||||||
|
"noDocs": "No release documentation available yet."
|
||||||
|
},
|
||||||
"command": {
|
"command": {
|
||||||
"navigate": "Navigate",
|
"navigate": "Navigate",
|
||||||
"recent": "Recent",
|
"recent": "Recent",
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { setCsrfTokenGetter } from "@workspace/api-client-react";
|
||||||
|
|
||||||
|
let token: string | null = null;
|
||||||
|
|
||||||
|
setCsrfTokenGetter(() => token);
|
||||||
|
|
||||||
|
export function getCsrfToken(): string | null {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadCsrfToken(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/auth/csrf`, {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
token = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = (await res.json()) as { token?: string };
|
||||||
|
token = data.token ?? null;
|
||||||
|
return token;
|
||||||
|
} catch {
|
||||||
|
token = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import { useAuth } from "@/hooks/use-auth";
|
|||||||
import { Layout } from "@/components/layout";
|
import { Layout } from "@/components/layout";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { PasswordInput } from "@/components/password-input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
@@ -115,8 +116,8 @@ export default function Admin() {
|
|||||||
|
|
||||||
const handleSetUserPassword = () => {
|
const handleSetUserPassword = () => {
|
||||||
if (!editUser || !editPassword) return;
|
if (!editUser || !editPassword) return;
|
||||||
if (editPassword.length < 8) {
|
if (editPassword.length < 6) {
|
||||||
toast({ title: "Password too short", description: "Minimum 8 characters.", variant: "destructive" });
|
toast({ title: "Password too short", description: "Minimum 6 characters.", variant: "destructive" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setUserPassword.mutate(
|
setUserPassword.mutate(
|
||||||
@@ -367,7 +368,7 @@ export default function Admin() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<Input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} placeholder="min. 6 characters" />
|
<PasswordInput value={newPassword} onChange={(e) => setNewPassword(e.target.value)} placeholder="min. 6 characters" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Email (Optional)</Label>
|
<Label>Email (Optional)</Label>
|
||||||
@@ -448,11 +449,10 @@ export default function Admin() {
|
|||||||
{editUser?.authProvider !== "oidc" ? (
|
{editUser?.authProvider !== "oidc" ? (
|
||||||
<div className="space-y-2 border-t pt-4">
|
<div className="space-y-2 border-t pt-4">
|
||||||
<Label>Set Password</Label>
|
<Label>Set Password</Label>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
|
||||||
value={editPassword}
|
value={editPassword}
|
||||||
onChange={(e) => setEditPassword(e.target.value)}
|
onChange={(e) => setEditPassword(e.target.value)}
|
||||||
placeholder="min. 8 characters"
|
placeholder="min. 6 characters"
|
||||||
data-testid="input-set-password"
|
data-testid="input-set-password"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">Resets the user's password immediately.</p>
|
<p className="text-xs text-muted-foreground">Resets the user's password immediately.</p>
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Link, useLocation, useParams } from "wouter";
|
||||||
|
import { marked } from "marked";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Layout } from "@/components/layout";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { FileText, CalendarDays, Tag, ArrowLeft, ExternalLink } from "lucide-react";
|
||||||
|
|
||||||
|
type ReleaseDoc = {
|
||||||
|
version: string;
|
||||||
|
file: string;
|
||||||
|
title: string;
|
||||||
|
date: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DOCS_BASE = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/docs`;
|
||||||
|
|
||||||
|
function fetchJson<T>(url: string): Promise<T> {
|
||||||
|
return fetch(url).then((res) => {
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
return res.json() as Promise<T>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function useReleases() {
|
||||||
|
const [releases, setReleases] = useState<ReleaseDoc[] | null>(null);
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
fetchJson<ReleaseDoc[]>(`${DOCS_BASE}/index.json`)
|
||||||
|
.then((data) => {
|
||||||
|
if (!cancelled) setReleases(data);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setError(true);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { releases, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
function useReleaseMarkdown(file: string) {
|
||||||
|
const [html, setHtml] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
setHtml(null);
|
||||||
|
setError(false);
|
||||||
|
fetch(`${DOCS_BASE}/${encodeURIComponent(file)}`)
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
return res.text();
|
||||||
|
})
|
||||||
|
.then(async (md) => {
|
||||||
|
const rendered = await marked.parse(md, { async: false, gfm: true });
|
||||||
|
if (!cancelled) setHtml(DOMPurify.sanitize(rendered));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setError(true);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [file]);
|
||||||
|
|
||||||
|
return { html, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
function DocsIndex() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { releases, error } = useReleases();
|
||||||
|
const [, setLocation] = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="space-y-6 pb-10">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight mb-2">{t("docs.title")}</h1>
|
||||||
|
<p className="text-muted-foreground">{t("docs.subtitle")}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-muted-foreground">{t("docs.noDocs")}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!releases && !error && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-20 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{releases && releases.length === 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground">{t("docs.noDocs")}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{releases && releases.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{releases.map((release) => (
|
||||||
|
<Card key={release.version} className="hover:bg-accent/50 transition-colors">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLocation(`/docs/${release.version}`)}
|
||||||
|
className="flex w-full items-center gap-4 p-4 text-left"
|
||||||
|
>
|
||||||
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
||||||
|
<FileText className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-semibold">{release.title}</span>
|
||||||
|
<Badge variant="secondary">{release.version}</Badge>
|
||||||
|
</div>
|
||||||
|
{release.date && (
|
||||||
|
<p className="mt-0.5 flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||||
|
<CalendarDays className="h-3.5 w-3.5" />
|
||||||
|
{new Date(`${release.date}T00:00:00`).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ExternalLink className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DocsDetail() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const params = useParams<{ version: string }>();
|
||||||
|
const version = params.version;
|
||||||
|
const { releases, error: indexError } = useReleases();
|
||||||
|
const release = releases?.find((r) => r.version === version);
|
||||||
|
const { html, error: mdError } = useReleaseMarkdown(release?.file ?? `${version}.md`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="space-y-6 pb-10">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<Button variant="outline" size="sm" asChild>
|
||||||
|
<Link href="/docs">
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||||
|
{t("docs.backToIndex")}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
{release && (
|
||||||
|
<a
|
||||||
|
href={`https://git.kubebase.de/admin/tool-evaluator/tags/${release.version}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Tag className="h-4 w-4" /> {release.version}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mdError || (release && !html) ? (
|
||||||
|
<p className="text-sm text-muted-foreground">{t("docs.noDocs")}</p>
|
||||||
|
) : !html ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Skeleton className="h-8 w-64" />
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
<Skeleton className="h-4 w-3/4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="docs-prose prose dark:prose-invert max-w-none"
|
||||||
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Docs() {
|
||||||
|
const [location] = useLocation();
|
||||||
|
const match = location.match(/^\/docs\/(.+)$/);
|
||||||
|
return match ? <DocsDetail /> : <DocsIndex />;
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { PasswordInput } from "@/components/password-input";
|
||||||
|
import { loadCsrfToken } from "@/lib/csrf";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { Wrench, AlertCircle } from "lucide-react";
|
import { Wrench, AlertCircle } from "lucide-react";
|
||||||
@@ -32,6 +34,7 @@ export default function Login() {
|
|||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries();
|
queryClient.invalidateQueries();
|
||||||
|
void loadCsrfToken();
|
||||||
setLocation(returnTo);
|
setLocation(returnTo);
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
@@ -76,9 +79,8 @@ export default function Login() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="password">{t("auth.password")}</Label>
|
<Label htmlFor="password">{t("auth.password")}</Label>
|
||||||
<Input
|
<PasswordInput
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -914,23 +914,23 @@ export default function ToolDetail() {
|
|||||||
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Delete this tool?</AlertDialogTitle>
|
<AlertDialogTitle>{t("detail.deleteConfirmTitle")}</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{hasTrash ? (
|
{hasTrash ? (
|
||||||
<>This will move <span className="font-medium">{tool?.name}</span> to the trash. It can be restored later.</>
|
<>{t("detail.deleteToTrash", { name: tool?.name })}</>
|
||||||
) : (
|
) : (
|
||||||
<>This will permanently remove <span className="font-medium">{tool?.name}</span> and all its ratings. This cannot be undone.</>
|
<>{t("detail.deletePermanent", { name: tool?.name })}</>
|
||||||
)}
|
)}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={deleteTool.isPending}
|
disabled={deleteTool.isPending}
|
||||||
>
|
>
|
||||||
{deleteTool.isPending ? "Deleting…" : "Delete"}
|
{deleteTool.isPending ? t("detail.deleting") : t("detail.deleteAction")}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Release-Dokumentation
|
||||||
|
|
||||||
|
Jeder Release hat eine version-gebundene Dokumentation unter
|
||||||
|
`docs/releases/`. Die Dokumentation wird öffentlich in der App unter
|
||||||
|
`/docs` (Index) und `/docs/<version>` (Detail) angezeigt.
|
||||||
|
|
||||||
|
## Struktur
|
||||||
|
|
||||||
|
- `docs/releases/TEMPLATE.md` — Vorlage für neue Releases
|
||||||
|
- `docs/releases/vX.Y.Z.md` — Dokumentation pro Release (eine Datei je Version)
|
||||||
|
|
||||||
|
## Inhalt
|
||||||
|
|
||||||
|
Pro Release wird abgedeckt (kombiniert):
|
||||||
|
|
||||||
|
- **Changelog:** Neue Features, Fixes & Verbesserungen
|
||||||
|
- **API-Änderungen:** Neue/geänderte/entfernte Endpunkte (Delta zur Vorversion)
|
||||||
|
- **Betrieb / Upgrade:** Env-Vars, DB-Migrationen, Breaking Changes
|
||||||
|
|
||||||
|
## Workflow beim Release
|
||||||
|
|
||||||
|
1. **Version taggen** wie bisher (`git tag vX.Y.Z`, CI baut und deployed).
|
||||||
|
2. **`docs/releases/vX.Y.Z.md` anlegen** — Vorlage aus
|
||||||
|
`TEMPLATE.md` kopieren. Entwurf aus der Git-Historie seit dem letzten Tag
|
||||||
|
ableiten:
|
||||||
|
```sh
|
||||||
|
git log --oneline vX.Y.Z-1..vX.Y.Z
|
||||||
|
```
|
||||||
|
(Funktions-/Fix-Commits in die passenden Abschnitte übernehmen, API-Delta
|
||||||
|
anhand `lib/api-spec/openapi.yaml` prüfen.)
|
||||||
|
3. **Committen & pushen.** Der Sync-Schritt (`scripts/sync-release-docs.mjs`)
|
||||||
|
kopiert die Markdown-Dateien beim Frontend-Build automatisch nach
|
||||||
|
`artifacts/toolrate/public/docs/` und generiert `index.json`. Dadurch sind
|
||||||
|
die Releases im Deployment als `/docs/...` verfügbar.
|
||||||
|
|
||||||
|
> Hinweis: `index.json` und die kopierten Dateien unter
|
||||||
|
> `artifacts/toolrate/public/docs/` sind Build-Artefakte und werden bei jedem
|
||||||
|
> Build neu generiert — nicht von Hand bearbeiten.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Dependency Policy
|
||||||
|
|
||||||
|
How this workspace keeps npm dependencies current, safe and reproducible.
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
1. **Exact pins** – Every direct dependency in `package.json` is pinned to an
|
||||||
|
exact version (`1.2.3`, never `^1.2.3`). `save-exact=true` is set in
|
||||||
|
`.npmrc` so `pnpm add` follows this rule automatically.
|
||||||
|
2. **Committed lockfile** – `pnpm-lock.yaml` is committed. CI and the Docker
|
||||||
|
build install with `--frozen-lockfile`, so builds are reproducible.
|
||||||
|
3. **Security gates** – CI runs `pnpm audit --prod` (fails on any finding) and
|
||||||
|
`pnpm audit --audit-level high` (fails on high/critical). A non-zero exit
|
||||||
|
blocks the release pipeline.
|
||||||
|
4. **Supply-chain protection** – `minimumReleaseAge: 1440` (1 day) in
|
||||||
|
`pnpm-workspace.yaml` blocks freshly published versions. Do not lower or
|
||||||
|
disable it; only allowlist trusted publishers via
|
||||||
|
`minimumReleaseAgeExclude` for urgent security fixes.
|
||||||
|
5. **Pinned critical packages** – `react`, `react-dom` and `esbuild` are
|
||||||
|
intentionally excluded from automated updates (see `renovate.json`). Bump
|
||||||
|
them deliberately, one release at a time, with a test pass.
|
||||||
|
|
||||||
|
## Update cadence
|
||||||
|
|
||||||
|
| Frequency | Scope | Who |
|
||||||
|
|-----------|-------|-----|
|
||||||
|
| Weekly | Patch + minor (grouped by Renovate) | Renovate PR, human merge after green CI |
|
||||||
|
| Monthly | One major version at a time | Human, own commit + release tag |
|
||||||
|
| As needed | Security advisories | Immediate fix + patch release |
|
||||||
|
| Yearly | Infrastructure review (Node LTS, Postgres major, k3s) | Human |
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Patch/minor**: merge freely once CI (typecheck + build + audit) is green.
|
||||||
|
- **Major**: never bundle multiple majors into one release. One major per
|
||||||
|
commit so regressions can be bisected to the responsible change.
|
||||||
|
- **"Safe intermediate"**: if a package has a newer major that is not yet
|
||||||
|
absorbed, stay on the latest patch/minor of the *current* major line. The
|
||||||
|
exact-pin guarantees we never float into a new major by accident.
|
||||||
|
- **Node/Infra**: Node base image and pnpm version in the `Dockerfile` are
|
||||||
|
pinned exactly. Update them together with a build + live smoke test.
|
||||||
|
- **Postgres**: image tag is managed in the `admin/apps` repository
|
||||||
|
(`apps/system/toolrate`). Major upgrades run through a backup/restore flow.
|
||||||
|
|
||||||
|
## Process for applying updates
|
||||||
|
|
||||||
|
1. `pnpm install` to refresh the lockfile.
|
||||||
|
2. Regenerate clients if the API changed: `pnpm --filter @workspace/api-spec run codegen`.
|
||||||
|
3. Run `pnpm run typecheck`, `pnpm run build` (with `PORT=8080 BASE_PATH=/`),
|
||||||
|
and `pnpm audit`.
|
||||||
|
4. Fix any code that the new majors require (the common ones are
|
||||||
|
`openid-client`, `recharts`, `react-day-picker`, `date-fns`,
|
||||||
|
`@hookform/resolvers`, `react-resizable-panels`).
|
||||||
|
5. Commit, tag `vX.Y.Z`, push. CI builds, audits and deploys to k3s.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# vX.Y.Z — Release Notes
|
||||||
|
|
||||||
|
> Template für neue Release-Dokumentationen. Eine Kopie pro Release unter
|
||||||
|
> `docs/releases/vX.Y.Z.md` anlegen, Platzhalter ersetzen, Abschnitte die
|
||||||
|
> nicht zutreffen entfernen. Die Seite wird unter `/docs/vX.Y.Z` in der App
|
||||||
|
> angezeigt.
|
||||||
|
|
||||||
|
**Datum:** YYYY-MM-DD · **Tag:** [`vX.Y.Z`](https://git.kubebase.de/admin/tool-evaluator/tags/vX.Y.Z)
|
||||||
|
|
||||||
|
## Neue Features
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## Fixes & Verbesserungen
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## API-Änderungen
|
||||||
|
|
||||||
|
- ... (neue/geänderte/entfernte Endpunkte — siehe `lib/api-spec/openapi.yaml`)
|
||||||
|
|
||||||
|
## Betrieb / Upgrade
|
||||||
|
|
||||||
|
- **Env-Vars:** ... (neu/geändert/entfernt)
|
||||||
|
- **Migration:** ... (Datenbank-/Schema-Änderungen, Schritte für den Betreiber)
|
||||||
|
- **Breaking Changes:** ... (nur wenn vorhanden)
|
||||||
|
|
||||||
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Commit: [`<short-sha>`](https://git.kubebase.de/admin/tool-evaluator/commit/<short-sha>)
|
||||||
|
- Tag: [`vX.Y.Z`](https://git.kubebase.de/admin/tool-evaluator/tags/vX.Y.Z)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# v0.6.0 — Release Notes
|
||||||
|
|
||||||
|
**Datum:** 2026-08-03 · **Tag:** [`v0.6.0`](https://git.kubebase.de/admin/tool-evaluator/tags/v0.6.0)
|
||||||
|
|
||||||
|
## Neue Features
|
||||||
|
|
||||||
|
- Vollständige Modernisierung aller Abhängigkeiten auf die aktuellen Hauptversionen
|
||||||
|
(TypeScript 7, Vite 8, React 19.2, openid-client 6, zod 4, recharts 3, react-day-picker 10).
|
||||||
|
|
||||||
|
## Fixes & Verbesserungen
|
||||||
|
|
||||||
|
- CI-Build durch `allowBuilds`-Konfiguration für pnpm 11 repariert
|
||||||
|
(Build-Scripts für esbuild & Co. werden nicht mehr blockiert).
|
||||||
|
- Image-Tagging vereinfacht: nur noch `latest` und `v*`-Tags, keine `nightly-*`/`sha-*`-Tags.
|
||||||
|
- Alle Dependencies exakt gepinnt; automatische Updates via Renovate vorbereitet
|
||||||
|
(`renovate.json`, `docs/dependency-policy.md`).
|
||||||
|
|
||||||
|
## API-Änderungen
|
||||||
|
|
||||||
|
- Keine Breaking Changes an der API. openid-client intern auf v6 migriert
|
||||||
|
(auth-Fluss verhält sich identisch).
|
||||||
|
|
||||||
|
## Betrieb / Upgrade
|
||||||
|
|
||||||
|
- **Env-Vars:** unverändert. Node-Image auf `node:24.18.1-alpine` gepinnt.
|
||||||
|
- **Migration:** keine Datenbank-Migration erforderlich.
|
||||||
|
- **Breaking Changes:** keine.
|
||||||
|
|
||||||
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
|
- `typedoc` (indirekte orval-Abhängigkeit) zeigt eine Peer-Dependency-Warnung
|
||||||
|
(erwartet TypeScript 5.x/6.x, installiert ist 7.x) — harmlos für Build & Laufzeit.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Commit: [`2f66fff`](https://git.kubebase.de/admin/tool-evaluator/commit/2f66fff)
|
||||||
|
- Tag: [`v0.6.0`](https://git.kubebase.de/admin/tool-evaluator/tags/v0.6.0)
|
||||||
@@ -17,6 +17,7 @@ const DEFAULT_JSON_ACCEPT = "application/json, application/problem+json";
|
|||||||
|
|
||||||
let _baseUrl: string | null = null;
|
let _baseUrl: string | null = null;
|
||||||
let _authTokenGetter: AuthTokenGetter | null = null;
|
let _authTokenGetter: AuthTokenGetter | null = null;
|
||||||
|
let _csrfTokenGetter: (() => string | null) | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a base URL that is prepended to every relative request URL
|
* Set a base URL that is prepended to every relative request URL
|
||||||
@@ -44,6 +45,15 @@ export function setAuthTokenGetter(getter: AuthTokenGetter | null): void {
|
|||||||
_authTokenGetter = getter;
|
_authTokenGetter = getter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a getter that supplies a CSRF token. Before every state-changing
|
||||||
|
* fetch an `X-CSRF-Token` header is attached when the getter returns a value.
|
||||||
|
* Pass `null` to clear the getter.
|
||||||
|
*/
|
||||||
|
export function setCsrfTokenGetter(getter: (() => string | null) | null): void {
|
||||||
|
_csrfTokenGetter = getter;
|
||||||
|
}
|
||||||
|
|
||||||
function isRequest(input: RequestInfo | URL): input is Request {
|
function isRequest(input: RequestInfo | URL): input is Request {
|
||||||
return typeof Request !== "undefined" && input instanceof Request;
|
return typeof Request !== "undefined" && input instanceof Request;
|
||||||
}
|
}
|
||||||
@@ -349,6 +359,14 @@ export async function customFetch<T = unknown>(
|
|||||||
headers.set("accept", DEFAULT_JSON_ACCEPT);
|
headers.set("accept", DEFAULT_JSON_ACCEPT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attach CSRF token for state-changing requests, unless one is already set.
|
||||||
|
if (_csrfTokenGetter && !headers.has("x-csrf-token")) {
|
||||||
|
const csrf = _csrfTokenGetter();
|
||||||
|
if (csrf) {
|
||||||
|
headers.set("x-csrf-token", csrf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Attach bearer token when an auth getter is configured and no
|
// Attach bearer token when an auth getter is configured and no
|
||||||
// Authorization header has been explicitly provided.
|
// Authorization header has been explicitly provided.
|
||||||
if (_authTokenGetter && !headers.has("authorization")) {
|
if (_authTokenGetter && !headers.has("authorization")) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
@@ -30,6 +30,10 @@ export interface AuthMode {
|
|||||||
mode: AuthModeMode;
|
mode: AuthModeMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CsrfToken {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LocalLoginInput {
|
export interface LocalLoginInput {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -123,12 +127,12 @@ export interface UserRoleUpdate {
|
|||||||
export interface ChangePasswordInput {
|
export interface ChangePasswordInput {
|
||||||
/** @minLength 1 */
|
/** @minLength 1 */
|
||||||
currentPassword: string;
|
currentPassword: string;
|
||||||
/** @minLength 8 */
|
/** @minLength 6 */
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetPasswordInput {
|
export interface SetPasswordInput {
|
||||||
/** @minLength 8 */
|
/** @minLength 6 */
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
@@ -26,6 +26,7 @@ import type {
|
|||||||
AuthUser,
|
AuthUser,
|
||||||
CategoryStats,
|
CategoryStats,
|
||||||
ChangePasswordInput,
|
ChangePasswordInput,
|
||||||
|
CsrfToken,
|
||||||
EmptyTrash200,
|
EmptyTrash200,
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
GetRatingDistributionParams,
|
GetRatingDistributionParams,
|
||||||
@@ -69,6 +70,21 @@ type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const withQueryKey = <T extends object, K>(query: T, queryKey: K): T & { queryKey: K } => {
|
||||||
|
const result = { queryKey } as T & { queryKey: K };
|
||||||
|
for (const key of Object.keys(query)) {
|
||||||
|
// The explicit queryKey always wins, matching the previous
|
||||||
|
// `{ ...query, queryKey }` spread where it was set last.
|
||||||
|
if (key === 'queryKey') continue;
|
||||||
|
Object.defineProperty(result, key, {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
get: () => (query as Record<string, unknown>)[key],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
export const getHealthCheckUrl = () => {
|
export const getHealthCheckUrl = () => {
|
||||||
|
|
||||||
|
|
||||||
@@ -81,7 +97,7 @@ export const getHealthCheckUrl = () => {
|
|||||||
* Returns server health status
|
* Returns server health status
|
||||||
* @summary Health check
|
* @summary Health check
|
||||||
*/
|
*/
|
||||||
export const healthCheck = async ( options?: RequestInit): Promise<HealthStatus> => {
|
export const healthCheck = async ( options?: Parameters<typeof customFetch>[1]): Promise<HealthStatus> => {
|
||||||
|
|
||||||
return customFetch<HealthStatus>(getHealthCheckUrl(),
|
return customFetch<HealthStatus>(getHealthCheckUrl(),
|
||||||
{
|
{
|
||||||
@@ -138,7 +154,7 @@ export function useHealthCheck<TData = Awaited<ReturnType<typeof healthCheck>>,
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -159,7 +175,7 @@ export const getGetVersionUrl = () => {
|
|||||||
* Returns the running build version, commit SHA and build date
|
* Returns the running build version, commit SHA and build date
|
||||||
* @summary Build version information
|
* @summary Build version information
|
||||||
*/
|
*/
|
||||||
export const getVersion = async ( options?: RequestInit): Promise<VersionInfo> => {
|
export const getVersion = async ( options?: Parameters<typeof customFetch>[1]): Promise<VersionInfo> => {
|
||||||
|
|
||||||
return customFetch<VersionInfo>(getGetVersionUrl(),
|
return customFetch<VersionInfo>(getGetVersionUrl(),
|
||||||
{
|
{
|
||||||
@@ -216,7 +232,7 @@ export function useGetVersion<TData = Awaited<ReturnType<typeof getVersion>>, TE
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -231,7 +247,7 @@ export const getListToolsUrl = (params?: ListToolsParams,) => {
|
|||||||
Object.entries(params || {}).forEach(([key, value]) => {
|
Object.entries(params || {}).forEach(([key, value]) => {
|
||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
normalizedParams.append(key, value === null ? 'null' : String(value))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -243,7 +259,7 @@ export const getListToolsUrl = (params?: ListToolsParams,) => {
|
|||||||
/**
|
/**
|
||||||
* @summary List all tools
|
* @summary List all tools
|
||||||
*/
|
*/
|
||||||
export const listTools = async (params?: ListToolsParams, options?: RequestInit): Promise<ToolWithStats[]> => {
|
export const listTools = async (params?: ListToolsParams, options?: Parameters<typeof customFetch>[1]): Promise<ToolWithStats[]> => {
|
||||||
|
|
||||||
return customFetch<ToolWithStats[]>(getListToolsUrl(params),
|
return customFetch<ToolWithStats[]>(getListToolsUrl(params),
|
||||||
{
|
{
|
||||||
@@ -300,7 +316,7 @@ export function useListTools<TData = Awaited<ReturnType<typeof listTools>>, TErr
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -320,21 +336,21 @@ export const getCreateToolUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Create a new tool
|
* @summary Create a new tool
|
||||||
*/
|
*/
|
||||||
export const createTool = async (toolInput: ToolInput, options?: RequestInit): Promise<Tool> => {
|
export const createTool = async (toolInput: ToolInput, options?: Parameters<typeof customFetch>[1]): Promise<Tool> => {
|
||||||
|
|
||||||
return customFetch<Tool>(getCreateToolUrl(),
|
return customFetch<Tool>(getCreateToolUrl(),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(toolInput)
|
||||||
toolInput,)
|
|
||||||
}
|
}
|
||||||
);}
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getCreateToolMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getCreateToolMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createTool>>, TError,{data: BodyType<ToolInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createTool>>, TError,{data: BodyType<ToolInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof createTool>>, TError,{data: BodyType<ToolInput>}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof createTool>>, TError,{data: BodyType<ToolInput>}, TContext> => {
|
||||||
@@ -386,7 +402,7 @@ export const getListCompareToolsUrl = (params: ListCompareToolsParams,) => {
|
|||||||
Object.entries(params || {}).forEach(([key, value]) => {
|
Object.entries(params || {}).forEach(([key, value]) => {
|
||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
normalizedParams.append(key, value === null ? 'null' : String(value))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -398,7 +414,7 @@ export const getListCompareToolsUrl = (params: ListCompareToolsParams,) => {
|
|||||||
/**
|
/**
|
||||||
* @summary Compare tools side by side (premium)
|
* @summary Compare tools side by side (premium)
|
||||||
*/
|
*/
|
||||||
export const listCompareTools = async (params: ListCompareToolsParams, options?: RequestInit): Promise<ToolWithStats[]> => {
|
export const listCompareTools = async (params: ListCompareToolsParams, options?: Parameters<typeof customFetch>[1]): Promise<ToolWithStats[]> => {
|
||||||
|
|
||||||
return customFetch<ToolWithStats[]>(getListCompareToolsUrl(params),
|
return customFetch<ToolWithStats[]>(getListCompareToolsUrl(params),
|
||||||
{
|
{
|
||||||
@@ -455,7 +471,7 @@ export function useListCompareTools<TData = Awaited<ReturnType<typeof listCompar
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -475,7 +491,7 @@ export const getGetToolRatingHistoryUrl = (id: number,) => {
|
|||||||
/**
|
/**
|
||||||
* @summary Get a tool's rating history over time
|
* @summary Get a tool's rating history over time
|
||||||
*/
|
*/
|
||||||
export const getToolRatingHistory = async (id: number, options?: RequestInit): Promise<RatingHistoryItem[]> => {
|
export const getToolRatingHistory = async (id: number, options?: Parameters<typeof customFetch>[1]): Promise<RatingHistoryItem[]> => {
|
||||||
|
|
||||||
return customFetch<RatingHistoryItem[]>(getGetToolRatingHistoryUrl(id),
|
return customFetch<RatingHistoryItem[]>(getGetToolRatingHistoryUrl(id),
|
||||||
{
|
{
|
||||||
@@ -512,7 +528,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {};
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getToolRatingHistory>>, TError, TData> & { queryKey: QueryKey }
|
return { queryKey, queryFn, enabled: id !== null && id !== undefined, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getToolRatingHistory>>, TError, TData> & { queryKey: QueryKey }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetToolRatingHistoryQueryResult = NonNullable<Awaited<ReturnType<typeof getToolRatingHistory>>>
|
export type GetToolRatingHistoryQueryResult = NonNullable<Awaited<ReturnType<typeof getToolRatingHistory>>>
|
||||||
@@ -532,7 +548,7 @@ export function useGetToolRatingHistory<TData = Awaited<ReturnType<typeof getToo
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -552,7 +568,7 @@ export const getGetToolUrl = (id: number,) => {
|
|||||||
/**
|
/**
|
||||||
* @summary Get a tool by ID
|
* @summary Get a tool by ID
|
||||||
*/
|
*/
|
||||||
export const getTool = async (id: number, options?: RequestInit): Promise<ToolWithStats> => {
|
export const getTool = async (id: number, options?: Parameters<typeof customFetch>[1]): Promise<ToolWithStats> => {
|
||||||
|
|
||||||
return customFetch<ToolWithStats>(getGetToolUrl(id),
|
return customFetch<ToolWithStats>(getGetToolUrl(id),
|
||||||
{
|
{
|
||||||
@@ -589,7 +605,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {};
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getTool>>, TError, TData> & { queryKey: QueryKey }
|
return { queryKey, queryFn, enabled: id !== null && id !== undefined, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getTool>>, TError, TData> & { queryKey: QueryKey }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetToolQueryResult = NonNullable<Awaited<ReturnType<typeof getTool>>>
|
export type GetToolQueryResult = NonNullable<Awaited<ReturnType<typeof getTool>>>
|
||||||
@@ -609,7 +625,7 @@ export function useGetTool<TData = Awaited<ReturnType<typeof getTool>>, TError =
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -630,21 +646,21 @@ export const getUpdateToolUrl = (id: number,) => {
|
|||||||
* @summary Update a tool
|
* @summary Update a tool
|
||||||
*/
|
*/
|
||||||
export const updateTool = async (id: number,
|
export const updateTool = async (id: number,
|
||||||
toolUpdate: ToolUpdate, options?: RequestInit): Promise<Tool> => {
|
toolUpdate: ToolUpdate, options?: Parameters<typeof customFetch>[1]): Promise<Tool> => {
|
||||||
|
|
||||||
return customFetch<Tool>(getUpdateToolUrl(id),
|
return customFetch<Tool>(getUpdateToolUrl(id),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(toolUpdate)
|
||||||
toolUpdate,)
|
|
||||||
}
|
}
|
||||||
);}
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getUpdateToolMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getUpdateToolMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateTool>>, TError,{id: number;data: BodyType<ToolUpdate>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateTool>>, TError,{id: number;data: BodyType<ToolUpdate>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof updateTool>>, TError,{id: number;data: BodyType<ToolUpdate>}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof updateTool>>, TError,{id: number;data: BodyType<ToolUpdate>}, TContext> => {
|
||||||
@@ -701,7 +717,7 @@ export const getDeleteToolUrl = (id: number,) => {
|
|||||||
/**
|
/**
|
||||||
* @summary Delete a tool
|
* @summary Delete a tool
|
||||||
*/
|
*/
|
||||||
export const deleteTool = async (id: number, options?: RequestInit): Promise<void> => {
|
export const deleteTool = async (id: number, options?: Parameters<typeof customFetch>[1]): Promise<void> => {
|
||||||
|
|
||||||
return customFetch<void>(getDeleteToolUrl(id),
|
return customFetch<void>(getDeleteToolUrl(id),
|
||||||
{
|
{
|
||||||
@@ -715,6 +731,7 @@ export const deleteTool = async (id: number, options?: RequestInit): Promise<voi
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getDeleteToolMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getDeleteToolMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteTool>>, TError,{id: number}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteTool>>, TError,{id: number}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof deleteTool>>, TError,{id: number}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof deleteTool>>, TError,{id: number}, TContext> => {
|
||||||
@@ -766,7 +783,7 @@ export const getListTrashedToolsUrl = (params?: ListTrashedToolsParams,) => {
|
|||||||
Object.entries(params || {}).forEach(([key, value]) => {
|
Object.entries(params || {}).forEach(([key, value]) => {
|
||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
normalizedParams.append(key, value === null ? 'null' : String(value))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -778,7 +795,7 @@ export const getListTrashedToolsUrl = (params?: ListTrashedToolsParams,) => {
|
|||||||
/**
|
/**
|
||||||
* @summary List trashed (soft-deleted) tools
|
* @summary List trashed (soft-deleted) tools
|
||||||
*/
|
*/
|
||||||
export const listTrashedTools = async (params?: ListTrashedToolsParams, options?: RequestInit): Promise<Tool[]> => {
|
export const listTrashedTools = async (params?: ListTrashedToolsParams, options?: Parameters<typeof customFetch>[1]): Promise<Tool[]> => {
|
||||||
|
|
||||||
return customFetch<Tool[]>(getListTrashedToolsUrl(params),
|
return customFetch<Tool[]>(getListTrashedToolsUrl(params),
|
||||||
{
|
{
|
||||||
@@ -835,7 +852,7 @@ export function useListTrashedTools<TData = Awaited<ReturnType<typeof listTrashe
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -855,21 +872,21 @@ export const getTrashToolsUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Move tools to trash (admin)
|
* @summary Move tools to trash (admin)
|
||||||
*/
|
*/
|
||||||
export const trashTools = async (trashToolsInput: TrashToolsInput, options?: RequestInit): Promise<TrashTools200> => {
|
export const trashTools = async (trashToolsInput: TrashToolsInput, options?: Parameters<typeof customFetch>[1]): Promise<TrashTools200> => {
|
||||||
|
|
||||||
return customFetch<TrashTools200>(getTrashToolsUrl(),
|
return customFetch<TrashTools200>(getTrashToolsUrl(),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(trashToolsInput)
|
||||||
trashToolsInput,)
|
|
||||||
}
|
}
|
||||||
);}
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getTrashToolsMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getTrashToolsMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof trashTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof trashTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof trashTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof trashTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext> => {
|
||||||
@@ -926,21 +943,21 @@ export const getDeleteTrashedToolsUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Permanently delete trashed tools (admin)
|
* @summary Permanently delete trashed tools (admin)
|
||||||
*/
|
*/
|
||||||
export const deleteTrashedTools = async (trashToolsInput: TrashToolsInput, options?: RequestInit): Promise<void> => {
|
export const deleteTrashedTools = async (trashToolsInput: TrashToolsInput, options?: Parameters<typeof customFetch>[1]): Promise<void> => {
|
||||||
|
|
||||||
return customFetch<void>(getDeleteTrashedToolsUrl(),
|
return customFetch<void>(getDeleteTrashedToolsUrl(),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(trashToolsInput)
|
||||||
trashToolsInput,)
|
|
||||||
}
|
}
|
||||||
);}
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getDeleteTrashedToolsMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getDeleteTrashedToolsMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteTrashedTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteTrashedTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof deleteTrashedTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof deleteTrashedTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext> => {
|
||||||
@@ -997,21 +1014,21 @@ export const getRestoreToolsUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Restore trashed tools
|
* @summary Restore trashed tools
|
||||||
*/
|
*/
|
||||||
export const restoreTools = async (trashToolsInput: TrashToolsInput, options?: RequestInit): Promise<RestoreTools200> => {
|
export const restoreTools = async (trashToolsInput: TrashToolsInput, options?: Parameters<typeof customFetch>[1]): Promise<RestoreTools200> => {
|
||||||
|
|
||||||
return customFetch<RestoreTools200>(getRestoreToolsUrl(),
|
return customFetch<RestoreTools200>(getRestoreToolsUrl(),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(trashToolsInput)
|
||||||
trashToolsInput,)
|
|
||||||
}
|
}
|
||||||
);}
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getRestoreToolsMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getRestoreToolsMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof restoreTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof restoreTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof restoreTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof restoreTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext> => {
|
||||||
@@ -1068,7 +1085,7 @@ export const getEmptyTrashUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Permanently delete all trashed tools (admin)
|
* @summary Permanently delete all trashed tools (admin)
|
||||||
*/
|
*/
|
||||||
export const emptyTrash = async ( options?: RequestInit): Promise<EmptyTrash200> => {
|
export const emptyTrash = async ( options?: Parameters<typeof customFetch>[1]): Promise<EmptyTrash200> => {
|
||||||
|
|
||||||
return customFetch<EmptyTrash200>(getEmptyTrashUrl(),
|
return customFetch<EmptyTrash200>(getEmptyTrashUrl(),
|
||||||
{
|
{
|
||||||
@@ -1082,6 +1099,7 @@ export const emptyTrash = async ( options?: RequestInit): Promise<EmptyTrash200>
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getEmptyTrashMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getEmptyTrashMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof emptyTrash>>, TError,void, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof emptyTrash>>, TError,void, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof emptyTrash>>, TError,void, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof emptyTrash>>, TError,void, TContext> => {
|
||||||
@@ -1138,7 +1156,7 @@ export const getListToolRatingsUrl = (id: number,) => {
|
|||||||
/**
|
/**
|
||||||
* @summary List ratings for a tool
|
* @summary List ratings for a tool
|
||||||
*/
|
*/
|
||||||
export const listToolRatings = async (id: number, options?: RequestInit): Promise<Rating[]> => {
|
export const listToolRatings = async (id: number, options?: Parameters<typeof customFetch>[1]): Promise<Rating[]> => {
|
||||||
|
|
||||||
return customFetch<Rating[]>(getListToolRatingsUrl(id),
|
return customFetch<Rating[]>(getListToolRatingsUrl(id),
|
||||||
{
|
{
|
||||||
@@ -1175,7 +1193,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {};
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listToolRatings>>, TError, TData> & { queryKey: QueryKey }
|
return { queryKey, queryFn, enabled: id !== null && id !== undefined, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listToolRatings>>, TError, TData> & { queryKey: QueryKey }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListToolRatingsQueryResult = NonNullable<Awaited<ReturnType<typeof listToolRatings>>>
|
export type ListToolRatingsQueryResult = NonNullable<Awaited<ReturnType<typeof listToolRatings>>>
|
||||||
@@ -1195,7 +1213,7 @@ export function useListToolRatings<TData = Awaited<ReturnType<typeof listToolRat
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1216,21 +1234,21 @@ export const getCreateRatingUrl = (id: number,) => {
|
|||||||
* @summary Submit a rating for a tool
|
* @summary Submit a rating for a tool
|
||||||
*/
|
*/
|
||||||
export const createRating = async (id: number,
|
export const createRating = async (id: number,
|
||||||
ratingInput: RatingInput, options?: RequestInit): Promise<Rating> => {
|
ratingInput: RatingInput, options?: Parameters<typeof customFetch>[1]): Promise<Rating> => {
|
||||||
|
|
||||||
return customFetch<Rating>(getCreateRatingUrl(id),
|
return customFetch<Rating>(getCreateRatingUrl(id),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(ratingInput)
|
||||||
ratingInput,)
|
|
||||||
}
|
}
|
||||||
);}
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getCreateRatingMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getCreateRatingMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createRating>>, TError,{id: number;data: BodyType<RatingInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createRating>>, TError,{id: number;data: BodyType<RatingInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof createRating>>, TError,{id: number;data: BodyType<RatingInput>}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof createRating>>, TError,{id: number;data: BodyType<RatingInput>}, TContext> => {
|
||||||
@@ -1287,7 +1305,7 @@ export const getGetAnalyticsSummaryUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Overall platform statistics
|
* @summary Overall platform statistics
|
||||||
*/
|
*/
|
||||||
export const getAnalyticsSummary = async ( options?: RequestInit): Promise<AnalyticsSummary> => {
|
export const getAnalyticsSummary = async ( options?: Parameters<typeof customFetch>[1]): Promise<AnalyticsSummary> => {
|
||||||
|
|
||||||
return customFetch<AnalyticsSummary>(getGetAnalyticsSummaryUrl(),
|
return customFetch<AnalyticsSummary>(getGetAnalyticsSummaryUrl(),
|
||||||
{
|
{
|
||||||
@@ -1344,7 +1362,7 @@ export function useGetAnalyticsSummary<TData = Awaited<ReturnType<typeof getAnal
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1359,7 +1377,7 @@ export const getGetTopToolsUrl = (params?: GetTopToolsParams,) => {
|
|||||||
Object.entries(params || {}).forEach(([key, value]) => {
|
Object.entries(params || {}).forEach(([key, value]) => {
|
||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
normalizedParams.append(key, value === null ? 'null' : String(value))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1371,7 +1389,7 @@ export const getGetTopToolsUrl = (params?: GetTopToolsParams,) => {
|
|||||||
/**
|
/**
|
||||||
* @summary Top-rated tools
|
* @summary Top-rated tools
|
||||||
*/
|
*/
|
||||||
export const getTopTools = async (params?: GetTopToolsParams, options?: RequestInit): Promise<TopToolEntry[]> => {
|
export const getTopTools = async (params?: GetTopToolsParams, options?: Parameters<typeof customFetch>[1]): Promise<TopToolEntry[]> => {
|
||||||
|
|
||||||
return customFetch<TopToolEntry[]>(getGetTopToolsUrl(params),
|
return customFetch<TopToolEntry[]>(getGetTopToolsUrl(params),
|
||||||
{
|
{
|
||||||
@@ -1428,7 +1446,7 @@ export function useGetTopTools<TData = Awaited<ReturnType<typeof getTopTools>>,
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1448,7 +1466,7 @@ export const getGetAnalyticsByCategoryUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Rating statistics grouped by category
|
* @summary Rating statistics grouped by category
|
||||||
*/
|
*/
|
||||||
export const getAnalyticsByCategory = async ( options?: RequestInit): Promise<CategoryStats[]> => {
|
export const getAnalyticsByCategory = async ( options?: Parameters<typeof customFetch>[1]): Promise<CategoryStats[]> => {
|
||||||
|
|
||||||
return customFetch<CategoryStats[]>(getGetAnalyticsByCategoryUrl(),
|
return customFetch<CategoryStats[]>(getGetAnalyticsByCategoryUrl(),
|
||||||
{
|
{
|
||||||
@@ -1505,7 +1523,7 @@ export function useGetAnalyticsByCategory<TData = Awaited<ReturnType<typeof getA
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1520,7 +1538,7 @@ export const getGetRatingDistributionUrl = (params?: GetRatingDistributionParams
|
|||||||
Object.entries(params || {}).forEach(([key, value]) => {
|
Object.entries(params || {}).forEach(([key, value]) => {
|
||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
normalizedParams.append(key, value === null ? 'null' : String(value))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1532,7 +1550,7 @@ export const getGetRatingDistributionUrl = (params?: GetRatingDistributionParams
|
|||||||
/**
|
/**
|
||||||
* @summary Distribution of rating scores across the platform
|
* @summary Distribution of rating scores across the platform
|
||||||
*/
|
*/
|
||||||
export const getRatingDistribution = async (params?: GetRatingDistributionParams, options?: RequestInit): Promise<RatingDistribution> => {
|
export const getRatingDistribution = async (params?: GetRatingDistributionParams, options?: Parameters<typeof customFetch>[1]): Promise<RatingDistribution> => {
|
||||||
|
|
||||||
return customFetch<RatingDistribution>(getGetRatingDistributionUrl(params),
|
return customFetch<RatingDistribution>(getGetRatingDistributionUrl(params),
|
||||||
{
|
{
|
||||||
@@ -1589,7 +1607,7 @@ export function useGetRatingDistribution<TData = Awaited<ReturnType<typeof getRa
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1609,7 +1627,7 @@ export const getListCategoriesUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary List all distinct tool categories
|
* @summary List all distinct tool categories
|
||||||
*/
|
*/
|
||||||
export const listCategories = async ( options?: RequestInit): Promise<string[]> => {
|
export const listCategories = async ( options?: Parameters<typeof customFetch>[1]): Promise<string[]> => {
|
||||||
|
|
||||||
return customFetch<string[]>(getListCategoriesUrl(),
|
return customFetch<string[]>(getListCategoriesUrl(),
|
||||||
{
|
{
|
||||||
@@ -1666,7 +1684,7 @@ export function useListCategories<TData = Awaited<ReturnType<typeof listCategori
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1686,7 +1704,7 @@ export const getListAllFeaturesUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary List all distinct feature strings across all tools
|
* @summary List all distinct feature strings across all tools
|
||||||
*/
|
*/
|
||||||
export const listAllFeatures = async ( options?: RequestInit): Promise<string[]> => {
|
export const listAllFeatures = async ( options?: Parameters<typeof customFetch>[1]): Promise<string[]> => {
|
||||||
|
|
||||||
return customFetch<string[]>(getListAllFeaturesUrl(),
|
return customFetch<string[]>(getListAllFeaturesUrl(),
|
||||||
{
|
{
|
||||||
@@ -1743,7 +1761,7 @@ export function useListAllFeatures<TData = Awaited<ReturnType<typeof listAllFeat
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1763,7 +1781,7 @@ export const getListAllTagsUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary List all distinct tag strings across all tools
|
* @summary List all distinct tag strings across all tools
|
||||||
*/
|
*/
|
||||||
export const listAllTags = async ( options?: RequestInit): Promise<string[]> => {
|
export const listAllTags = async ( options?: Parameters<typeof customFetch>[1]): Promise<string[]> => {
|
||||||
|
|
||||||
return customFetch<string[]>(getListAllTagsUrl(),
|
return customFetch<string[]>(getListAllTagsUrl(),
|
||||||
{
|
{
|
||||||
@@ -1820,7 +1838,7 @@ export function useListAllTags<TData = Awaited<ReturnType<typeof listAllTags>>,
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1840,7 +1858,7 @@ export const getGetAuthModeUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Get authentication mode (oidc or local)
|
* @summary Get authentication mode (oidc or local)
|
||||||
*/
|
*/
|
||||||
export const getAuthMode = async ( options?: RequestInit): Promise<AuthMode> => {
|
export const getAuthMode = async ( options?: Parameters<typeof customFetch>[1]): Promise<AuthMode> => {
|
||||||
|
|
||||||
return customFetch<AuthMode>(getGetAuthModeUrl(),
|
return customFetch<AuthMode>(getGetAuthModeUrl(),
|
||||||
{
|
{
|
||||||
@@ -1897,7 +1915,84 @@ export function useGetAuthMode<TData = Awaited<ReturnType<typeof getAuthMode>>,
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const getGetCsrfTokenUrl = () => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return `/api/auth/csrf`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Get a CSRF token for state-changing requests
|
||||||
|
*/
|
||||||
|
export const getCsrfToken = async ( options?: Parameters<typeof customFetch>[1]): Promise<CsrfToken> => {
|
||||||
|
|
||||||
|
return customFetch<CsrfToken>(getGetCsrfTokenUrl(),
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
method: 'GET'
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const getGetCsrfTokenQueryKey = () => {
|
||||||
|
return [
|
||||||
|
`/api/auth/csrf`
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getGetCsrfTokenQueryOptions = <TData = Awaited<ReturnType<typeof getCsrfToken>>, TError = ErrorType<unknown>>( options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getCsrfToken>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||||
|
) => {
|
||||||
|
|
||||||
|
const {query: queryOptions, request: requestOptions} = options ?? {};
|
||||||
|
|
||||||
|
const queryKey = queryOptions?.queryKey ?? getGetCsrfTokenQueryKey();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof getCsrfToken>>> = ({ signal }) => getCsrfToken({ signal, ...requestOptions });
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getCsrfToken>>, TError, TData> & { queryKey: QueryKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetCsrfTokenQueryResult = NonNullable<Awaited<ReturnType<typeof getCsrfToken>>>
|
||||||
|
export type GetCsrfTokenQueryError = ErrorType<unknown>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Get a CSRF token for state-changing requests
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function useGetCsrfToken<TData = Awaited<ReturnType<typeof getCsrfToken>>, TError = ErrorType<unknown>>(
|
||||||
|
options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof getCsrfToken>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||||
|
|
||||||
|
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||||
|
|
||||||
|
const queryOptions = getGetCsrfTokenQueryOptions(options)
|
||||||
|
|
||||||
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1917,21 +2012,21 @@ export const getLocalLoginUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Local username/password login
|
* @summary Local username/password login
|
||||||
*/
|
*/
|
||||||
export const localLogin = async (localLoginInput: LocalLoginInput, options?: RequestInit): Promise<AuthUser> => {
|
export const localLogin = async (localLoginInput: LocalLoginInput, options?: Parameters<typeof customFetch>[1]): Promise<AuthUser> => {
|
||||||
|
|
||||||
return customFetch<AuthUser>(getLocalLoginUrl(),
|
return customFetch<AuthUser>(getLocalLoginUrl(),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(localLoginInput)
|
||||||
localLoginInput,)
|
|
||||||
}
|
}
|
||||||
);}
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getLocalLoginMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getLocalLoginMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof localLogin>>, TError,{data: BodyType<LocalLoginInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof localLogin>>, TError,{data: BodyType<LocalLoginInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof localLogin>>, TError,{data: BodyType<LocalLoginInput>}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof localLogin>>, TError,{data: BodyType<LocalLoginInput>}, TContext> => {
|
||||||
@@ -1988,7 +2083,7 @@ export const getGetMeUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Get current authenticated user
|
* @summary Get current authenticated user
|
||||||
*/
|
*/
|
||||||
export const getMe = async ( options?: RequestInit): Promise<AuthUser> => {
|
export const getMe = async ( options?: Parameters<typeof customFetch>[1]): Promise<AuthUser> => {
|
||||||
|
|
||||||
return customFetch<AuthUser>(getGetMeUrl(),
|
return customFetch<AuthUser>(getGetMeUrl(),
|
||||||
{
|
{
|
||||||
@@ -2045,7 +2140,7 @@ export function useGetMe<TData = Awaited<ReturnType<typeof getMe>>, TError = Err
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2065,21 +2160,21 @@ export const getChangeMyPasswordUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Change own password (local users only)
|
* @summary Change own password (local users only)
|
||||||
*/
|
*/
|
||||||
export const changeMyPassword = async (changePasswordInput: ChangePasswordInput, options?: RequestInit): Promise<void> => {
|
export const changeMyPassword = async (changePasswordInput: ChangePasswordInput, options?: Parameters<typeof customFetch>[1]): Promise<void> => {
|
||||||
|
|
||||||
return customFetch<void>(getChangeMyPasswordUrl(),
|
return customFetch<void>(getChangeMyPasswordUrl(),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(changePasswordInput)
|
||||||
changePasswordInput,)
|
|
||||||
}
|
}
|
||||||
);}
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getChangeMyPasswordMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getChangeMyPasswordMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof changeMyPassword>>, TError,{data: BodyType<ChangePasswordInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof changeMyPassword>>, TError,{data: BodyType<ChangePasswordInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof changeMyPassword>>, TError,{data: BodyType<ChangePasswordInput>}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof changeMyPassword>>, TError,{data: BodyType<ChangePasswordInput>}, TContext> => {
|
||||||
@@ -2136,7 +2231,7 @@ export const getGetPasswordRedirectUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Get redirect URL for managing credentials in the identity provider
|
* @summary Get redirect URL for managing credentials in the identity provider
|
||||||
*/
|
*/
|
||||||
export const getPasswordRedirect = async ( options?: RequestInit): Promise<PasswordRedirect> => {
|
export const getPasswordRedirect = async ( options?: Parameters<typeof customFetch>[1]): Promise<PasswordRedirect> => {
|
||||||
|
|
||||||
return customFetch<PasswordRedirect>(getGetPasswordRedirectUrl(),
|
return customFetch<PasswordRedirect>(getGetPasswordRedirectUrl(),
|
||||||
{
|
{
|
||||||
@@ -2193,7 +2288,7 @@ export function useGetPasswordRedirect<TData = Awaited<ReturnType<typeof getPass
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2213,7 +2308,7 @@ export const getGetMePreferencesUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Get current user's browse preferences
|
* @summary Get current user's browse preferences
|
||||||
*/
|
*/
|
||||||
export const getMePreferences = async ( options?: RequestInit): Promise<UserPreferences> => {
|
export const getMePreferences = async ( options?: Parameters<typeof customFetch>[1]): Promise<UserPreferences> => {
|
||||||
|
|
||||||
return customFetch<UserPreferences>(getGetMePreferencesUrl(),
|
return customFetch<UserPreferences>(getGetMePreferencesUrl(),
|
||||||
{
|
{
|
||||||
@@ -2270,7 +2365,7 @@ export function useGetMePreferences<TData = Awaited<ReturnType<typeof getMePrefe
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2290,21 +2385,21 @@ export const getUpdateMePreferencesUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Update current user's browse preferences
|
* @summary Update current user's browse preferences
|
||||||
*/
|
*/
|
||||||
export const updateMePreferences = async (userPreferences: UserPreferences, options?: RequestInit): Promise<UserPreferences> => {
|
export const updateMePreferences = async (userPreferences: UserPreferences, options?: Parameters<typeof customFetch>[1]): Promise<UserPreferences> => {
|
||||||
|
|
||||||
return customFetch<UserPreferences>(getUpdateMePreferencesUrl(),
|
return customFetch<UserPreferences>(getUpdateMePreferencesUrl(),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(userPreferences)
|
||||||
userPreferences,)
|
|
||||||
}
|
}
|
||||||
);}
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getUpdateMePreferencesMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getUpdateMePreferencesMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateMePreferences>>, TError,{data: BodyType<UserPreferences>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateMePreferences>>, TError,{data: BodyType<UserPreferences>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof updateMePreferences>>, TError,{data: BodyType<UserPreferences>}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof updateMePreferences>>, TError,{data: BodyType<UserPreferences>}, TContext> => {
|
||||||
@@ -2361,7 +2456,7 @@ export const getGetMeWatchlistUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Get current user's watchlist tools (premium)
|
* @summary Get current user's watchlist tools (premium)
|
||||||
*/
|
*/
|
||||||
export const getMeWatchlist = async ( options?: RequestInit): Promise<ToolWithStats[]> => {
|
export const getMeWatchlist = async ( options?: Parameters<typeof customFetch>[1]): Promise<ToolWithStats[]> => {
|
||||||
|
|
||||||
return customFetch<ToolWithStats[]>(getGetMeWatchlistUrl(),
|
return customFetch<ToolWithStats[]>(getGetMeWatchlistUrl(),
|
||||||
{
|
{
|
||||||
@@ -2418,7 +2513,7 @@ export function useGetMeWatchlist<TData = Awaited<ReturnType<typeof getMeWatchli
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2438,7 +2533,7 @@ export const getListUsersUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary List all local users (admin only)
|
* @summary List all local users (admin only)
|
||||||
*/
|
*/
|
||||||
export const listUsers = async ( options?: RequestInit): Promise<User[]> => {
|
export const listUsers = async ( options?: Parameters<typeof customFetch>[1]): Promise<User[]> => {
|
||||||
|
|
||||||
return customFetch<User[]>(getListUsersUrl(),
|
return customFetch<User[]>(getListUsersUrl(),
|
||||||
{
|
{
|
||||||
@@ -2495,7 +2590,7 @@ export function useListUsers<TData = Awaited<ReturnType<typeof listUsers>>, TErr
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2515,21 +2610,21 @@ export const getCreateUserUrl = () => {
|
|||||||
/**
|
/**
|
||||||
* @summary Create a new local user (admin only)
|
* @summary Create a new local user (admin only)
|
||||||
*/
|
*/
|
||||||
export const createUser = async (userCreateInput: UserCreateInput, options?: RequestInit): Promise<User> => {
|
export const createUser = async (userCreateInput: UserCreateInput, options?: Parameters<typeof customFetch>[1]): Promise<User> => {
|
||||||
|
|
||||||
return customFetch<User>(getCreateUserUrl(),
|
return customFetch<User>(getCreateUserUrl(),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(userCreateInput)
|
||||||
userCreateInput,)
|
|
||||||
}
|
}
|
||||||
);}
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getCreateUserMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getCreateUserMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createUser>>, TError,{data: BodyType<UserCreateInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createUser>>, TError,{data: BodyType<UserCreateInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof createUser>>, TError,{data: BodyType<UserCreateInput>}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof createUser>>, TError,{data: BodyType<UserCreateInput>}, TContext> => {
|
||||||
@@ -2587,21 +2682,21 @@ export const getUpdateUserUrl = (id: number,) => {
|
|||||||
* @summary Update user role (admin only)
|
* @summary Update user role (admin only)
|
||||||
*/
|
*/
|
||||||
export const updateUser = async (id: number,
|
export const updateUser = async (id: number,
|
||||||
userRoleUpdate: UserRoleUpdate, options?: RequestInit): Promise<User> => {
|
userRoleUpdate: UserRoleUpdate, options?: Parameters<typeof customFetch>[1]): Promise<User> => {
|
||||||
|
|
||||||
return customFetch<User>(getUpdateUserUrl(id),
|
return customFetch<User>(getUpdateUserUrl(id),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(userRoleUpdate)
|
||||||
userRoleUpdate,)
|
|
||||||
}
|
}
|
||||||
);}
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getUpdateUserMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getUpdateUserMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{id: number;data: BodyType<UserRoleUpdate>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{id: number;data: BodyType<UserRoleUpdate>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{id: number;data: BodyType<UserRoleUpdate>}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{id: number;data: BodyType<UserRoleUpdate>}, TContext> => {
|
||||||
@@ -2658,7 +2753,7 @@ export const getDeleteUserUrl = (id: number,) => {
|
|||||||
/**
|
/**
|
||||||
* @summary Delete a user (admin only)
|
* @summary Delete a user (admin only)
|
||||||
*/
|
*/
|
||||||
export const deleteUser = async (id: number, options?: RequestInit): Promise<void> => {
|
export const deleteUser = async (id: number, options?: Parameters<typeof customFetch>[1]): Promise<void> => {
|
||||||
|
|
||||||
return customFetch<void>(getDeleteUserUrl(id),
|
return customFetch<void>(getDeleteUserUrl(id),
|
||||||
{
|
{
|
||||||
@@ -2672,6 +2767,7 @@ export const deleteUser = async (id: number, options?: RequestInit): Promise<voi
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getDeleteUserMutationOptions = <TError = ErrorType<unknown>,
|
export const getDeleteUserMutationOptions = <TError = ErrorType<unknown>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteUser>>, TError,{id: number}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteUser>>, TError,{id: number}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof deleteUser>>, TError,{id: number}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof deleteUser>>, TError,{id: number}, TContext> => {
|
||||||
@@ -2729,21 +2825,21 @@ export const getSetUserPasswordUrl = (id: number,) => {
|
|||||||
* @summary Set/reset a user's password (admin only, local users only)
|
* @summary Set/reset a user's password (admin only, local users only)
|
||||||
*/
|
*/
|
||||||
export const setUserPassword = async (id: number,
|
export const setUserPassword = async (id: number,
|
||||||
setPasswordInput: SetPasswordInput, options?: RequestInit): Promise<void> => {
|
setPasswordInput: SetPasswordInput, options?: Parameters<typeof customFetch>[1]): Promise<void> => {
|
||||||
|
|
||||||
return customFetch<void>(getSetUserPasswordUrl(id),
|
return customFetch<void>(getSetUserPasswordUrl(id),
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(setPasswordInput)
|
||||||
setPasswordInput,)
|
|
||||||
}
|
}
|
||||||
);}
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getSetUserPasswordMutationOptions = <TError = ErrorType<ErrorResponse>,
|
export const getSetUserPasswordMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof setUserPassword>>, TError,{id: number;data: BodyType<SetPasswordInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof setUserPassword>>, TError,{id: number;data: BodyType<SetPasswordInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
): UseMutationOptions<Awaited<ReturnType<typeof setUserPassword>>, TError,{id: number;data: BodyType<SetPasswordInput>}, TContext> => {
|
): UseMutationOptions<Awaited<ReturnType<typeof setUserPassword>>, TError,{id: number;data: BodyType<SetPasswordInput>}, TContext> => {
|
||||||
@@ -2795,7 +2891,7 @@ export const getListAuditLogsUrl = (params?: ListAuditLogsParams,) => {
|
|||||||
Object.entries(params || {}).forEach(([key, value]) => {
|
Object.entries(params || {}).forEach(([key, value]) => {
|
||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
normalizedParams.append(key, value === null ? 'null' : String(value))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2807,7 +2903,7 @@ export const getListAuditLogsUrl = (params?: ListAuditLogsParams,) => {
|
|||||||
/**
|
/**
|
||||||
* @summary List audit log entries (admin only)
|
* @summary List audit log entries (admin only)
|
||||||
*/
|
*/
|
||||||
export const listAuditLogs = async (params?: ListAuditLogsParams, options?: RequestInit): Promise<AuditLog[]> => {
|
export const listAuditLogs = async (params?: ListAuditLogsParams, options?: Parameters<typeof customFetch>[1]): Promise<AuditLog[]> => {
|
||||||
|
|
||||||
return customFetch<AuditLog[]>(getListAuditLogsUrl(params),
|
return customFetch<AuditLog[]>(getListAuditLogsUrl(params),
|
||||||
{
|
{
|
||||||
@@ -2864,7 +2960,7 @@ export function useListAuditLogs<TData = Awaited<ReturnType<typeof listAuditLogs
|
|||||||
|
|
||||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||||
|
|
||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return withQueryKey(query, queryOptions.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from "./generated/api";
|
export * from "./generated/api";
|
||||||
export * from "./generated/api.schemas";
|
export * from "./generated/api.schemas";
|
||||||
export { setBaseUrl, setAuthTokenGetter, customFetch } from "./custom-fetch";
|
export { setBaseUrl, setAuthTokenGetter, setCsrfTokenGetter, customFetch } from "./custom-fetch";
|
||||||
export type { AuthTokenGetter } from "./custom-fetch";
|
export type { AuthTokenGetter } from "./custom-fetch";
|
||||||
|
|||||||
@@ -570,6 +570,19 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/AuthMode"
|
$ref: "#/components/schemas/AuthMode"
|
||||||
|
|
||||||
|
/auth/csrf:
|
||||||
|
get:
|
||||||
|
operationId: getCsrfToken
|
||||||
|
tags: [auth]
|
||||||
|
summary: Get a CSRF token for state-changing requests
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: CSRF token
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/CsrfToken"
|
||||||
|
|
||||||
/auth/login:
|
/auth/login:
|
||||||
post:
|
post:
|
||||||
operationId: localLogin
|
operationId: localLogin
|
||||||
@@ -942,6 +955,13 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
enum: [oidc, local]
|
enum: [oidc, local]
|
||||||
|
|
||||||
|
CsrfToken:
|
||||||
|
type: object
|
||||||
|
required: [token]
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
|
||||||
LocalLoginInput:
|
LocalLoginInput:
|
||||||
type: object
|
type: object
|
||||||
required: [username, password]
|
required: [username, password]
|
||||||
@@ -1013,7 +1033,7 @@ components:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
newPassword:
|
newPassword:
|
||||||
type: string
|
type: string
|
||||||
minLength: 8
|
minLength: 6
|
||||||
|
|
||||||
SetPasswordInput:
|
SetPasswordInput:
|
||||||
type: object
|
type: object
|
||||||
@@ -1021,7 +1041,7 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
password:
|
password:
|
||||||
type: string
|
type: string
|
||||||
minLength: 8
|
minLength: 6
|
||||||
|
|
||||||
PasswordRedirect:
|
PasswordRedirect:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"codegen": "orval --config ./orval.config.ts && pnpm -w run typecheck:libs"
|
"codegen": "orval --config ./orval.config.ts && pnpm -w run typecheck:libs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"orval": "^8.9.1"
|
"orval": "8.23.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
@@ -25,7 +25,7 @@ export const GetVersionResponse = zod.object({
|
|||||||
"version": zod.string(),
|
"version": zod.string(),
|
||||||
"commitSha": zod.string().nullish(),
|
"commitSha": zod.string().nullish(),
|
||||||
"buildDate": zod.string().nullish(),
|
"buildDate": zod.string().nullish(),
|
||||||
"trashRetentionDays": zod.number().optional()
|
"trashRetentionDays": zod.int().optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export const ListToolsQueryParams = zod.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const ListToolsResponseItem = zod.object({
|
export const ListToolsResponseItem = zod.object({
|
||||||
"id": zod.number(),
|
"id": zod.int(),
|
||||||
"name": zod.string(),
|
"name": zod.string(),
|
||||||
"description": zod.string(),
|
"description": zod.string(),
|
||||||
"category": zod.string(),
|
"category": zod.string(),
|
||||||
@@ -58,7 +58,7 @@ export const ListToolsResponseItem = zod.object({
|
|||||||
"tags": zod.array(zod.string()).optional(),
|
"tags": zod.array(zod.string()).optional(),
|
||||||
"createdAt": zod.coerce.date(),
|
"createdAt": zod.coerce.date(),
|
||||||
"updatedAt": zod.coerce.date(),
|
"updatedAt": zod.coerce.date(),
|
||||||
"ratingCount": zod.number(),
|
"ratingCount": zod.int(),
|
||||||
"avgUsefulness": zod.number().nullable(),
|
"avgUsefulness": zod.number().nullable(),
|
||||||
"avgUsability": zod.number().nullable(),
|
"avgUsability": zod.number().nullable(),
|
||||||
"avgCombined": zod.number().nullable()
|
"avgCombined": zod.number().nullable()
|
||||||
@@ -84,16 +84,8 @@ export const CreateToolBody = zod.object({
|
|||||||
"tags": zod.array(zod.string()).optional()
|
"tags": zod.array(zod.string()).optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const CreateToolResponse = zod.object({
|
||||||
/**
|
"id": zod.int(),
|
||||||
* @summary Compare tools side by side (premium)
|
|
||||||
*/
|
|
||||||
export const ListCompareToolsQueryParams = zod.object({
|
|
||||||
"ids": zod.coerce.string().describe('Comma-separated tool ids')
|
|
||||||
})
|
|
||||||
|
|
||||||
export const ListCompareToolsResponseItem = zod.object({
|
|
||||||
"id": zod.number(),
|
|
||||||
"name": zod.string(),
|
"name": zod.string(),
|
||||||
"description": zod.string(),
|
"description": zod.string(),
|
||||||
"category": zod.string(),
|
"category": zod.string(),
|
||||||
@@ -104,7 +96,31 @@ export const ListCompareToolsResponseItem = zod.object({
|
|||||||
"tags": zod.array(zod.string()).optional(),
|
"tags": zod.array(zod.string()).optional(),
|
||||||
"createdAt": zod.coerce.date(),
|
"createdAt": zod.coerce.date(),
|
||||||
"updatedAt": zod.coerce.date(),
|
"updatedAt": zod.coerce.date(),
|
||||||
"ratingCount": zod.number(),
|
"deletedAt": zod.coerce.date().nullish(),
|
||||||
|
"deletedBy": zod.string().nullish()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Compare tools side by side (premium)
|
||||||
|
*/
|
||||||
|
export const ListCompareToolsQueryParams = zod.object({
|
||||||
|
"ids": zod.coerce.string().describe('Comma-separated tool ids')
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ListCompareToolsResponseItem = zod.object({
|
||||||
|
"id": zod.int(),
|
||||||
|
"name": zod.string(),
|
||||||
|
"description": zod.string(),
|
||||||
|
"category": zod.string(),
|
||||||
|
"websiteUrl": zod.string().nullish(),
|
||||||
|
"iconUrl": zod.string().nullish(),
|
||||||
|
"createdBy": zod.string().nullish(),
|
||||||
|
"features": zod.array(zod.string()).optional(),
|
||||||
|
"tags": zod.array(zod.string()).optional(),
|
||||||
|
"createdAt": zod.coerce.date(),
|
||||||
|
"updatedAt": zod.coerce.date(),
|
||||||
|
"ratingCount": zod.int(),
|
||||||
"avgUsefulness": zod.number().nullable(),
|
"avgUsefulness": zod.number().nullable(),
|
||||||
"avgUsability": zod.number().nullable(),
|
"avgUsability": zod.number().nullable(),
|
||||||
"avgCombined": zod.number().nullable()
|
"avgCombined": zod.number().nullable()
|
||||||
@@ -116,7 +132,7 @@ export const ListCompareToolsResponse = zod.array(ListCompareToolsResponseItem)
|
|||||||
* @summary Get a tool's rating history over time
|
* @summary Get a tool's rating history over time
|
||||||
*/
|
*/
|
||||||
export const GetToolRatingHistoryParams = zod.object({
|
export const GetToolRatingHistoryParams = zod.object({
|
||||||
"id": zod.coerce.number()
|
"id": zod.coerce.number().int()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const GetToolRatingHistoryResponseItem = zod.object({
|
export const GetToolRatingHistoryResponseItem = zod.object({
|
||||||
@@ -132,11 +148,11 @@ export const GetToolRatingHistoryResponse = zod.array(GetToolRatingHistoryRespon
|
|||||||
* @summary Get a tool by ID
|
* @summary Get a tool by ID
|
||||||
*/
|
*/
|
||||||
export const GetToolParams = zod.object({
|
export const GetToolParams = zod.object({
|
||||||
"id": zod.coerce.number()
|
"id": zod.coerce.number().int()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const GetToolResponse = zod.object({
|
export const GetToolResponse = zod.object({
|
||||||
"id": zod.number(),
|
"id": zod.int(),
|
||||||
"name": zod.string(),
|
"name": zod.string(),
|
||||||
"description": zod.string(),
|
"description": zod.string(),
|
||||||
"category": zod.string(),
|
"category": zod.string(),
|
||||||
@@ -147,7 +163,7 @@ export const GetToolResponse = zod.object({
|
|||||||
"tags": zod.array(zod.string()).optional(),
|
"tags": zod.array(zod.string()).optional(),
|
||||||
"createdAt": zod.coerce.date(),
|
"createdAt": zod.coerce.date(),
|
||||||
"updatedAt": zod.coerce.date(),
|
"updatedAt": zod.coerce.date(),
|
||||||
"ratingCount": zod.number(),
|
"ratingCount": zod.int(),
|
||||||
"avgUsefulness": zod.number().nullable(),
|
"avgUsefulness": zod.number().nullable(),
|
||||||
"avgUsability": zod.number().nullable(),
|
"avgUsability": zod.number().nullable(),
|
||||||
"avgCombined": zod.number().nullable()
|
"avgCombined": zod.number().nullable()
|
||||||
@@ -158,7 +174,7 @@ export const GetToolResponse = zod.object({
|
|||||||
* @summary Update a tool
|
* @summary Update a tool
|
||||||
*/
|
*/
|
||||||
export const UpdateToolParams = zod.object({
|
export const UpdateToolParams = zod.object({
|
||||||
"id": zod.coerce.number()
|
"id": zod.coerce.number().int()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -175,7 +191,7 @@ export const UpdateToolBody = zod.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const UpdateToolResponse = zod.object({
|
export const UpdateToolResponse = zod.object({
|
||||||
"id": zod.number(),
|
"id": zod.int(),
|
||||||
"name": zod.string(),
|
"name": zod.string(),
|
||||||
"description": zod.string(),
|
"description": zod.string(),
|
||||||
"category": zod.string(),
|
"category": zod.string(),
|
||||||
@@ -195,9 +211,11 @@ export const UpdateToolResponse = zod.object({
|
|||||||
* @summary Delete a tool
|
* @summary Delete a tool
|
||||||
*/
|
*/
|
||||||
export const DeleteToolParams = zod.object({
|
export const DeleteToolParams = zod.object({
|
||||||
"id": zod.coerce.number()
|
"id": zod.coerce.number().int()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const DeleteToolResponse = zod.void()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary List trashed (soft-deleted) tools
|
* @summary List trashed (soft-deleted) tools
|
||||||
@@ -207,7 +225,7 @@ export const ListTrashedToolsQueryParams = zod.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const ListTrashedToolsResponseItem = zod.object({
|
export const ListTrashedToolsResponseItem = zod.object({
|
||||||
"id": zod.number(),
|
"id": zod.int(),
|
||||||
"name": zod.string(),
|
"name": zod.string(),
|
||||||
"description": zod.string(),
|
"description": zod.string(),
|
||||||
"category": zod.string(),
|
"category": zod.string(),
|
||||||
@@ -232,11 +250,11 @@ export const trashToolsBodyIdsMax = 500;
|
|||||||
|
|
||||||
|
|
||||||
export const TrashToolsBody = zod.object({
|
export const TrashToolsBody = zod.object({
|
||||||
"ids": zod.array(zod.number()).min(1).max(trashToolsBodyIdsMax)
|
"ids": zod.array(zod.int()).min(1).max(trashToolsBodyIdsMax)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const TrashToolsResponse = zod.object({
|
export const TrashToolsResponse = zod.object({
|
||||||
"trashed": zod.number().optional()
|
"trashed": zod.int().optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -248,9 +266,11 @@ export const deleteTrashedToolsBodyIdsMax = 500;
|
|||||||
|
|
||||||
|
|
||||||
export const DeleteTrashedToolsBody = zod.object({
|
export const DeleteTrashedToolsBody = zod.object({
|
||||||
"ids": zod.array(zod.number()).min(1).max(deleteTrashedToolsBodyIdsMax)
|
"ids": zod.array(zod.int()).min(1).max(deleteTrashedToolsBodyIdsMax)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const DeleteTrashedToolsResponse = zod.void()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Restore trashed tools
|
* @summary Restore trashed tools
|
||||||
@@ -260,11 +280,11 @@ export const restoreToolsBodyIdsMax = 500;
|
|||||||
|
|
||||||
|
|
||||||
export const RestoreToolsBody = zod.object({
|
export const RestoreToolsBody = zod.object({
|
||||||
"ids": zod.array(zod.number()).min(1).max(restoreToolsBodyIdsMax)
|
"ids": zod.array(zod.int()).min(1).max(restoreToolsBodyIdsMax)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const RestoreToolsResponse = zod.object({
|
export const RestoreToolsResponse = zod.object({
|
||||||
"restored": zod.number().optional()
|
"restored": zod.int().optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -272,7 +292,7 @@ export const RestoreToolsResponse = zod.object({
|
|||||||
* @summary Permanently delete all trashed tools (admin)
|
* @summary Permanently delete all trashed tools (admin)
|
||||||
*/
|
*/
|
||||||
export const EmptyTrashResponse = zod.object({
|
export const EmptyTrashResponse = zod.object({
|
||||||
"deleted": zod.number().optional()
|
"deleted": zod.int().optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -280,7 +300,7 @@ export const EmptyTrashResponse = zod.object({
|
|||||||
* @summary List ratings for a tool
|
* @summary List ratings for a tool
|
||||||
*/
|
*/
|
||||||
export const ListToolRatingsParams = zod.object({
|
export const ListToolRatingsParams = zod.object({
|
||||||
"id": zod.coerce.number()
|
"id": zod.coerce.number().int()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const listToolRatingsResponseUsefulnessMax = 5;
|
export const listToolRatingsResponseUsefulnessMax = 5;
|
||||||
@@ -290,10 +310,10 @@ export const listToolRatingsResponseUsabilityMax = 5;
|
|||||||
|
|
||||||
|
|
||||||
export const ListToolRatingsResponseItem = zod.object({
|
export const ListToolRatingsResponseItem = zod.object({
|
||||||
"id": zod.number(),
|
"id": zod.int(),
|
||||||
"toolId": zod.number(),
|
"toolId": zod.int(),
|
||||||
"usefulness": zod.number().min(1).max(listToolRatingsResponseUsefulnessMax),
|
"usefulness": zod.int().min(1).max(listToolRatingsResponseUsefulnessMax),
|
||||||
"usability": zod.number().min(1).max(listToolRatingsResponseUsabilityMax),
|
"usability": zod.int().min(1).max(listToolRatingsResponseUsabilityMax),
|
||||||
"comment": zod.string().nullish(),
|
"comment": zod.string().nullish(),
|
||||||
"reviewerName": zod.string().nullish(),
|
"reviewerName": zod.string().nullish(),
|
||||||
"createdAt": zod.coerce.date()
|
"createdAt": zod.coerce.date()
|
||||||
@@ -305,7 +325,7 @@ export const ListToolRatingsResponse = zod.array(ListToolRatingsResponseItem)
|
|||||||
* @summary Submit a rating for a tool
|
* @summary Submit a rating for a tool
|
||||||
*/
|
*/
|
||||||
export const CreateRatingParams = zod.object({
|
export const CreateRatingParams = zod.object({
|
||||||
"id": zod.coerce.number()
|
"id": zod.coerce.number().int()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createRatingBodyUsefulnessMax = 5;
|
export const createRatingBodyUsefulnessMax = 5;
|
||||||
@@ -315,25 +335,41 @@ export const createRatingBodyUsabilityMax = 5;
|
|||||||
|
|
||||||
|
|
||||||
export const CreateRatingBody = zod.object({
|
export const CreateRatingBody = zod.object({
|
||||||
"usefulness": zod.number().min(1).max(createRatingBodyUsefulnessMax),
|
"usefulness": zod.int().min(1).max(createRatingBodyUsefulnessMax),
|
||||||
"usability": zod.number().min(1).max(createRatingBodyUsabilityMax),
|
"usability": zod.int().min(1).max(createRatingBodyUsabilityMax),
|
||||||
"comment": zod.string().optional(),
|
"comment": zod.string().optional(),
|
||||||
"reviewerName": zod.string().optional()
|
"reviewerName": zod.string().optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const createRatingResponseUsefulnessMax = 5;
|
||||||
|
|
||||||
|
export const createRatingResponseUsabilityMax = 5;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const CreateRatingResponse = zod.object({
|
||||||
|
"id": zod.int(),
|
||||||
|
"toolId": zod.int(),
|
||||||
|
"usefulness": zod.int().min(1).max(createRatingResponseUsefulnessMax),
|
||||||
|
"usability": zod.int().min(1).max(createRatingResponseUsabilityMax),
|
||||||
|
"comment": zod.string().nullish(),
|
||||||
|
"reviewerName": zod.string().nullish(),
|
||||||
|
"createdAt": zod.coerce.date()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Overall platform statistics
|
* @summary Overall platform statistics
|
||||||
*/
|
*/
|
||||||
export const GetAnalyticsSummaryResponse = zod.object({
|
export const GetAnalyticsSummaryResponse = zod.object({
|
||||||
"totalTools": zod.number(),
|
"totalTools": zod.int(),
|
||||||
"totalRatings": zod.number(),
|
"totalRatings": zod.int(),
|
||||||
"avgUsefulness": zod.number().nullable(),
|
"avgUsefulness": zod.number().nullable(),
|
||||||
"avgUsability": zod.number().nullable(),
|
"avgUsability": zod.number().nullable(),
|
||||||
"avgCombined": zod.number().nullable(),
|
"avgCombined": zod.number().nullable(),
|
||||||
"categoriesCount": zod.number(),
|
"categoriesCount": zod.int(),
|
||||||
"mostRatedTool": zod.object({
|
"mostRatedTool": zod.object({
|
||||||
"id": zod.number(),
|
"id": zod.int(),
|
||||||
"name": zod.string(),
|
"name": zod.string(),
|
||||||
"description": zod.string(),
|
"description": zod.string(),
|
||||||
"category": zod.string(),
|
"category": zod.string(),
|
||||||
@@ -344,7 +380,7 @@ export const GetAnalyticsSummaryResponse = zod.object({
|
|||||||
"tags": zod.array(zod.string()).optional(),
|
"tags": zod.array(zod.string()).optional(),
|
||||||
"createdAt": zod.coerce.date(),
|
"createdAt": zod.coerce.date(),
|
||||||
"updatedAt": zod.coerce.date(),
|
"updatedAt": zod.coerce.date(),
|
||||||
"ratingCount": zod.number(),
|
"ratingCount": zod.int(),
|
||||||
"avgUsefulness": zod.number().nullable(),
|
"avgUsefulness": zod.number().nullable(),
|
||||||
"avgUsability": zod.number().nullable(),
|
"avgUsability": zod.number().nullable(),
|
||||||
"avgCombined": zod.number().nullable()
|
"avgCombined": zod.number().nullable()
|
||||||
@@ -356,13 +392,13 @@ export const GetAnalyticsSummaryResponse = zod.object({
|
|||||||
* @summary Top-rated tools
|
* @summary Top-rated tools
|
||||||
*/
|
*/
|
||||||
export const GetTopToolsQueryParams = zod.object({
|
export const GetTopToolsQueryParams = zod.object({
|
||||||
"limit": zod.coerce.number().optional(),
|
"limit": zod.coerce.number().int().optional(),
|
||||||
"metric": zod.enum(['usefulness', 'usability', 'combined']).optional()
|
"metric": zod.enum(['usefulness', 'usability', 'combined']).optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const GetTopToolsResponseItem = zod.object({
|
export const GetTopToolsResponseItem = zod.object({
|
||||||
"tool": zod.object({
|
"tool": zod.object({
|
||||||
"id": zod.number(),
|
"id": zod.int(),
|
||||||
"name": zod.string(),
|
"name": zod.string(),
|
||||||
"description": zod.string(),
|
"description": zod.string(),
|
||||||
"category": zod.string(),
|
"category": zod.string(),
|
||||||
@@ -373,13 +409,13 @@ export const GetTopToolsResponseItem = zod.object({
|
|||||||
"tags": zod.array(zod.string()).optional(),
|
"tags": zod.array(zod.string()).optional(),
|
||||||
"createdAt": zod.coerce.date(),
|
"createdAt": zod.coerce.date(),
|
||||||
"updatedAt": zod.coerce.date(),
|
"updatedAt": zod.coerce.date(),
|
||||||
"ratingCount": zod.number(),
|
"ratingCount": zod.int(),
|
||||||
"avgUsefulness": zod.number().nullable(),
|
"avgUsefulness": zod.number().nullable(),
|
||||||
"avgUsability": zod.number().nullable(),
|
"avgUsability": zod.number().nullable(),
|
||||||
"avgCombined": zod.number().nullable()
|
"avgCombined": zod.number().nullable()
|
||||||
}),
|
}),
|
||||||
"score": zod.number(),
|
"score": zod.number(),
|
||||||
"ratingCount": zod.number()
|
"ratingCount": zod.int()
|
||||||
})
|
})
|
||||||
export const GetTopToolsResponse = zod.array(GetTopToolsResponseItem)
|
export const GetTopToolsResponse = zod.array(GetTopToolsResponseItem)
|
||||||
|
|
||||||
@@ -389,8 +425,8 @@ export const GetTopToolsResponse = zod.array(GetTopToolsResponseItem)
|
|||||||
*/
|
*/
|
||||||
export const GetAnalyticsByCategoryResponseItem = zod.object({
|
export const GetAnalyticsByCategoryResponseItem = zod.object({
|
||||||
"category": zod.string(),
|
"category": zod.string(),
|
||||||
"toolCount": zod.number(),
|
"toolCount": zod.int(),
|
||||||
"totalRatings": zod.number(),
|
"totalRatings": zod.int(),
|
||||||
"avgUsefulness": zod.number().nullable(),
|
"avgUsefulness": zod.number().nullable(),
|
||||||
"avgUsability": zod.number().nullable()
|
"avgUsability": zod.number().nullable()
|
||||||
})
|
})
|
||||||
@@ -401,17 +437,17 @@ export const GetAnalyticsByCategoryResponse = zod.array(GetAnalyticsByCategoryRe
|
|||||||
* @summary Distribution of rating scores across the platform
|
* @summary Distribution of rating scores across the platform
|
||||||
*/
|
*/
|
||||||
export const GetRatingDistributionQueryParams = zod.object({
|
export const GetRatingDistributionQueryParams = zod.object({
|
||||||
"toolId": zod.coerce.number().optional()
|
"toolId": zod.coerce.number().int().optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const GetRatingDistributionResponse = zod.object({
|
export const GetRatingDistributionResponse = zod.object({
|
||||||
"usefulness": zod.array(zod.object({
|
"usefulness": zod.array(zod.object({
|
||||||
"score": zod.number(),
|
"score": zod.int(),
|
||||||
"count": zod.number()
|
"count": zod.int()
|
||||||
})),
|
})),
|
||||||
"usability": zod.array(zod.object({
|
"usability": zod.array(zod.object({
|
||||||
"score": zod.number(),
|
"score": zod.int(),
|
||||||
"count": zod.number()
|
"count": zod.int()
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -445,6 +481,14 @@ export const GetAuthModeResponse = zod.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Get a CSRF token for state-changing requests
|
||||||
|
*/
|
||||||
|
export const GetCsrfTokenResponse = zod.object({
|
||||||
|
"token": zod.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Local username/password login
|
* @summary Local username/password login
|
||||||
*/
|
*/
|
||||||
@@ -484,7 +528,7 @@ export const GetMeResponse = zod.object({
|
|||||||
* @summary Change own password (local users only)
|
* @summary Change own password (local users only)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const changeMyPasswordBodyNewPasswordMin = 8;
|
export const changeMyPasswordBodyNewPasswordMin = 6;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -493,6 +537,8 @@ export const ChangeMyPasswordBody = zod.object({
|
|||||||
"newPassword": zod.string().min(changeMyPasswordBodyNewPasswordMin)
|
"newPassword": zod.string().min(changeMyPasswordBodyNewPasswordMin)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const ChangeMyPasswordResponse = zod.void()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Get redirect URL for managing credentials in the identity provider
|
* @summary Get redirect URL for managing credentials in the identity provider
|
||||||
@@ -508,7 +554,7 @@ export const GetPasswordRedirectResponse = zod.object({
|
|||||||
export const GetMePreferencesResponse = zod.object({
|
export const GetMePreferencesResponse = zod.object({
|
||||||
"view": zod.enum(['grid', 'table', 'rows']).optional(),
|
"view": zod.enum(['grid', 'table', 'rows']).optional(),
|
||||||
"density": zod.enum(['cozy', 'compact']).optional(),
|
"density": zod.enum(['cozy', 'compact']).optional(),
|
||||||
"watchlist": zod.array(zod.number()).optional()
|
"watchlist": zod.array(zod.int()).optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -518,13 +564,13 @@ export const GetMePreferencesResponse = zod.object({
|
|||||||
export const UpdateMePreferencesBody = zod.object({
|
export const UpdateMePreferencesBody = zod.object({
|
||||||
"view": zod.enum(['grid', 'table', 'rows']).optional(),
|
"view": zod.enum(['grid', 'table', 'rows']).optional(),
|
||||||
"density": zod.enum(['cozy', 'compact']).optional(),
|
"density": zod.enum(['cozy', 'compact']).optional(),
|
||||||
"watchlist": zod.array(zod.number()).optional()
|
"watchlist": zod.array(zod.int()).optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const UpdateMePreferencesResponse = zod.object({
|
export const UpdateMePreferencesResponse = zod.object({
|
||||||
"view": zod.enum(['grid', 'table', 'rows']).optional(),
|
"view": zod.enum(['grid', 'table', 'rows']).optional(),
|
||||||
"density": zod.enum(['cozy', 'compact']).optional(),
|
"density": zod.enum(['cozy', 'compact']).optional(),
|
||||||
"watchlist": zod.array(zod.number()).optional()
|
"watchlist": zod.array(zod.int()).optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -532,7 +578,7 @@ export const UpdateMePreferencesResponse = zod.object({
|
|||||||
* @summary Get current user's watchlist tools (premium)
|
* @summary Get current user's watchlist tools (premium)
|
||||||
*/
|
*/
|
||||||
export const GetMeWatchlistResponseItem = zod.object({
|
export const GetMeWatchlistResponseItem = zod.object({
|
||||||
"id": zod.number(),
|
"id": zod.int(),
|
||||||
"name": zod.string(),
|
"name": zod.string(),
|
||||||
"description": zod.string(),
|
"description": zod.string(),
|
||||||
"category": zod.string(),
|
"category": zod.string(),
|
||||||
@@ -543,7 +589,7 @@ export const GetMeWatchlistResponseItem = zod.object({
|
|||||||
"tags": zod.array(zod.string()).optional(),
|
"tags": zod.array(zod.string()).optional(),
|
||||||
"createdAt": zod.coerce.date(),
|
"createdAt": zod.coerce.date(),
|
||||||
"updatedAt": zod.coerce.date(),
|
"updatedAt": zod.coerce.date(),
|
||||||
"ratingCount": zod.number(),
|
"ratingCount": zod.int(),
|
||||||
"avgUsefulness": zod.number().nullable(),
|
"avgUsefulness": zod.number().nullable(),
|
||||||
"avgUsability": zod.number().nullable(),
|
"avgUsability": zod.number().nullable(),
|
||||||
"avgCombined": zod.number().nullable()
|
"avgCombined": zod.number().nullable()
|
||||||
@@ -557,7 +603,7 @@ export const GetMeWatchlistResponse = zod.array(GetMeWatchlistResponseItem)
|
|||||||
export const listUsersResponseAuthProviderDefault = `local`;
|
export const listUsersResponseAuthProviderDefault = `local`;
|
||||||
|
|
||||||
export const ListUsersResponseItem = zod.object({
|
export const ListUsersResponseItem = zod.object({
|
||||||
"id": zod.number(),
|
"id": zod.int(),
|
||||||
"username": zod.string(),
|
"username": zod.string(),
|
||||||
"email": zod.string().nullish(),
|
"email": zod.string().nullish(),
|
||||||
"role": zod.enum(['admin', 'user']),
|
"role": zod.enum(['admin', 'user']),
|
||||||
@@ -585,12 +631,24 @@ export const CreateUserBody = zod.object({
|
|||||||
"tier": zod.enum(['free', 'premium', 'enterprise']).optional()
|
"tier": zod.enum(['free', 'premium', 'enterprise']).optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const createUserResponseAuthProviderDefault = `local`;
|
||||||
|
|
||||||
|
export const CreateUserResponse = zod.object({
|
||||||
|
"id": zod.int(),
|
||||||
|
"username": zod.string(),
|
||||||
|
"email": zod.string().nullish(),
|
||||||
|
"role": zod.enum(['admin', 'user']),
|
||||||
|
"tier": zod.enum(['free', 'premium', 'enterprise']).optional(),
|
||||||
|
"authProvider": zod.enum(['local', 'oidc']).default(createUserResponseAuthProviderDefault),
|
||||||
|
"createdAt": zod.coerce.date()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Update user role (admin only)
|
* @summary Update user role (admin only)
|
||||||
*/
|
*/
|
||||||
export const UpdateUserParams = zod.object({
|
export const UpdateUserParams = zod.object({
|
||||||
"id": zod.coerce.number()
|
"id": zod.coerce.number().int()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const UpdateUserBody = zod.object({
|
export const UpdateUserBody = zod.object({
|
||||||
@@ -601,7 +659,7 @@ export const UpdateUserBody = zod.object({
|
|||||||
export const updateUserResponseAuthProviderDefault = `local`;
|
export const updateUserResponseAuthProviderDefault = `local`;
|
||||||
|
|
||||||
export const UpdateUserResponse = zod.object({
|
export const UpdateUserResponse = zod.object({
|
||||||
"id": zod.number(),
|
"id": zod.int(),
|
||||||
"username": zod.string(),
|
"username": zod.string(),
|
||||||
"email": zod.string().nullish(),
|
"email": zod.string().nullish(),
|
||||||
"role": zod.enum(['admin', 'user']),
|
"role": zod.enum(['admin', 'user']),
|
||||||
@@ -615,18 +673,20 @@ export const UpdateUserResponse = zod.object({
|
|||||||
* @summary Delete a user (admin only)
|
* @summary Delete a user (admin only)
|
||||||
*/
|
*/
|
||||||
export const DeleteUserParams = zod.object({
|
export const DeleteUserParams = zod.object({
|
||||||
"id": zod.coerce.number()
|
"id": zod.coerce.number().int()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const DeleteUserResponse = zod.void()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Set/reset a user's password (admin only, local users only)
|
* @summary Set/reset a user's password (admin only, local users only)
|
||||||
*/
|
*/
|
||||||
export const SetUserPasswordParams = zod.object({
|
export const SetUserPasswordParams = zod.object({
|
||||||
"id": zod.coerce.number()
|
"id": zod.coerce.number().int()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const setUserPasswordBodyPasswordMin = 8;
|
export const setUserPasswordBodyPasswordMin = 6;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -634,20 +694,22 @@ export const SetUserPasswordBody = zod.object({
|
|||||||
"password": zod.string().min(setUserPasswordBodyPasswordMin)
|
"password": zod.string().min(setUserPasswordBodyPasswordMin)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const SetUserPasswordResponse = zod.void()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary List audit log entries (admin only)
|
* @summary List audit log entries (admin only)
|
||||||
*/
|
*/
|
||||||
export const ListAuditLogsQueryParams = zod.object({
|
export const ListAuditLogsQueryParams = zod.object({
|
||||||
"entityType": zod.coerce.string().optional(),
|
"entityType": zod.coerce.string().optional(),
|
||||||
"entityId": zod.coerce.number().optional(),
|
"entityId": zod.coerce.number().int().optional(),
|
||||||
"limit": zod.coerce.number().optional()
|
"limit": zod.coerce.number().int().optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ListAuditLogsResponseItem = zod.object({
|
export const ListAuditLogsResponseItem = zod.object({
|
||||||
"id": zod.number(),
|
"id": zod.int(),
|
||||||
"entityType": zod.string(),
|
"entityType": zod.string(),
|
||||||
"entityId": zod.number().nullish(),
|
"entityId": zod.int().nullish(),
|
||||||
"action": zod.string(),
|
"action": zod.string(),
|
||||||
"userId": zod.string(),
|
"userId": zod.string(),
|
||||||
"username": zod.string(),
|
"username": zod.string(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
@@ -9,6 +9,6 @@
|
|||||||
export interface ChangePasswordInput {
|
export interface ChangePasswordInput {
|
||||||
/** @minLength 1 */
|
/** @minLength 1 */
|
||||||
currentPassword: string;
|
currentPassword: string;
|
||||||
/** @minLength 8 */
|
/** @minLength 6 */
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Generated by orval v8.23.0 🍺
|
||||||
|
* Do not edit manually.
|
||||||
|
* Api
|
||||||
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
* OpenAPI spec version: 0.1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CsrfToken {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
@@ -15,6 +15,7 @@ export * from './authUserRole';
|
|||||||
export * from './authUserTier';
|
export * from './authUserTier';
|
||||||
export * from './categoryStats';
|
export * from './categoryStats';
|
||||||
export * from './changePasswordInput';
|
export * from './changePasswordInput';
|
||||||
|
export * from './csrfToken';
|
||||||
export * from './emptyTrash200';
|
export * from './emptyTrash200';
|
||||||
export * from './errorResponse';
|
export * from './errorResponse';
|
||||||
export * from './getRatingDistributionParams';
|
export * from './getRatingDistributionParams';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export interface SetPasswordInput {
|
export interface SetPasswordInput {
|
||||||
/** @minLength 8 */
|
/** @minLength 6 */
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Generated by orval v8.9.1 🍺
|
* Generated by orval v8.23.0 🍺
|
||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* Api
|
* Api
|
||||||
* ToolRate API — Tool listing and rating platform
|
* ToolRate API — Tool listing and rating platform
|
||||||
|
|||||||
+4
-4
@@ -13,13 +13,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"drizzle-zod": "^0.8.3",
|
"drizzle-zod": "0.8.3",
|
||||||
"pg": "^8.20.0",
|
"pg": "8.22.0",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@types/pg": "^8.20.0",
|
"@types/pg": "8.20.3",
|
||||||
"drizzle-kit": "^0.31.10"
|
"drizzle-kit": "0.31.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-17
@@ -10,24 +10,13 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.8.3",
|
"prettier": "3.9.6",
|
||||||
"typescript": "~5.9.3"
|
"typescript": "7.0.2"
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"supportedArchitectures": {
|
|
||||||
"os": [
|
|
||||||
"current",
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"current",
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-darwin-arm64": "^4.62.4",
|
"@esbuild/darwin-arm64": "0.28.1",
|
||||||
"@tailwindcss/oxide-darwin-arm64": "^4.3.3",
|
"@rollup/rollup-darwin-arm64": "4.62.4",
|
||||||
"lightningcss-darwin-arm64": "^1.33.0"
|
"@tailwindcss/oxide-darwin-arm64": "4.3.3",
|
||||||
|
"lightningcss-darwin-arm64": "1.33.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2539
-1929
File diff suppressed because it is too large
Load Diff
+50
-32
@@ -27,6 +27,14 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
minimumReleaseAge: 1440
|
minimumReleaseAge: 1440
|
||||||
|
|
||||||
|
supportedArchitectures:
|
||||||
|
os:
|
||||||
|
- current
|
||||||
|
- linux
|
||||||
|
cpu:
|
||||||
|
- current
|
||||||
|
- x64
|
||||||
|
|
||||||
minimumReleaseAgeExclude:
|
minimumReleaseAgeExclude:
|
||||||
# Exclude @replit scoped packages from the minimum release age check.
|
# Exclude @replit scoped packages from the minimum release age check.
|
||||||
# These are published by Replit and trusted — the supply-chain attack vector
|
# These are published by Replit and trusted — the supply-chain attack vector
|
||||||
@@ -41,39 +49,42 @@ packages:
|
|||||||
- scripts
|
- scripts
|
||||||
|
|
||||||
catalog:
|
catalog:
|
||||||
'@replit/vite-plugin-cartographer': ^0.5.1
|
'@replit/vite-plugin-cartographer': 0.6.1
|
||||||
'@replit/vite-plugin-dev-banner': ^0.1.1
|
'@replit/vite-plugin-dev-banner': 0.1.2
|
||||||
'@replit/vite-plugin-runtime-error-modal': ^0.0.6
|
'@replit/vite-plugin-runtime-error-modal': 0.0.6
|
||||||
'@tailwindcss/vite': ^4.1.14
|
'@tailwindcss/vite': 4.3.3
|
||||||
'@tanstack/react-query': ^5.90.21
|
'@tanstack/react-query': 5.101.4
|
||||||
'@tanstack/react-virtual': ^3.13.6
|
'@tanstack/react-virtual': 3.14.9
|
||||||
'@types/node': ^25.3.3
|
'@types/node': 26.1.2
|
||||||
'@types/react': ^19.2.0
|
'@types/react': 19.2.18
|
||||||
'@types/react-dom': ^19.2.0
|
'@types/react-dom': 19.2.4
|
||||||
'@vitejs/plugin-react': ^5.0.4
|
'@vitejs/plugin-react': 6.0.5
|
||||||
class-variance-authority: ^0.7.1
|
class-variance-authority: 0.7.1
|
||||||
clsx: ^2.1.1
|
clsx: 2.1.1
|
||||||
drizzle-orm: ^0.45.2
|
dompurify: 3.4.12
|
||||||
framer-motion: ^12.23.24
|
drizzle-orm: 0.45.2
|
||||||
lucide-react: ^0.545.0
|
framer-motion: 12.43.0
|
||||||
# Must be this exact version because expo requires it
|
lucide-react: 1.28.0
|
||||||
react: 19.1.0
|
marked: 18.0.7
|
||||||
# Must be this exact version because expo requires it
|
react: 19.2.8
|
||||||
react-dom: 19.1.0
|
react-dom: 19.2.8
|
||||||
tailwind-merge: ^3.3.1
|
tailwind-merge: 3.6.0
|
||||||
tailwindcss: ^4.1.14
|
tailwindcss: 4.3.3
|
||||||
tsx: ^4.21.0
|
tsx: 4.23.4
|
||||||
vite: ^7.3.4
|
vite: 8.2.0
|
||||||
wouter: ^3.3.5
|
wouter: 3.10.0
|
||||||
zod: ^3.25.76
|
zod: 4.4.3
|
||||||
|
|
||||||
autoInstallPeers: false
|
autoInstallPeers: false
|
||||||
|
|
||||||
onlyBuiltDependencies:
|
# pnpm 11: allowBuilds (map) replaces onlyBuiltDependencies (list).
|
||||||
- '@swc/core'
|
# Build scripts are blocked by default (supply-chain defense). Only allow
|
||||||
- esbuild
|
# scripts for packages that genuinely need a postinstall to work.
|
||||||
- msw
|
allowBuilds:
|
||||||
- unrs-resolver
|
'@swc/core': true
|
||||||
|
esbuild: true
|
||||||
|
msw: true
|
||||||
|
unrs-resolver: true
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
# replit uses linux-x64 only, we can exclude all other platforms
|
# replit uses linux-x64 only, we can exclude all other platforms
|
||||||
@@ -157,7 +168,14 @@ overrides:
|
|||||||
"@expo/ngrok-bin>@expo/ngrok-bin-win32-ia32": "-"
|
"@expo/ngrok-bin>@expo/ngrok-bin-win32-ia32": "-"
|
||||||
"@expo/ngrok-bin>@expo/ngrok-bin-win32-x64": "-"
|
"@expo/ngrok-bin>@expo/ngrok-bin-win32-x64": "-"
|
||||||
# drizzle-kit uses esbuild internally on an older version that's vulnerable, this overrides it
|
# drizzle-kit uses esbuild internally on an older version that's vulnerable, this overrides it
|
||||||
"@esbuild-kit/esm-loader": "npm:tsx@^4.21.0"
|
"@esbuild-kit/esm-loader": "npm:tsx@4.23.1"
|
||||||
esbuild: "0.27.3"
|
esbuild: "0.28.1"
|
||||||
# Fix GHSA-q8mj-m7cp-5q26: qs DoS via stringify with null/undefined in comma-format arrays
|
# Fix GHSA-q8mj-m7cp-5q26: qs DoS via stringify with null/undefined in comma-format arrays
|
||||||
qs: ">=6.15.2"
|
qs: ">=6.15.2"
|
||||||
|
# Fix GHSA-v422-hmwv-36x6: body-parser DoS via invalid limit value (express dep)
|
||||||
|
body-parser: ">=2.3.0"
|
||||||
|
# Build-time tooling (orval/typedoc) advisories: force patched versions
|
||||||
|
markdown-it: ">=14.1.2"
|
||||||
|
linkify-it: ">=5.0.2"
|
||||||
|
brace-expansion: ">=5.0.8"
|
||||||
|
fast-uri: ">=3.1.4"
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended",
|
||||||
|
":maintainLockFilesWeekly",
|
||||||
|
":semanticCommitTypeAll(chore)"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchDepTypes": ["dependencies", "devDependencies", "peerDependencies"],
|
||||||
|
"matchUpdateTypes": ["patch", "minor"],
|
||||||
|
"groupName": "non-breaking updates",
|
||||||
|
"minimumReleaseAge": "1 day"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["major"],
|
||||||
|
"groupName": "major updates",
|
||||||
|
"minimumReleaseAge": "7 days",
|
||||||
|
"labels": ["major-update"],
|
||||||
|
"prPriority": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackageNames": ["react", "react-dom", "esbuild"],
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schedule": ["on the first day of the week"],
|
||||||
|
"lockFileMaintenance": { "enabled": true }
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Copies docs/releases/*.md into the toolrate frontend's static public dir
|
||||||
|
// and generates index.json (version list) so the app can render /docs.
|
||||||
|
//
|
||||||
|
// Run before `vite build` (Vite copies public/ -> dist/public verbatim), and
|
||||||
|
// before `vite dev` so the docs are available locally too.
|
||||||
|
//
|
||||||
|
// Usage: node scripts/src/sync-release-docs.mjs
|
||||||
|
|
||||||
|
import { readdir, readFile, copyFile, mkdir, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { resolve, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
||||||
|
const sourceDir = resolve(root, "docs/releases");
|
||||||
|
const targetDir = resolve(root, "artifacts/toolrate/public/docs");
|
||||||
|
|
||||||
|
const isReleaseFile = (name) => /^v\d+\.\d+\.\d+\.md$/.test(name);
|
||||||
|
|
||||||
|
function parseVersion(name) {
|
||||||
|
return name.replace(/\.md$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic-ish comparison: v0.6.0 > v0.5.0 > v0.4.2
|
||||||
|
function cmp(a, b) {
|
||||||
|
const pa = parseVersion(a).slice(1).split(".").map(Number);
|
||||||
|
const pb = parseVersion(b).slice(1).split(".").map(Number);
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTitle(content) {
|
||||||
|
const m = content.match(/^#\s+(.+)$/m);
|
||||||
|
return m ? m[1].trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDate(content) {
|
||||||
|
// e.g. "**Datum:** 2026-08-03" or "**Date:** 2026-08-03" in the header block.
|
||||||
|
// Match the first ISO date that follows a Datum/Date label, tolerating
|
||||||
|
// bold markers/colons around it.
|
||||||
|
const m = content.match(/(?:Datum|Date)[^\d\n]{0,20}(\d{4}-\d{2}-\d{2})/);
|
||||||
|
return m ? m[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let names;
|
||||||
|
try {
|
||||||
|
names = (await readdir(sourceDir)).filter(isReleaseFile);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === "ENOENT") {
|
||||||
|
console.warn(`[sync-release-docs] ${sourceDir} not found; nothing to sync`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (names.length === 0) {
|
||||||
|
console.warn("[sync-release-docs] no release docs found; clearing target dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
names.sort(cmp).reverse();
|
||||||
|
|
||||||
|
await rm(targetDir, { recursive: true, force: true });
|
||||||
|
await mkdir(targetDir, { recursive: true });
|
||||||
|
|
||||||
|
const index = [];
|
||||||
|
for (const name of names) {
|
||||||
|
const content = await readFile(join(sourceDir, name), "utf8");
|
||||||
|
await copyFile(join(sourceDir, name), join(targetDir, name));
|
||||||
|
index.push({
|
||||||
|
version: parseVersion(name),
|
||||||
|
file: name,
|
||||||
|
title: extractTitle(content) ?? parseVersion(name),
|
||||||
|
date: extractDate(content) ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(join(targetDir, "index.json"), JSON.stringify(index, null, 2));
|
||||||
|
|
||||||
|
console.log(`[sync-release-docs] synced ${names.length} release doc(s) -> ${targetDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("[sync-release-docs] failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user