From 0c4657885ad1d89c1124a908a161d32c9fcabd99 Mon Sep 17 00:00:00 2001 From: Gautam25Raj Date: Fri, 26 Jun 2026 03:21:07 +0530 Subject: [PATCH] feat: implement auth caching with Redis and add build/test workflows --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 1 + .github/workflows/builds.yaml | 52 ++++++++++++ .github/workflows/lint-format.yaml | 31 +++++++ .github/workflows/pr-checks.yaml | 56 ------------- .github/workflows/tests.yaml | 80 +++++++++++++++++++ .../tests/portfolio-contract.test.tsx | 2 +- apps/server/src/auth/index.ts | 59 +++++++++++++- apps/server/src/utils/authCache.ts | 12 +-- 9 files changed, 224 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/builds.yaml create mode 100644 .github/workflows/lint-format.yaml delete mode 100644 .github/workflows/pr-checks.yaml create mode 100644 .github/workflows/tests.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a9617a52..87ba317e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -17,10 +17,10 @@ body: - "Site (apps/site)" - "Studio (apps/studio)" - "Server (apps/server)" + - "Portfolio (apps/portfolio)" - "Docs Platform (apps/docs-platform)" - "Blog Platform (apps/blog-platform)" - "UI Library (packages/ui)" - - "API Client (packages/api-client)" - "Infrastructure / Other" validations: required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index a176fca0..0180ef3e 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -13,6 +13,7 @@ body: - "Site (apps/site)" - "Studio (apps/studio)" - "Server (apps/server)" + - "Portfolio (apps/portfolio)" - "Docs Platform (apps/docs-platform)" - "Blog Platform (apps/blog-platform)" - "UI Library (packages/ui)" diff --git a/.github/workflows/builds.yaml b/.github/workflows/builds.yaml new file mode 100644 index 00000000..3ef54007 --- /dev/null +++ b/.github/workflows/builds.yaml @@ -0,0 +1,52 @@ +name: Build Apps + +on: + pull_request: + branches: [main, master, develop] + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + NEXT_PUBLIC_BACKEND_URL: "http://localhost:8080/api/v1" + BACKEND_INTERNAL_URL: "http://localhost:8080/api/v1" + SITE_URL: "http://localhost:3000" + AUTH_SECRET: "dummy-secret-for-build-purposes-only" + AUTH_BASE_URL: "http://localhost:8080" + JWT_SECRET: "dummy-jwt-secret" + DATABASE_URL: "postgresql://dummy:dummy@localhost:5432/dummy" + +jobs: + build: + name: Build (${{ matrix.workspace }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + workspace: + - "@veriworkly/site" + - "@veriworkly/studio" + - "@veriworkly/portfolio" + - "@veriworkly/blog-platform" + - "@veriworkly/docs-platform" + - "@veriworkly/server" + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + + - name: Install Dependencies + run: npm ci --legacy-peer-deps + + - name: Prisma Generate + run: npm exec -w @veriworkly/server -- prisma generate --schema=prisma/schema.prisma + + - name: Mock Portfolio Template Library + run: node scripts/mock-template-library.mjs + + - name: Build Application + run: npm run build -w ${{ matrix.workspace }} diff --git a/.github/workflows/lint-format.yaml b/.github/workflows/lint-format.yaml new file mode 100644 index 00000000..5bf26ab2 --- /dev/null +++ b/.github/workflows/lint-format.yaml @@ -0,0 +1,31 @@ +name: Lint & Format + +on: + pull_request: + branches: [main, master, develop] + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + lint-format: + name: Lint & Format Check + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + + - name: Install Dependencies + run: npm ci --legacy-peer-deps + + - name: Run Linting & Formatting + run: | + npm run lint + npm run format diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml deleted file mode 100644 index e395bf37..00000000 --- a/.github/workflows/pr-checks.yaml +++ /dev/null @@ -1,56 +0,0 @@ -name: PR Checks - -on: - pull_request: - branches: [main, master, develop] - workflow_dispatch: - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true # Future-proofs actions against Node 20 deprecation - -jobs: - quality: - name: Quality & Testing - runs-on: ubuntu-latest - env: - NEXT_PUBLIC_BACKEND_URL: "http://localhost:8080/api/v1" - BACKEND_INTERNAL_URL: "http://localhost:8080/api/v1" - SITE_URL: "http://localhost:3000" - AUTH_SECRET: "dummy-secret-for-build-purposes-only" - AUTH_BASE_URL: "http://localhost:8080" - JWT_SECRET: "dummy-jwt-secret" - DATABASE_URL: "postgresql://dummy:dummy@localhost:5432/dummy" - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "npm" - - - name: Install Dependencies - run: npm ci --legacy-peer-deps - - - name: Mock Portfolio Template Library for CI - run: node scripts/mock-template-library.mjs - - - name: Linting & Formatting - run: | - npm run lint - npm run format - - - name: Prisma Generate - run: npm exec -w @veriworkly/server -- prisma generate --schema=prisma/schema.prisma - env: - DATABASE_URL: "postgresql://dummy:dummy@localhost:5432/dummy" - - - name: Run Backend Tests - run: npm run test -w @veriworkly/server --if-present - - - name: Run Studio Tests - run: npm run test:contracts -w @veriworkly/studio --if-present - - - name: Build All Applications - run: npm run build diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 00000000..916a6875 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,80 @@ +name: Run Tests + +on: + pull_request: + branches: [main, master, develop] + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + NEXT_PUBLIC_BACKEND_URL: "http://localhost:8080/api/v1" + BACKEND_INTERNAL_URL: "http://localhost:8080/api/v1" + SITE_URL: "http://localhost:3000" + AUTH_SECRET: "dummy-secret-for-build-purposes-only" + AUTH_BASE_URL: "http://localhost:8080" + JWT_SECRET: "dummy-jwt-secret" + DATABASE_URL: "postgresql://dummy:dummy@localhost:5432/dummy" + +jobs: + test-server: + name: Test Server + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + + - name: Install Dependencies + run: npm ci --legacy-peer-deps + + - name: Prisma Generate + run: npm exec -w @veriworkly/server -- prisma generate --schema=prisma/schema.prisma + + - name: Run Backend Tests + run: npm run test -w @veriworkly/server --if-present + + test-studio: + name: Test Studio + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + + - name: Install Dependencies + run: npm ci --legacy-peer-deps + + - name: Run Studio Tests + run: npm run test:contracts -w @veriworkly/studio --if-present + + test-portfolio: + name: Test Portfolio + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + + - name: Install Dependencies + run: npm ci --legacy-peer-deps + + - name: Mock Portfolio Template Library + run: node scripts/mock-template-library.mjs + + - name: Run Portfolio Tests + run: npm run test -w @veriworkly/portfolio --if-present diff --git a/apps/portfolio/tests/portfolio-contract.test.tsx b/apps/portfolio/tests/portfolio-contract.test.tsx index 1da67755..a4e357cd 100644 --- a/apps/portfolio/tests/portfolio-contract.test.tsx +++ b/apps/portfolio/tests/portfolio-contract.test.tsx @@ -130,7 +130,7 @@ describe("portfolio content contract", () => { expect(atelier).toContain(`data-section="${section}"`); } expect(signal).toContain("signal-timeline"); - expect(signal).toContain("signal-quote-grid"); + expect(signal).toContain("signal-quotes-grid"); expect(atelier).toContain("atelier-service-list"); expect(atelier).toContain("atelier-testimonial-list"); }); diff --git a/apps/server/src/auth/index.ts b/apps/server/src/auth/index.ts index e5f0049f..f34db622 100644 --- a/apps/server/src/auth/index.ts +++ b/apps/server/src/auth/index.ts @@ -8,6 +8,7 @@ import { toNodeHandler, fromNodeHeaders } from "better-auth/node"; import { config } from "#config"; import { prisma } from "#utils/prisma"; +import { getRedis } from "#utils/redis"; import { sendAuthOtpEmail } from "#auth/mailer"; import { createAuthMiddleware } from "better-auth/api"; @@ -18,6 +19,38 @@ export const auth = betterAuth({ provider: "postgresql", }), + secondaryStorage: { + get: async (key) => { + try { + const redis = getRedis(); + return (await redis.get(key)) || null; + } catch (error) { + console.error("better-auth secondaryStorage get error:", error); + return null; + } + }, + + set: async (key, value, ttl) => { + try { + const redis = getRedis(); + + if (ttl) await redis.set(key, value, { EX: ttl }); + else await redis.set(key, value); + } catch (error) { + console.error("better-auth secondaryStorage set error:", error); + } + }, + + delete: async (key) => { + try { + const redis = getRedis(); + await redis.del(key); + } catch (error) { + console.error("better-auth secondaryStorage delete error:", error); + } + }, + }, + appName: "Veriworkly", secret: config.auth.secret, @@ -26,12 +59,30 @@ export const auth = betterAuth({ basePath: "/api/v1/auth", trustedOrigins: config.allowedOrigins, + rateLimit: { + enabled: true, + window: Math.max(1, Math.ceil(config.rateLimit.authWindowMs / 1000)), + max: config.rateLimit.authMaxRequests, + storage: "secondary-storage", + customRules: { + "/email-otp/send-verification-otp": { + window: 60, + max: 3, + }, + "/email-otp/verify-otp": { + window: 60, + max: 5, + }, + }, + }, + advanced: { trustedProxyHeaders: true, + cookiePrefix: "veriworkly-auth", + useSecureCookies: config.nodeEnv === "production", ipAddress: { ipAddressHeaders: config.auth.ipAddressHeaders, }, - cookiePrefix: "veriworkly-auth", crossSubDomainCookies: { enabled: !!config.auth.cookieDomain, domain: config.auth.cookieDomain, @@ -39,6 +90,7 @@ export const auth = betterAuth({ }, session: { + storeSessionInDatabase: true, expiresIn: config.auth.sessionTtlSeconds, updateAge: config.auth.sessionResetTtlOnUse, cookieCache: { @@ -77,9 +129,8 @@ export const auth = betterAuth({ if (shouldInvalidate) { const cookieHeader = ctx.headers?.get("cookie") || ""; - if (cookieHeader) { - await invalidateSessionCache(cookieHeader); - } + + if (cookieHeader) await invalidateSessionCache(cookieHeader); } }), }, diff --git a/apps/server/src/utils/authCache.ts b/apps/server/src/utils/authCache.ts index 09a3dc70..e0e71cbf 100644 --- a/apps/server/src/utils/authCache.ts +++ b/apps/server/src/utils/authCache.ts @@ -9,9 +9,7 @@ export function extractStableAuthCookieFingerprint(cookieHeader: string): string .filter(Boolean) .filter((c) => c.includes("veriworkly-auth")); - if (!authCookies.length) { - return null; - } + if (!authCookies.length) return null; const stableSessionCookie = authCookies.find( (cookie) => @@ -19,9 +17,7 @@ export function extractStableAuthCookieFingerprint(cookieHeader: string): string cookie.startsWith("__Secure-veriworkly-auth.session_token="), ); - if (stableSessionCookie) { - return stableSessionCookie; - } + if (stableSessionCookie) return stableSessionCookie; return authCookies.sort().join(";"); } @@ -38,9 +34,7 @@ export function getSessionCacheKey(cookieHeader: string): string | null { export async function invalidateSessionCache(cookieHeader: string): Promise { const cacheKey = getSessionCacheKey(cookieHeader); - if (cacheKey) { - await cacheDel(cacheKey); - } + if (cacheKey) await cacheDel(cacheKey); } export async function invalidateCacheByToken(token: string): Promise {