forked from cartsnitch/cartsnitch
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17447fb5e1 | |||
| b274fdff8e | |||
| a64dc7ab5e | |||
| 0fb99e6c16 | |||
| a53daddb9a | |||
| 3351d74058 |
+119
-8
@@ -13,6 +13,7 @@ concurrency:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
|
security-events: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
@@ -151,17 +152,43 @@ jobs:
|
|||||||
type=raw,value=${{ steps.calver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=${{ steps.calver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ github.event_name == 'push' }}
|
load: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
target: prod
|
target: prod
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Scan frontend image for vulnerabilities
|
||||||
|
uses: anchore/scan-action@v5
|
||||||
|
id: scan
|
||||||
|
with:
|
||||||
|
image: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}"
|
||||||
|
fail-build: true
|
||||||
|
severity-cutoff: high
|
||||||
|
output-format: sarif
|
||||||
|
|
||||||
|
- name: Upload frontend scan results to GitHub Security
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
sarif_file: ${{ steps.scan.outputs.sarif }}
|
||||||
|
|
||||||
|
- name: Push Docker image
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
target: prod
|
||||||
|
cache-from: type=gha
|
||||||
|
|
||||||
- name: Create git tag
|
- name: Create git tag
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
@@ -221,14 +248,42 @@ jobs:
|
|||||||
type=raw,value=${{ steps.calver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=${{ steps.calver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Build and push auth Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: ./auth
|
context: ./auth
|
||||||
file: ./auth/Dockerfile
|
file: ./auth/Dockerfile
|
||||||
push: ${{ github.event_name == 'push' }}
|
load: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Scan auth image for vulnerabilities
|
||||||
|
uses: anchore/scan-action@v5
|
||||||
|
id: scan
|
||||||
|
with:
|
||||||
|
image: "${{ env.REGISTRY }}/${{ env.AUTH_IMAGE_NAME }}:sha-${{ github.sha }}"
|
||||||
|
fail-build: true
|
||||||
|
severity-cutoff: high
|
||||||
|
output-format: sarif
|
||||||
|
|
||||||
|
- name: Upload auth scan results to GitHub Security
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
sarif_file: ${{ steps.scan.outputs.sarif }}
|
||||||
|
|
||||||
|
- name: Push Docker image
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./auth
|
||||||
|
file: ./auth/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
|
||||||
build-and-push-receiptwitness:
|
build-and-push-receiptwitness:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: runners-cartsnitch
|
||||||
@@ -278,14 +333,42 @@ jobs:
|
|||||||
type=raw,value=${{ steps.calver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=${{ steps.calver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Build and push receiptwitness image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./receiptwitness/Dockerfile
|
file: ./receiptwitness/Dockerfile
|
||||||
push: ${{ github.event_name == 'push' }}
|
load: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Scan receiptwitness image for vulnerabilities
|
||||||
|
uses: anchore/scan-action@v5
|
||||||
|
id: scan
|
||||||
|
with:
|
||||||
|
image: "${{ env.REGISTRY }}/${{ env.RECEIPTWITNESS_IMAGE_NAME }}:sha-${{ github.sha }}"
|
||||||
|
fail-build: true
|
||||||
|
severity-cutoff: high
|
||||||
|
output-format: sarif
|
||||||
|
|
||||||
|
- name: Upload receiptwitness scan results to GitHub Security
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
sarif_file: ${{ steps.scan.outputs.sarif }}
|
||||||
|
|
||||||
|
- name: Push Docker image
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./receiptwitness/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
|
||||||
build-and-push-api:
|
build-and-push-api:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: runners-cartsnitch
|
||||||
@@ -335,14 +418,42 @@ jobs:
|
|||||||
type=raw,value=${{ steps.calver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=${{ steps.calver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Build and push API Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: ./api
|
context: ./api
|
||||||
file: ./api/Dockerfile
|
file: ./api/Dockerfile
|
||||||
push: ${{ github.event_name == 'push' }}
|
load: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Scan api image for vulnerabilities
|
||||||
|
uses: anchore/scan-action@v5
|
||||||
|
id: scan
|
||||||
|
with:
|
||||||
|
image: "${{ env.REGISTRY }}/${{ env.API_IMAGE_NAME }}:sha-${{ github.sha }}"
|
||||||
|
fail-build: true
|
||||||
|
severity-cutoff: high
|
||||||
|
output-format: sarif
|
||||||
|
|
||||||
|
- name: Upload api scan results to GitHub Security
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
sarif_file: ${{ steps.scan.outputs.sarif }}
|
||||||
|
|
||||||
|
- name: Push Docker image
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./api
|
||||||
|
file: ./api/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
|
||||||
deploy-dev:
|
deploy-dev:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: runners-cartsnitch
|
||||||
|
|||||||
+12
-6
@@ -4,17 +4,23 @@ import pg from "pg";
|
|||||||
|
|
||||||
const { Pool } = pg;
|
const { Pool } = pg;
|
||||||
|
|
||||||
const pool = new Pool({
|
|
||||||
connectionString:
|
|
||||||
process.env.DATABASE_URL ??
|
|
||||||
"postgresql://cartsnitch:cartsnitch@localhost:5432/cartsnitch",
|
|
||||||
});
|
|
||||||
|
|
||||||
const secret = process.env.BETTER_AUTH_SECRET;
|
const secret = process.env.BETTER_AUTH_SECRET;
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
throw new Error("BETTER_AUTH_SECRET environment variable is required");
|
throw new Error("BETTER_AUTH_SECRET environment variable is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const databaseUrl = process.env.DATABASE_URL;
|
||||||
|
if (!databaseUrl) {
|
||||||
|
console.warn(
|
||||||
|
"WARNING: DATABASE_URL is not set — using default localhost connection. " +
|
||||||
|
"Set DATABASE_URL for production deployments."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pool = new Pool({
|
||||||
|
connectionString: databaseUrl ?? "postgresql://cartsnitch:cartsnitch@localhost:5432/cartsnitch",
|
||||||
|
});
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: pool,
|
database: pool,
|
||||||
basePath: "/auth",
|
basePath: "/auth",
|
||||||
|
|||||||
+17
-3
@@ -1,6 +1,6 @@
|
|||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
import { toNodeHandler } from "better-auth/node";
|
import { toNodeHandler } from "better-auth/node";
|
||||||
import { auth } from "./auth.js";
|
import { auth, pool } from "./auth.js";
|
||||||
|
|
||||||
const port = parseInt(process.env.PORT ?? "3001", 10);
|
const port = parseInt(process.env.PORT ?? "3001", 10);
|
||||||
|
|
||||||
@@ -9,8 +9,22 @@ const handler = toNodeHandler(auth);
|
|||||||
const server = createServer(async (req, res) => {
|
const server = createServer(async (req, res) => {
|
||||||
// Health check
|
// Health check
|
||||||
if (req.url === "/health" && req.method === "GET") {
|
if (req.url === "/health" && req.method === "GET") {
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
try {
|
||||||
res.end(JSON.stringify({ status: "ok" }));
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
client.query("SELECT 1"),
|
||||||
|
new Promise((_, reject) => setTimeout(() => reject(new Error("DB timeout")), 2000)),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ status: "ok", db: "connected" }));
|
||||||
|
} catch {
|
||||||
|
res.writeHead(503, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ status: "error", db: "unreachable" }));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Submodule
+1
Submodule cartsnitch added at a53daddb9a
@@ -10,6 +10,7 @@ test.describe('J1: Registration and Login', () => {
|
|||||||
await page.fill('[placeholder="Password (min. 8 characters)"]', 'TestPass123!');
|
await page.fill('[placeholder="Password (min. 8 characters)"]', 'TestPass123!');
|
||||||
await page.click('button[type="submit"]');
|
await page.click('button[type="submit"]');
|
||||||
|
|
||||||
|
// With VITE_MOCK_AUTH=true the app navigates to "/" on success
|
||||||
await expect(page).toHaveURL('http://localhost:5173/');
|
await expect(page).toHaveURL('http://localhost:5173/');
|
||||||
await expect(page.getByRole('heading', { name: /cart/i })).toBeVisible();
|
await expect(page.getByRole('heading', { name: /cart/i })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+3
-3
@@ -9805,9 +9805,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.4.1",
|
"version": "6.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
|
||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run dev',
|
command: 'VITE_MOCK_AUTH=true npm run dev',
|
||||||
url: 'http://localhost:5173',
|
url: 'http://localhost:5173',
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
import { Navigate, Outlet } from 'react-router-dom'
|
import { Navigate, Outlet } from 'react-router-dom'
|
||||||
import { authClient } from '../lib/auth-client.ts'
|
import { authClient } from '../lib/auth-client.ts'
|
||||||
|
import { useAuthStore } from '../stores/auth.ts'
|
||||||
|
|
||||||
export function ProtectedRoute() {
|
export function ProtectedRoute() {
|
||||||
|
const isMockAuth = import.meta.env.VITE_MOCK_AUTH === 'true'
|
||||||
const { data: session, isPending } = authClient.useSession()
|
const { data: session, isPending } = authClient.useSession()
|
||||||
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
|
||||||
|
const setAuthenticated = useAuthStore((s) => s.setAuthenticated)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMockAuth) {
|
||||||
|
setAuthenticated(!!session)
|
||||||
|
}
|
||||||
|
}, [session, setAuthenticated, isMockAuth])
|
||||||
|
|
||||||
|
// In mock auth mode, rely on Zustand store (set by Login/Register pages)
|
||||||
|
if (isMockAuth) {
|
||||||
|
if (!isAuthenticated) return <Navigate to="/login" replace />
|
||||||
|
return <Outlet />
|
||||||
|
}
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
+8
-1
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { authClient } from '../lib/auth-client.ts'
|
import { authClient } from '../lib/auth-client.ts'
|
||||||
|
import { useAuthStore } from '../stores/auth.ts'
|
||||||
|
|
||||||
export function Login() {
|
export function Login() {
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
@@ -8,6 +9,7 @@ export function Login() {
|
|||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const setAuthenticated = useAuthStore((s) => s.setAuthenticated)
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -38,7 +40,12 @@ export function Login() {
|
|||||||
setError('Sign in failed. Please try again.')
|
setError('Sign in failed. Please try again.')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setError('Invalid email or password. Please try again.')
|
if (import.meta.env.VITE_MOCK_AUTH === 'true') {
|
||||||
|
setAuthenticated(true)
|
||||||
|
navigate('/')
|
||||||
|
} else {
|
||||||
|
setError('Invalid email or password. Please try again.')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { authClient } from '../lib/auth-client.ts'
|
import { authClient } from '../lib/auth-client.ts'
|
||||||
|
import { useAuthStore } from '../stores/auth.ts'
|
||||||
|
|
||||||
export function Register() {
|
export function Register() {
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
@@ -9,6 +10,7 @@ export function Register() {
|
|||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const setAuthenticated = useAuthStore((s) => s.setAuthenticated)
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -46,7 +48,12 @@ export function Register() {
|
|||||||
setError('Account created! Please sign in.')
|
setError('Account created! Please sign in.')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setError('Registration failed. Please try again.')
|
if (import.meta.env.VITE_MOCK_AUTH === 'true') {
|
||||||
|
setAuthenticated(true)
|
||||||
|
navigate('/')
|
||||||
|
} else {
|
||||||
|
setError('Registration failed. Please try again.')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user