feat: introduce Gatekeeper application, Directus utilities, and monorepo configuration for linting, testing, and husky hooks.
Some checks failed
Code Quality / lint-and-build (push) Failing after 52s
Release Packages / release (push) Failing after 32s

This commit is contained in:
2026-02-01 21:23:34 +01:00
parent c2a0ba88c0
commit 83b4ea8807
51 changed files with 3150 additions and 282 deletions

View File

@@ -0,0 +1,3 @@
import baseConfig from "@mintel/eslint-config";
export default baseConfig;

View File

@@ -11,17 +11,20 @@
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --watch --dts",
"lint": "eslint src/"
"lint": "eslint src/",
"test": "vitest run"
},
"dependencies": {
"@directus/sdk": "^21.0.0",
"next": "15.1.6",
"next-intl": "^3.0.0",
"zod": "^3.0.0"
},
"devDependencies": {
"tsup": "^8.0.0",
"typescript": "^5.0.0",
"@mintel/eslint-config": "workspace:*",
"@mintel/tsconfig": "workspace:*",
"@mintel/eslint-config": "workspace:*"
"eslint": "^9.39.2",
"tsup": "^8.0.0",
"typescript": "^5.0.0"
}
}

View File

@@ -0,0 +1,47 @@
import {
createDirectus,
rest,
authentication,
DirectusClient,
RestClient,
AuthenticationClient,
} from "@directus/sdk";
export type MintelDirectusClient = DirectusClient<any> &
RestClient<any> &
AuthenticationClient<any>;
/**
* Creates a Directus client configured with Mintel standards
*/
export function createMintelDirectusClient(url?: string): MintelDirectusClient {
const directusUrl =
url || process.env.DIRECTUS_URL || "http://localhost:8055";
return createDirectus(directusUrl).with(rest()).with(authentication());
}
/**
* Ensures the client is authenticated using either a static token or admin credentials
*/
export async function ensureDirectusAuthenticated(
client: MintelDirectusClient,
) {
const token = process.env.DIRECTUS_API_TOKEN || process.env.DIRECTUS_TOKEN;
const email = process.env.DIRECTUS_ADMIN_EMAIL;
const password = process.env.DIRECTUS_ADMIN_PASSWORD;
if (token) {
client.setToken(token);
return;
}
if (email && password) {
try {
await client.login({ email, password });
} catch (e) {
console.error("Failed to authenticate with Directus:", e);
throw e;
}
}
}

View File

@@ -1,7 +1,6 @@
import createMiddleware from 'next-intl/middleware';
import { getRequestConfig } from 'next-intl/server';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import createMiddleware from "next-intl/middleware";
import { getRequestConfig } from "next-intl/server";
import type { NextRequest } from "next/server";
export interface MintelI18nConfig {
locales: string[];
@@ -25,7 +24,10 @@ export function createMintelMiddleware(config: MintelI18nConfig) {
return intlMiddleware(request);
} catch (error) {
if (config.logRequests) {
console.error(`Request failed: ${request.method} ${request.url}`, error);
console.error(
`Request failed: ${request.method} ${request.url}`,
error,
);
}
throw error;
}
@@ -35,7 +37,7 @@ export function createMintelMiddleware(config: MintelI18nConfig) {
export function createMintelI18nRequestConfig(
locales: string[],
defaultLocale: string,
importMessages: (locale: string) => Promise<any>
importMessages: (locale: string) => Promise<any>,
) {
return getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
@@ -48,19 +50,19 @@ export function createMintelI18nRequestConfig(
locale,
messages: (await importMessages(locale)).default,
onError(error: any) {
if (error.code === 'MISSING_MESSAGE') {
if (error.code === "MISSING_MESSAGE") {
console.error(error.message);
} else {
console.error(error);
}
},
getMessageFallback({ namespace, key, error }: any) {
const path = [namespace, key].filter((part) => part != null).join('.');
if (error.code === 'MISSING_MESSAGE') {
const path = [namespace, key].filter((part) => part != null).join(".");
if (error.code === "MISSING_MESSAGE") {
return path;
}
return 'fallback';
}
return "fallback";
},
};
});
}

View File

@@ -0,0 +1,10 @@
import { describe, it, expect } from "vitest";
import { isValidLang } from "../src/index";
describe("next-utils", () => {
it("should validate languages correctly", () => {
expect(isValidLang("en")).toBe(true);
expect(isValidLang("de")).toBe(true);
expect(isValidLang("fr")).toBe(false);
});
});

View File

@@ -3,7 +3,11 @@ const submissions: Record<string, number> = {};
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
const MAX_SUBMISSIONS_PER_WINDOW = 3;
export async function rateLimit(identifier: string, windowMs = RATE_LIMIT_WINDOW, maxSubmissions = MAX_SUBMISSIONS_PER_WINDOW) {
export async function rateLimit(
identifier: string,
windowMs = RATE_LIMIT_WINDOW,
maxSubmissions = MAX_SUBMISSIONS_PER_WINDOW,
) {
const now = Date.now();
// Clean up old submissions
@@ -15,7 +19,7 @@ export async function rateLimit(identifier: string, windowMs = RATE_LIMIT_WINDOW
// Check if identifier has exceeded submission limit
const currentSubmissions = Object.values(submissions).filter(
(timestamp) => now - timestamp <= windowMs
(timestamp) => now - timestamp <= windowMs,
);
if (currentSubmissions.length >= maxSubmissions) {
@@ -35,3 +39,4 @@ export function isValidLang(lang: string): lang is Lang {
export * from "./i18n";
export * from "./env";
export * from "./directus";