diff --git a/.env.template b/.env.template index 5c04089..c64f76b 100644 --- a/.env.template +++ b/.env.template @@ -1,6 +1,6 @@ # -------------- GENERAL CONFIGURATION -------------- -BASE_PATH=/space # OPTIONAL: Base URL path for the application. If not provided, SPACE will use "/" as default value. +BASE_PATH=/space # OPTIONAL: Base URL path for the application. If not provided, SPACE will use "" as default value. DO NOT USE "/" as BASE_PATH, use an empty string instead. ADMIN_USER=admin # OPTIONAL: Username for the default admin user. If not provided, SPACE will use "admin" as default value. ADMIN_PASSWORD=space4all # OPTIONAL: Password for the default admin user. If not provided, SPACE will use "space4all" as default value. ALLOWED_ORIGINS=http://localhost:3000 # OPTIONAL: Semicolon-separated list of allowed origins for CORS. If not provided, it defaults to "*". @@ -35,4 +35,4 @@ JWT_EXPIRATION=1h # OPTIONAL: JWT expiration time # -------------- FRONTEND CONFIGURATION -------------- ENVIRONMENT=production # OPTIONAL: Environment configuration to be used in the frontend. If not provided, it defaults to "production". -SPACE_HOST=http://localhost:5403 # OPTIONAL: Host URL where the SPACE API is running. If not provided, it defaults to "http://localhost:5403". Change this if your API is running in a different host or port, or if it's behind a proxy. +SPACE_HOST=http://localhost:5403 # OPTIONAL: Host URL where the SPACE API is running. If not provided, it defaults to "http://localhost:5403". Change this if your API is running in a different host or port, or if it's behind a proxy. DO NOT INCLUDE trailing slash. diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 007514b..65ae528 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -24,7 +24,6 @@ jobs: envkey_ENVIRONMENT: "testing" envkey_DATABASE_NAME: ${{ vars.CI_MONGO_INITDB_DATABASE }} envkey_MONGO_URI: mongodb://localhost:27017/space_testing_db?authSource=space_testing_db - envkey_BASE_URL_PATH: "/api/v1" envkey_ADMIN_USER: "admin" envkey_ADMIN_PASSWORD: "4dm1n" envkey_REDIS_URL: "redis://localhost:6379" diff --git a/api/.env.template b/api/.env.template index 1720c63..6bb4e19 100644 --- a/api/.env.template +++ b/api/.env.template @@ -1,7 +1,5 @@ # ---------- SERVER CONFIGURATION ---------- -BASE_URL_PATH= # OPTIONAL: Base URL path for the application. If not provided, SPACE will use "/api/v1" as default value. - ALLOWED_ORIGINS= # OPTIONAL: Semicolon-separated list of allowed origins for CORS. If not provided, it defaults to "*". # ---------- DATABASE CONFIGURATION (MongoDB) ---------- diff --git a/api/src/main/app.ts b/api/src/main/app.ts index 5cbe5d5..134e427 100644 --- a/api/src/main/app.ts +++ b/api/src/main/app.ts @@ -55,10 +55,10 @@ const initializeServer = async ( container.resolve('eventService').initialize(server); console.log( - ` ${green}➜${reset} ${bold}API:${reset} ${blue}http://localhost${addressInfo.port !== 80 ? `:${bold}${addressInfo.port}${reset}${process.env.BASE_URL_PATH || ''}` : `${process.env.BASE_URL_PATH || ''}`}` + ` ${green}➜${reset} ${bold}API:${reset} ${blue}http://localhost${addressInfo.port !== 80 ? `:${bold}${addressInfo.port}${reset}` : ''}` ); console.log( - ` ${green}➜${reset} ${bold}WebSockets:${reset} ${blue}ws://localhost${addressInfo.port !== 80 ? `:${bold}${addressInfo.port}${reset}${process.env.BASE_URL_PATH || ''}/events/pricings` : `${process.env.BASE_URL_PATH || ''}/events/pricings`}` + ` ${green}➜${reset} ${bold}WebSockets:${reset} ${blue}ws://localhost${addressInfo.port !== 80 ? `:${bold}${addressInfo.port}${reset}/events/pricings` : '/events/pricings'}` ); if (['development', 'testing'].includes(process.env.ENVIRONMENT ?? '')) { diff --git a/api/src/main/middlewares/AnalyticsMiddleware.ts b/api/src/main/middlewares/AnalyticsMiddleware.ts index 9e792f5..52c3a91 100644 --- a/api/src/main/middlewares/AnalyticsMiddleware.ts +++ b/api/src/main/middlewares/AnalyticsMiddleware.ts @@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from 'express'; import container from '../config/container'; export const analyticsTrackerMiddleware = (req: Request, res: Response, next: NextFunction) => { - const baseUrl = (process.env.BASE_URL_PATH ?? "") + '/api/v1'; + const baseUrl = '/api/v1'; const analyticsService = container.resolve('analyticsService'); // Check if the current route is public (doesn't require authentication) diff --git a/api/src/main/middlewares/AuthMiddleware.ts b/api/src/main/middlewares/AuthMiddleware.ts index a5d9332..687c108 100644 --- a/api/src/main/middlewares/AuthMiddleware.ts +++ b/api/src/main/middlewares/AuthMiddleware.ts @@ -97,7 +97,7 @@ async function authenticateOrgApiKey(req: Request, apiKey: string): Promise { try { const method = req.method.toUpperCase() as HttpMethod; - const baseUrlPath = (process.env.BASE_URL_PATH ?? "") + '/api/v1'; + const baseUrlPath = '/api/v1'; const apiPath = extractApiPath(req.path, baseUrlPath); // Find matching permission rule diff --git a/api/src/main/routes/AnalyticsRoutes.ts b/api/src/main/routes/AnalyticsRoutes.ts index eca0e06..9772e27 100644 --- a/api/src/main/routes/AnalyticsRoutes.ts +++ b/api/src/main/routes/AnalyticsRoutes.ts @@ -4,7 +4,7 @@ import AnalyticsController from '../controllers/AnalyticsController'; const loadFileRoutes = function (app: express.Application) { const analyticsController = new AnalyticsController(); - const baseUrl = (process.env.BASE_URL_PATH ?? "") + '/api/v1'; + const baseUrl = '/api/v1'; app .route(baseUrl + '/analytics/api-calls') diff --git a/api/src/main/routes/CacheRoutes.ts b/api/src/main/routes/CacheRoutes.ts index 75c04bd..71c4e92 100644 --- a/api/src/main/routes/CacheRoutes.ts +++ b/api/src/main/routes/CacheRoutes.ts @@ -6,7 +6,7 @@ import CacheController from '../controllers/CacheController'; const loadFileRoutes = function (app: express.Application) { const cacheController = new CacheController(); - const baseUrl = (process.env.BASE_URL_PATH ?? "") + '/api/v1'; + const baseUrl = '/api/v1'; app .route(baseUrl + '/cache/get') diff --git a/api/src/main/routes/ContractRoutes.ts b/api/src/main/routes/ContractRoutes.ts index 7ff31de..2693c98 100644 --- a/api/src/main/routes/ContractRoutes.ts +++ b/api/src/main/routes/ContractRoutes.ts @@ -8,7 +8,7 @@ import { hasPermission, memberRole } from '../middlewares/AuthMiddleware'; const loadFileRoutes = function (app: express.Application) { const contractController = new ContractController(); - const baseUrl = (process.env.BASE_URL_PATH ?? "") + '/api/v1'; + const baseUrl = '/api/v1'; app .route(baseUrl + '/organizations/:organizationId/contracts') diff --git a/api/src/main/routes/EventRoutes.ts b/api/src/main/routes/EventRoutes.ts index 3a9756f..9c04fb8 100644 --- a/api/src/main/routes/EventRoutes.ts +++ b/api/src/main/routes/EventRoutes.ts @@ -4,7 +4,7 @@ import EventController from '../controllers/EventController'; const loadFileRoutes = (app: express.Application) => { const eventController = new EventController(); - const baseUrl = (process.env.BASE_URL_PATH ?? "") + '/api/v1'; + const baseUrl = '/api/v1'; // This route can be used to check the status of the event service app diff --git a/api/src/main/routes/FeatureEvaluationRoutes.ts b/api/src/main/routes/FeatureEvaluationRoutes.ts index 726bc55..524d92f 100644 --- a/api/src/main/routes/FeatureEvaluationRoutes.ts +++ b/api/src/main/routes/FeatureEvaluationRoutes.ts @@ -7,7 +7,7 @@ import { handleValidation } from '../middlewares/ValidationHandlingMiddleware'; const loadFileRoutes = function (app: express.Application) { const featureEvaluationController = new FeatureEvaluationController(); - const baseUrl = (process.env.BASE_URL_PATH ?? "") + '/api/v1'; + const baseUrl = '/api/v1'; app .route(baseUrl + '/features') diff --git a/api/src/main/routes/HealthcheckRoutes.ts b/api/src/main/routes/HealthcheckRoutes.ts index efb1a19..c4a92d0 100644 --- a/api/src/main/routes/HealthcheckRoutes.ts +++ b/api/src/main/routes/HealthcheckRoutes.ts @@ -1,7 +1,7 @@ import express from 'express'; const loadFileRoutes = function (app: express.Application) { - const baseUrl = (process.env.BASE_URL_PATH ?? "") + '/api/v1'; + const baseUrl = '/api/v1'; // Public route for authentication (does not require API Key) app diff --git a/api/src/main/routes/OrganizationRoutes.ts b/api/src/main/routes/OrganizationRoutes.ts index 86d9765..db6e9ad 100644 --- a/api/src/main/routes/OrganizationRoutes.ts +++ b/api/src/main/routes/OrganizationRoutes.ts @@ -9,7 +9,7 @@ import { memberRole } from '../middlewares/AuthMiddleware'; const loadFileRoutes = function (app: express.Application) { const organizationController = new OrganizationController(); - const baseUrl = (process.env.BASE_URL_PATH ?? "") + '/api/v1'; + const baseUrl = '/api/v1'; // Public route for authentication (does not require API Key) app diff --git a/api/src/main/routes/ServiceRoutes.ts b/api/src/main/routes/ServiceRoutes.ts index 115c426..c7fb130 100644 --- a/api/src/main/routes/ServiceRoutes.ts +++ b/api/src/main/routes/ServiceRoutes.ts @@ -11,7 +11,7 @@ const loadFileRoutes = function (app: express.Application) { const serviceController = new ServiceController(); const upload = handlePricingUpload(['pricing'], './public/static/pricings/uploaded'); - const baseUrl = (process.env.BASE_URL_PATH ?? "") + '/api/v1'; + const baseUrl = '/api/v1'; // ============================================ // Organization-scoped routes (User API Keys) diff --git a/api/src/main/routes/UserRoutes.ts b/api/src/main/routes/UserRoutes.ts index a3f464b..7e9e640 100644 --- a/api/src/main/routes/UserRoutes.ts +++ b/api/src/main/routes/UserRoutes.ts @@ -7,7 +7,7 @@ import { handleValidation } from '../middlewares/ValidationHandlingMiddleware'; const loadFileRoutes = function (app: express.Application) { const userController = new UserController(); - const baseUrl = (process.env.BASE_URL_PATH ?? "") + '/api/v1'; + const baseUrl = '/api/v1'; // Public route for authentication (does not require API Key) app diff --git a/api/src/test/utils/testApp.ts b/api/src/test/utils/testApp.ts index 4378db9..27ecc11 100644 --- a/api/src/test/utils/testApp.ts +++ b/api/src/test/utils/testApp.ts @@ -8,7 +8,7 @@ dotenv.config(); let testServer: Server | null = null; let testApp: Application | null = null; -const baseUrl = (process.env.BASE_URL_PATH ?? "") + '/api/v1'; +const baseUrl = '/api/v1'; const getApp = async (): Promise => { if (!testServer) { diff --git a/docker-compose.yml b/docker-compose.yml index a7d790b..c539156 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,7 +44,6 @@ services: ENVIRONMENT: ${ENVIRONMENT:-production} ADMIN_USER: ${ADMIN_USER:-admin} ADMIN_PASSWORD: ${ADMIN_PASSWORD:-space4all} - BASE_URL_PATH: ${BASE_PATH:-} MONGO_URI: ${MONGO_URI:-mongodb://${DATABASE_USERNAME:-mongoUser}:${DATABASE_PASSWORD:-mongoPassword}@mongodb:27017/${DATABASE_NAME:-space_db}} REDIS_URL: ${REDIS_URL:-redis://redis:6379} JWT_SECRET: ${JWT_SECRET:-test_secret} @@ -72,8 +71,8 @@ services: dockerfile: ./docker/Dockerfile args: VITE_ENVIRONMENT: ${ENVIRONMENT:-production} - VITE_SPACE_BASE_URL: ${SPACE_HOST:-http://localhost:5403}${BASE_PATH:-}/api/v1 # Change to http://localhost/api/v1 if running SPACE in kubernetes - VITE_FRONTEND_BASE_PATH: ${BASE_PATH:-} + VITE_BASE_PATH: ${BASE_PATH:-} + VITE_SPACE_BASE_URL: ${SPACE_HOST:-http://localhost:5403}${BASE_PATH:-}/api/v1 depends_on: space-server: condition: service_healthy diff --git a/frontend/.env.example b/frontend/.env.example index ac0addc..fe41a4e 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,4 +1,3 @@ VITE_ENVIRONMENT="development|production" VITE_SPACE_BASE_URL=base_url_of_the_space_server -VITE_FRONTEND_BASE_PATH=base_path_for_frontend # WITHOUT TRAILING SLASH, e.g. /space VITE_SPACE_ADMIN_API_KEY=your_space_api_key # Only work when using development environment \ No newline at end of file diff --git a/frontend/docker/Dockerfile b/frontend/docker/Dockerfile index dec6da9..12a1915 100644 --- a/frontend/docker/Dockerfile +++ b/frontend/docker/Dockerfile @@ -27,11 +27,10 @@ COPY ./vite.config.ts . COPY ./index.html . ARG VITE_ENVIRONMENT -ARG VITE_FRONTEND_BASE_PATH="/" +ARG VITE_BASE_PATH="/" ARG VITE_SPACE_BASE_URL -ENV VITE_FRONTEND_BASE_PATH=${VITE_FRONTEND_BASE_PATH} - +ENV BASE_PATH=${VITE_BASE_PATH} # Build your application RUN pnpm run build @@ -39,18 +38,18 @@ RUN pnpm add -D serve # Conditional Reorganization Block: # This only runs if VITE_FRONTEND_BASE_PATH is defined and is NOT equal to "/" -RUN if [ -n "$VITE_FRONTEND_BASE_PATH" ] && [ "$VITE_FRONTEND_BASE_PATH" != "/" ]; then \ - # Extract the directory name (e.g., from "/space/" to "space") to exclude it from the find command - FOLDER_NAME=$(echo "$VITE_FRONTEND_BASE_PATH" | sed 's/\///g'); \ - echo "Reorganizing dist for subpath: $VITE_FRONTEND_BASE_PATH"; \ - # Create the nested directory structure - mkdir -p ./dist${VITE_FRONTEND_BASE_PATH} && \ - # Move everything EXCEPT index.html and the newly created subfolder into the subfolder - find ./dist -mindepth 1 -maxdepth 1 ! -name "index.html" ! -name "$FOLDER_NAME" \ - -exec mv -t ./dist${VITE_FRONTEND_BASE_PATH}/ {} +; \ - else \ - echo "Skipping reorganization: App will be served from root (/)"; \ - fi +# RUN if [ -n "$VITE_FRONTEND_BASE_PATH" ] && [ "$VITE_FRONTEND_BASE_PATH" != "/" ]; then \ +# # Extract the directory name (e.g., from "/space/" to "space") to exclude it from the find command +# FOLDER_NAME=$(echo "$VITE_FRONTEND_BASE_PATH" | sed 's/\///g'); \ +# echo "Reorganizing dist for subpath: $VITE_FRONTEND_BASE_PATH"; \ +# # Create the nested directory structure +# mkdir -p ./dist${VITE_FRONTEND_BASE_PATH} && \ +# # Move everything EXCEPT index.html and the newly created subfolder into the subfolder +# find ./dist -mindepth 1 -maxdepth 1 ! -name "index.html" ! -name "$FOLDER_NAME" \ +# -exec mv -t ./dist${VITE_FRONTEND_BASE_PATH}/ {} +; \ +# else \ +# echo "Skipping reorganization: App will be served from root (/)"; \ +# fi # Specify the command to start the server CMD ["pnpm", "exec", "serve", "-s", "./dist", "-l", "5050"] \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index ba4daaf..91b26db 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,12 +3,12 @@ - - - - + + + + - + SPACE diff --git a/frontend/src/components/contracts/ContractsTable.tsx b/frontend/src/components/contracts/ContractsTable.tsx index a5be704..8736917 100644 --- a/frontend/src/components/contracts/ContractsTable.tsx +++ b/frontend/src/components/contracts/ContractsTable.tsx @@ -188,7 +188,7 @@ export default function ContractsTable({ contracts, page, setPage, limit, setLim navigate(`${import.meta.env.BASE_URL}contracts/${c.userContact?.userId}`)} + onClick={() => navigate(`/contracts/${c.userContact?.userId}`)} className="px-3 py-1 bg-indigo-600 hover:bg-indigo-700 text-white rounded-md text-sm transition-colors cursor-pointer" > View diff --git a/frontend/src/layouts/logged-view/components/sidebar/index.tsx b/frontend/src/layouts/logged-view/components/sidebar/index.tsx index 25fe49b..7359bd2 100644 --- a/frontend/src/layouts/logged-view/components/sidebar/index.tsx +++ b/frontend/src/layouts/logged-view/components/sidebar/index.tsx @@ -7,34 +7,34 @@ import { AiOutlineDashboard } from 'react-icons/ai'; import OrganizationSelector from '@/components/OrganizationSelector'; const mainTabs = [ - { label: 'Overview', path: '', icon: }, - { label: 'Contracts Dashboard', path: 'contracts/dashboard', icon: }, - { label: 'Members', path: 'members', icon: }, - { label: 'API Keys', path: 'api-keys', icon: }, - { label: 'Services Management', path: 'services', icon: }, + { label: 'Overview', path: '/', icon: }, + { label: 'Contracts Dashboard', path: '/contracts/dashboard', icon: }, + { label: 'Members', path: '/members', icon: }, + { label: 'API Keys', path: '/api-keys', icon: }, + { label: 'Services Management', path: '/services', icon: }, ]; const settingsTabs = [ - { label: 'Organization Settings', path: 'organization-settings', icon: }, - { label: 'Profile Settings', path: 'settings', icon: }, + { label: 'Organization Settings', path: '/organization-settings', icon: }, + { label: 'Profile Settings', path: '/settings', icon: }, ]; const adminOnlyTabs = [ - { label: 'Users Management', path: 'users', icon: , adminOnly: true }, - { label: 'Organizations', path: 'organizations', icon: , adminOnly: true }, - { label: 'Instance Monitoring', path: 'instance-monitoring', icon: , adminOnly: true }, + { label: 'Users Management', path: '/users', icon: , adminOnly: true }, + { label: 'Organizations', path: '/organizations', icon: , adminOnly: true }, + { label: 'Instance Monitoring', path: '/instance-monitoring', icon: , adminOnly: true }, ]; function getSelectedTab(pathname: string) { - if (pathname.startsWith('members')) return 'members'; - if (pathname.startsWith('api-keys')) return 'api-keys'; - if (pathname.startsWith('services')) return 'services'; - if (pathname.startsWith('organization-settings')) return 'organization-settings'; - if (pathname.startsWith('settings')) return 'settings'; - if (pathname.startsWith('contracts/dashboard')) return 'contracts/dashboard'; - if (pathname.startsWith('instance-monitoring')) return 'instance-monitoring'; - if (pathname.startsWith('users')) return 'users'; - if (pathname.startsWith('organizations')) return 'organizations'; + if (pathname.startsWith('/members')) return '/members'; + if (pathname.startsWith('/api-keys')) return '/api-keys'; + if (pathname.startsWith('/services')) return '/services'; + if (pathname.startsWith('/organization-settings')) return '/organization-settings'; + if (pathname.startsWith('/settings')) return '/settings'; + if (pathname.startsWith('/contracts/dashboard')) return '/contracts/dashboard'; + if (pathname.startsWith('/instance-monitoring')) return '/instance-monitoring'; + if (pathname.startsWith('/users')) return '/users'; + if (pathname.startsWith('/organizations')) return '/organizations'; return '/'; } @@ -106,7 +106,7 @@ export default function Sidebar({ (selected === tab.path ? 'bg-indigo-100 dark:bg-gray-800 font-bold' : '') } - onClick={() => navigate(`${import.meta.env.BASE_URL}${tab.path}`)} + onClick={() => navigate(tab.path)} aria-current={selected === tab.path ? 'page' : undefined} > {tab.icon} @@ -155,7 +155,7 @@ export default function Sidebar({ (selected === tab.path ? 'bg-indigo-100 dark:bg-gray-800 font-bold' : '') } - onClick={() => navigate(`${import.meta.env.BASE_URL}${tab.path}`)} + onClick={() => navigate(tab.path)} aria-current={selected === tab.path ? 'page' : undefined} > {tab.icon} @@ -182,7 +182,7 @@ export default function Sidebar({ (selected === tab.path ? 'bg-indigo-100 dark:bg-gray-800 font-bold' : '') } - onClick={() => navigate(`${import.meta.env.BASE_URL}${tab.path}`)} + onClick={() => navigate(tab.path)} aria-current={selected === tab.path ? 'page' : undefined} > {tab.icon} diff --git a/frontend/src/pages/contracts/ContractDetailPage.tsx b/frontend/src/pages/contracts/ContractDetailPage.tsx index d874717..f3f5be9 100644 --- a/frontend/src/pages/contracts/ContractDetailPage.tsx +++ b/frontend/src/pages/contracts/ContractDetailPage.tsx @@ -118,7 +118,7 @@ export default function ContractDetailPage() { navigate(`${import.meta.env.BASE_URL}contracts/dashboard`)} + onClick={() => navigate(`/contracts/dashboard`)} className="inline-flex items-center px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors cursor-pointer" > @@ -145,7 +145,7 @@ export default function ContractDetailPage() { navigate(`${import.meta.env.BASE_URL}contracts/dashboard`)} + onClick={() => navigate(`/contracts/dashboard`)} className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors cursor-pointer" > @@ -186,7 +186,7 @@ export default function ContractDetailPage() { navigate(`${import.meta.env.BASE_URL}contracts/dashboard`)} + onClick={() => navigate(`/contracts/dashboard`)} className="inline-flex items-center px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors shadow-lg hover:shadow-xl cursor-pointer" > diff --git a/frontend/src/pages/login/index.tsx b/frontend/src/pages/login/index.tsx index bb05848..5348591 100644 --- a/frontend/src/pages/login/index.tsx +++ b/frontend/src/pages/login/index.tsx @@ -90,7 +90,7 @@ export default function LoginPage() {