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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/builds.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
31 changes: 31 additions & 0 deletions .github/workflows/lint-format.yaml
Original file line number Diff line number Diff line change
@@ -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
56 changes: 0 additions & 56 deletions .github/workflows/pr-checks.yaml

This file was deleted.

80 changes: 80 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion apps/portfolio/tests/portfolio-contract.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
visible: true,
items: [{ id: "strategy", title: "Product strategy", summary: "Focused product direction." }],
});
expect(renderToStaticMarkup(<SignalTemplate project={content} />)).toContain(

Check failure on line 36 in apps/portfolio/tests/portfolio-contract.test.tsx

View workflow job for this annotation

GitHub Actions / Test Portfolio

tests/portfolio-contract.test.tsx > portfolio content contract > renders visible supporting sections in both launch templates

AssertionError: expected '<div>Mock Signal Template: VeriWorkly…' to contain 'Product strategy' Expected: "Product strategy" Received: "<div>Mock Signal Template: VeriWorkly User</div>" ❯ tests/portfolio-contract.test.tsx:36:72
"Product strategy",
);
expect(renderToStaticMarkup(<AtelierTemplate project={content} />)).toContain(
Expand All @@ -49,7 +49,7 @@
];
const signal = renderToStaticMarkup(<SignalTemplate project={content} />);
const atelier = renderToStaticMarkup(<AtelierTemplate project={content} />);
expect(signal.indexOf("Services first")).toBeLessThan(signal.indexOf("Selected work"));

Check failure on line 52 in apps/portfolio/tests/portfolio-contract.test.tsx

View workflow job for this annotation

GitHub Actions / Test Portfolio

tests/portfolio-contract.test.tsx > portfolio content contract > renders sections in the order selected in the editor

AssertionError: expected -1 to be less than -1 ❯ tests/portfolio-contract.test.tsx:52:46
expect(atelier.indexOf("Services first")).toBeLessThan(
atelier.indexOf("Your strongest project"),
);
Expand All @@ -65,7 +65,7 @@
const markup = renderToStaticMarkup(<SignalTemplate project={content} />);
expect(markup).not.toContain("javascript:");
expect(markup).not.toContain("Unsafe");
expect(markup).toContain("https://example.com/");

Check failure on line 68 in apps/portfolio/tests/portfolio-contract.test.tsx

View workflow job for this annotation

GitHub Actions / Test Portfolio

tests/portfolio-contract.test.tsx > portfolio content contract > does not render unsafe or incomplete social links

AssertionError: expected '<div>Mock Signal Template: VeriWorkly…' to contain 'https://example.com/' Expected: "https://example.com/" Received: "<div>Mock Signal Template: VeriWorkly User</div>" ❯ tests/portfolio-contract.test.tsx:68:20
expect(markup).toContain("Website");
expect(renderToStaticMarkup(<AtelierTemplate project={content} />)).toContain("Website");
});
Expand Down Expand Up @@ -126,11 +126,11 @@
"testimonials",
"awards",
]) {
expect(signal).toContain(`data-section="${section}"`);

Check failure on line 129 in apps/portfolio/tests/portfolio-contract.test.tsx

View workflow job for this annotation

GitHub Actions / Test Portfolio

tests/portfolio-contract.test.tsx > portfolio content contract > renders supporting content with section-specific compositions

AssertionError: expected '<div>Mock Signal Template: Gautam Raj…' to contain 'data-section="experience"' Expected: "data-section="experience"" Received: "<div>Mock Signal Template: Gautam Raj</div>" ❯ tests/portfolio-contract.test.tsx:129:22
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");
});
Expand Down
59 changes: 55 additions & 4 deletions apps/server/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -26,19 +59,38 @@ 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,
},
},

session: {
storeSessionInDatabase: true,
expiresIn: config.auth.sessionTtlSeconds,
updateAge: config.auth.sessionResetTtlOnUse,
cookieCache: {
Expand Down Expand Up @@ -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);
}
}),
},
Expand Down
12 changes: 3 additions & 9 deletions apps/server/src/utils/authCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,15 @@ 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) =>
cookie.startsWith("veriworkly-auth.session_token=") ||
cookie.startsWith("__Secure-veriworkly-auth.session_token="),
);

if (stableSessionCookie) {
return stableSessionCookie;
}
if (stableSessionCookie) return stableSessionCookie;

return authCookies.sort().join(";");
}
Expand All @@ -38,9 +34,7 @@ export function getSessionCacheKey(cookieHeader: string): string | null {
export async function invalidateSessionCache(cookieHeader: string): Promise<void> {
const cacheKey = getSessionCacheKey(cookieHeader);

if (cacheKey) {
await cacheDel(cacheKey);
}
if (cacheKey) await cacheDel(cacheKey);
}

export async function invalidateCacheByToken(token: string): Promise<void> {
Expand Down
Loading