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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ The backend handles the core evaluation logic and integrates with the GCP Pub/Su
### Local Setup & Execution
1. **Configure Environment:**
```bash
cd server
export AES_SECRET_KEY=defaultaessecret
export TOKEN_SIGNING_KEY=default_token_signing_key
export JDBC_DATABASE_URL="jdbc:mysql://localhost:3306/stax_db"
Expand Down Expand Up @@ -74,10 +75,11 @@ A React-based UI built with Mantine and Tailwind CSS for interacting with the St
### Setup & Execution
1. **Install Dependencies:**
```bash
cd frontend
npm install
```
2. **Environment Configuration:**
* Copy `.env.template` to `.env.local`.
* Copy `.env.template` to `.env.local` and set `NEXT_PUBLIC_API_BASE_URL` (e.g. `http://localhost:8080`) and `NEXT_PUBLIC_APP_BASE_URL` (e.g. `http://localhost:3000`).
* Add `NEXT_PUBLIC_GOOGLE_CLIENT_ID` if using authentication.
3. **Run Development Server:**
```bash
Expand All @@ -98,7 +100,8 @@ A React-based UI built with Mantine and Tailwind CSS for interacting with the St
├── terraform/ # Infrastructure as Code (GCP)
│ ├── modules/ # Reusable GCP resource definitions
│ └── quickstart/ # Main deployment entry point
├── backend/ # Spring Boot Java Server
├── server/ # Spring Boot Java Server
│ └── src/ # Evaluation logic and API routes
└── frontend/ # Next.js & React Web App
└── src/ # UI Components and Mantine hooks
├── app/ # App routes and page components
└── components/ # Reusable UI components
17 changes: 17 additions & 0 deletions frontend/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# URL to the Stax Spring Boot server (e.g. http://localhost:8080)
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080

# URL of the Stax Frontend Next.js app (e.g. http://localhost:3000)
NEXT_PUBLIC_APP_BASE_URL=http://localhost:3000

# Google OAuth Client ID for authentication
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-google-client-id

# Set to true to enable Google OAuth authentication (default is false for local development)
NEXT_PUBLIC_AUTH_ENABLED=false

# Optional configurations
NEXT_PUBLIC_FEEDBACK_PRODUCT_ID=
NEXT_PUBLIC_HATS_API_KEY=
NEXT_PUBLIC_HATS_TRIGGER_ID=
NEXT_PUBLIC_GA_TAG_ID=
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ export default function ProjectsTable() {
setAllProjects(res?.["projects"] || []);
setIsLoadingProjects(false);
},
onError: () => {
setIsLoadingProjects(false);
},
});

useEffect(() => {
Expand Down
3 changes: 2 additions & 1 deletion frontend/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

"use client";

import { MainConfig } from "@/config/config";
import { JWT_TOKEN_KEY } from "@/config/constants";
import { routes } from "@/config/routes";
import LocalStorage from "@/utils/LocalStorage";
Expand All @@ -28,7 +29,7 @@ export default function RootPage() {

useEffect(() => {
const hasJwtToken = LocalStorage.get(JWT_TOKEN_KEY);
if (hasJwtToken) {
if (!MainConfig.isAuthEnabled || hasJwtToken) {
router.push(routes.projects);
} else {
router.push(routes.signin);
Expand Down
2 changes: 1 addition & 1 deletion frontend/config/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

export const MainConfig = {
isPlaygroundStreamingEnabled: false, // if you want streaming enabled for pointwise set to true
isAuthEnabled: true, // Enable the authentication and bearer token passing to backend API
isAuthEnabled: process.env.NEXT_PUBLIC_AUTH_ENABLED === "true", // Enable the authentication and bearer token passing to backend API
isPlaygroundAttachmentEnabled: false, // Not fully developed yet
isEvaluatorUndoRedoEnabled: false, // Not fully developed yet
};
89 changes: 35 additions & 54 deletions frontend/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,67 +22,45 @@ import { routes } from "./config/routes";
// Define the strict CSP for production

export function middleware(request: NextRequest) {
// Handle API routes specifically to prevent caching
if (request.nextUrl.pathname.startsWith("/api/")) {
const response = NextResponse.next();
const response = NextResponse.next();

// Set cache control headers for API routes
response.headers.set(
"Cache-Control",
"no-cache, no-store, must-revalidate, private",
);
response.headers.set("Pragma", "no-cache");
response.headers.set("Expires", "0");
response.headers.set("X-Cache-Status", "disabled");
// Set cache control headers for all pages and API routes
response.headers.set(
"Cache-Control",
"no-cache, no-store, must-revalidate, private",
);
response.headers.set("Pragma", "no-cache");
response.headers.set("Expires", "0");
response.headers.set("X-Cache-Status", "disabled");

return response;
}

const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https: http:;
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`;

const isDev = process?.env?.NODE_ENV === "development";
// Replace newline characters and spaces
const contentSecurityPolicyHeaderValue = cspHeader
.replace(/\s{2,}/g, " ")
.trim();

const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
if (!isDev) {
requestHeaders.set(
"Content-Security-Policy",
contentSecurityPolicyHeaderValue,
);
}

const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
if (!isDev) {
response.headers.set(
"Content-Security-Policy",
contentSecurityPolicyHeaderValue,
);
// Optional: only enforce strict CSP when explicitly enabled via env
if (process.env.ENABLE_STRICT_CSP === "true") {
const isHttps =
request.headers.get("x-forwarded-proto") === "https" ||
request.nextUrl.protocol === "https:";
const upgradeInsecure = isHttps ? "upgrade-insecure-requests;" : "";
const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline' https: http:;
style-src 'self' 'unsafe-inline' https: http:;
img-src 'self' blob: data: https:;
font-src 'self' data: https:;
connect-src 'self' https: http: ws: wss:;
object-src 'none';
base-uri 'self';
form-action 'self';
${upgradeInsecure}
`.replace(/\s{2,}/g, " ").trim();
response.headers.set("Content-Security-Policy", cspHeader);
}

// --------- Authorization redirects ---------
const pathname = request.nextUrl.pathname;

if (pathname === routes.root) {
if (process.env.NEXT_PUBLIC_AUTH_ENABLED !== "true") {
return NextResponse.redirect(new URL(routes.projects, request.url));
}
// Add authorization code / error to the query params when user is redirected back to app
let redirectRoute = routes.signin;
if (request.nextUrl.searchParams.get("code")) {
Expand All @@ -96,7 +74,10 @@ export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL(redirectRoute, request.url));
}

// Default case - apply CSP and continue
if (pathname === routes.signin && process.env.NEXT_PUBLIC_AUTH_ENABLED !== "true") {
return NextResponse.redirect(new URL(routes.projects, request.url));
}

return response;
}

Expand Down
13 changes: 13 additions & 0 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ const nextConfig = {
},
];
},
async rewrites() {
const backendUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8080";
return [
{
source: "/api/:path*",
destination: `${backendUrl}/:path*`,
},
{
source: "/streaming/:path*",
destination: `${backendUrl}/streaming/:path*`,
},
];
},
};

module.exports = nextConfig;
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"test:watch": "jest --watch",
"lint:check": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"githooks:init": "cp git-hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit"
"githooks:init": "cp git-hooks/pre-commit ../.git/hooks/pre-commit && chmod +x ../.git/hooks/pre-commit"
},
"dependencies": {
"@codemirror/lang-json": "^6.0.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ private ProjectDTO enrichProjectWithFields(Project project, Set<String> includeF

private List<ProjectDTO> enrichProjectsWithFields(
List<Project> projects, Set<String> includeFields) {
if (projects == null || projects.isEmpty()) {
return List.of();
}
List<String> projectIds = projects.stream().map(Project::getId).toList();
User user = projects.get(0).getUser();

Expand Down
8 changes: 4 additions & 4 deletions terraform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ You can use GCP Cloud Run CLI to build and push the container images for UI serv
export PROJECT_ID=<your project id>
export REGION=us-central1

# From the backend java directory using Dockerfile.
gcloud run deploy stax-ui --project=${PROJECT_ID} --source . --region=${REGION}

# From the UI directory. Make sure you have set all the NEXT_PUBLIC_* environment variables. We use the `gcloud run deploy` to build and push the container image.
# From the backend java directory (server/) using Dockerfile.
gcloud run deploy stax-backend --project=${PROJECT_ID} --source . --region=${REGION}

# From the UI directory (frontend/). Make sure you have set all the NEXT_PUBLIC_* environment variables. We use the `gcloud run deploy` to build and push the container image.
gcloud run deploy stax-ui --project=${PROJECT_ID} --source . --region=${REGION}
```

After these steps, the images will be present in the following locations.
Expand Down
4 changes: 2 additions & 2 deletions terraform/quickstart/cloud_run_backend.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module "cloud_run_backend" {
project_id = local.project_id
region = local.region

cloud_run_image = "${local.region}-docker.pkg.dev/${local.project_id}/stax-backend"
cloud_run_image = "${local.region}-docker.pkg.dev/${local.project_id}/${var.artifact_registry_repo}/stax-backend"
cloud_run_service_name = "stax-backend"
cloud_run_cpu_limit = 4
cloud_run_memory_limit = "16Gi"
Expand Down Expand Up @@ -64,7 +64,7 @@ module "cloud_run_backend" {
},
{
name = "GCP_BUCKET_ID",
value = "stax-prod-project-bucket"
value = var.gcs_bucket_name
},
# This is needed for using Google OAuth authentication. As a prerequisite, configure a Google oauth client.
# TODO: Uncomment the following lines and replace with the real oauth client id here
Expand Down
2 changes: 1 addition & 1 deletion terraform/quickstart/cloud_run_ui.tf
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module "ui_service" {
region = local.region

cloud_run_service_name = "stax-ui"
cloud_run_image = "${local.region}-docker.pkg.dev/${local.project_id}/stax-ui"
cloud_run_image = "${local.region}-docker.pkg.dev/${local.project_id}/${var.artifact_registry_repo}/stax-ui"
cloud_run_cpu_limit = 4
cloud_run_memory_limit = "2Gi"
allow_unauthenticated = true
Expand Down
40 changes: 28 additions & 12 deletions terraform/quickstart/iam.tf
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,54 @@
# limitations under the License.

locals {
# TODO: change this to your email
# member = "user:you@gmail.com"
member = "user:xinyij@google.com"
admin_member = var.admin_email != "" ? (startswith(var.admin_email, "user:") || startswith(var.admin_email, "serviceAccount:") || startswith(var.admin_email, "group:") ? var.admin_email : "user:${var.admin_email}") : null
default_service_account = "serviceAccount:${data.google_project.default_project.number}-compute@developer.gserviceaccount.com"
}

resource "google_project_iam_member" "admin" {
count = local.admin_member != null ? 1 : 0
project = data.google_project.default_project.project_id
role = "roles/admin"

member = local.member
role = "roles/resourcemanager.projectIamAdmin"
member = local.admin_member
}

resource "google_project_iam_binding" "storage_admin" {
resource "google_project_iam_member" "storage_admin_user" {
count = local.admin_member != null ? 1 : 0
project = data.google_project.default_project.project_id
role = "roles/storage.objectAdmin"
member = local.admin_member
}

members = [local.member, "serviceAccount:${local.service_account}"]
resource "google_project_iam_member" "storage_admin_sa" {
project = data.google_project.default_project.project_id
role = "roles/storage.objectAdmin"
member = "serviceAccount:${local.service_account}"
}

resource "google_project_iam_binding" "sql_client" {
resource "google_project_iam_member" "sql_client_user" {
count = local.admin_member != null ? 1 : 0
project = data.google_project.default_project.project_id
role = "roles/cloudsql.client"
member = local.admin_member
}

members = [local.member, "serviceAccount:${local.service_account}"]
resource "google_project_iam_member" "sql_client_sa" {
project = data.google_project.default_project.project_id
role = "roles/cloudsql.client"
member = "serviceAccount:${local.service_account}"
}

resource "google_project_iam_binding" "iam_id_token_creator" {
resource "google_project_iam_member" "iam_id_token_creator_user" {
count = local.admin_member != null ? 1 : 0
project = data.google_project.default_project.project_id
role = "roles/iam.serviceAccountOpenIdTokenCreator"
member = local.admin_member
}

members = [local.member, "serviceAccount:${local.service_account}"]
resource "google_project_iam_member" "iam_id_token_creator_sa" {
project = data.google_project.default_project.project_id
role = "roles/iam.serviceAccountOpenIdTokenCreator"
member = "serviceAccount:${local.service_account}"
}


Expand Down
5 changes: 2 additions & 3 deletions terraform/quickstart/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@
# limitations under the License.

provider "google" {
# TODO: Prerequisite - change this to your project ID.
project = "planck-opensource-test-769621"
project = var.project_id != "" ? var.project_id : null
}

data "google_project" "default_project" {}
data "google_compute_default_service_account" "default" {}

locals {
project_id = data.google_project.default_project.project_id
region = "us-central1"
region = var.region
service_account = data.google_compute_default_service_account.default.email
}
Loading