diff --git a/.dockerignore b/.dockerignore index 43380f0..a459492 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,7 @@ frontend/node_modules -frontend/package-lock.json frontend/build backend/node_modules -backend/package-lock.json backend/dist backend/instant-mock.db backend/data diff --git a/Dockerfile b/Dockerfile index e7a198f..dc5025f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ ARG REACT_APP_FRONTEND_PORT ENV REACT_APP_FRONTEND_PORT=$REACT_APP_FRONTEND_PORT COPY frontend/package*.json ./ -RUN npm install +RUN npm ci COPY frontend/ . RUN npm run build @@ -21,7 +21,7 @@ FROM node:20-alpine AS backend-builder WORKDIR /app/backend COPY backend/package*.json ./ -RUN npm install +RUN npm ci COPY backend/ . RUN npm run build diff --git a/backend/package-lock.json b/backend/package-lock.json index dfcd841..d89e61c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -31,6 +31,7 @@ "dotenv": "^16.4.5", "express": "^4.17.1", "express-http-proxy": "^2.0.0", + "figlet": "^1.8.0", "get-port": "^7.1.0", "global-agent": "^3.0.0", "graphql": "^16.9.0", @@ -55,6 +56,7 @@ "@mikro-orm/cli": "^6.3.13", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/figlet": "^1.7.0", "@types/global-agent": "^2.1.3", "@types/jest": "^29.5.13", "@types/lodash": "^4.17.7", @@ -4061,6 +4063,13 @@ "@types/send": "*" } }, + "node_modules/@types/figlet": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@types/figlet/-/figlet-1.7.0.tgz", + "integrity": "sha512-KwrT7p/8Eo3Op/HBSIwGXOsTZKYiM9NpWRBJ5sVjWP/SmlS+oxxRvJht/FNAtliJvja44N3ul1yATgohnVBV0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/global-agent": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@types/global-agent/-/global-agent-2.1.3.tgz", @@ -7494,7 +7503,7 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.8.0.tgz", "integrity": "sha512-chzvGjd+Sp7KUvPHZv6EXV5Ir3Q7kYNpCr4aHrRW79qFtTefmQZNny+W1pW9kf5zeE6dikku2W50W/wAH2xWgw==", - "dev": true, + "license": "MIT", "bin": { "figlet": "bin/index.js" }, diff --git a/backend/package.json b/backend/package.json index d5539c1..d655e1a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,9 +4,12 @@ "main": "dist/server.js", "scripts": { "build": "npx tsc && npm run copy-graphql", - "copy-graphql": "cp -R src/**/*.graphql dist/graphql/", "codegen:apollo": "graphql-codegen --config codegen.apollo.ts", + "copy-graphql": "cp -R src/**/*.graphql dist/graphql/", + "db:reset": "ts-node src/scripts/reset-database.ts", + "debug": "kill -9 $(lsof -t -i :9229) 2>/dev/null || true && DEBUGGING=true node --inspect-brk=9229 -r ts-node/register src/server.ts & pid=$! && sleep 2 && open -a 'Google Chrome' 'chrome://inspect' && wait $pid", "dev": "npm run db:reset && ts-node-dev --respawn --transpile-only src/server.ts", + "generate:key": "ts-node src/scripts/generate-encryption-key.ts", "lint": "eslint .", "lint-fix": "eslint . --fix", "migrate:create": "npx mikro-orm migration:create --config src/mikro-orm.sqlite.ts", @@ -16,9 +19,7 @@ "seed:create": "npx mikro-orm seeder:create --config src/mikro-orm.sqlite.ts", "seed:run": "npx mikro-orm seeder:run --config src/mikro-orm.sqlite.ts", "start": "node dist/server.js", - "test": "jest", - "generate:key": "ts-node src/scripts/generate-encryption-key.ts", - "db:reset": "ts-node src/scripts/reset-database.ts" + "test": "jest" }, "dependencies": { "@apollo/client": "3.11.8", @@ -44,6 +45,7 @@ "dotenv": "^16.4.5", "express": "^4.17.1", "express-http-proxy": "^2.0.0", + "figlet": "^1.8.0", "get-port": "^7.1.0", "global-agent": "^3.0.0", "graphql": "^16.9.0", @@ -68,6 +70,7 @@ "@mikro-orm/cli": "^6.3.13", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/figlet": "^1.7.0", "@types/global-agent": "^2.1.3", "@types/jest": "^29.5.13", "@types/lodash": "^4.17.7", diff --git a/backend/src/config/config.ts b/backend/src/config/config.ts index 28efb10..3711466 100644 --- a/backend/src/config/config.ts +++ b/backend/src/config/config.ts @@ -41,7 +41,7 @@ const config = convict({ frontendPort: { env: 'FRONTEND_PORT', format: 'port', - default: 3033, + default: 3032, }, }, supertokens: { diff --git a/backend/src/config/supertokens.ts b/backend/src/config/supertokens.ts index 25686d8..febde6d 100644 --- a/backend/src/config/supertokens.ts +++ b/backend/src/config/supertokens.ts @@ -91,7 +91,7 @@ function getConfiguredProviders() { } export const SuperTokensConfig: TypeInput = { - debug: true, + // debug: true, supertokens: { connectionURI: config.get('supertokens.connectionUri'), }, diff --git a/backend/src/graphql/client.ts b/backend/src/graphql/client.ts index 991add3..0f209a2 100644 --- a/backend/src/graphql/client.ts +++ b/backend/src/graphql/client.ts @@ -42,12 +42,27 @@ export default class Client { constructor() { this.schemaLoader = new SchemaLoader(); + this.loadLocalSchemas(); + } + + private async loadLocalSchemas() { + logger.startup('Loading local schemas'); + const schemas = await this.schemaLoader.loadFromFiles(); + logger.startup('Local schemas loaded', { + count: schemas.length, + schemaNames: schemas.map((s) => s.name), + }); } private async getGraphsFromFiles(): Promise< NonNullable['graphs'] > { + logger.debug('Attempting to load schemas from files'); const schemas = await this.schemaLoader.loadFromFiles(); + logger.debug('Loaded schemas:', { + count: schemas.length, + schemaNames: schemas.map((s) => s.name), + }); return schemas.map(({name}) => ({ id: `local-${name}`, @@ -237,42 +252,47 @@ export default class Client { async getGraphs(): Promise< NonNullable['graphs'] > { - if (process.env.USE_LOCAL_SCHEMA === 'true') { - logger.graph('Fetching graphs from local files'); - return this.getGraphsFromFiles(); + logger.graph('Fetching graphs from local files'); + const localGraphs = await this.getGraphsFromFiles(); + + if (!this.organizationId) { + return localGraphs; } logger.graph('Fetching graphs from Apollo', { organizationId: this.organizationId, }); - const {data} = await this.apolloClient.query({ - query: GET_GRAPHS, - variables: {organizationId: this.organizationId}, - }); - - if (data.organization?.__typename === 'Organization') { - logger.graph('Successfully retrieved graphs', { - count: data.organization.graphs.length, + try { + const {data} = await this.apolloClient.query({ + query: GET_GRAPHS, + variables: {organizationId: this.organizationId}, }); - return data.organization.graphs; + + if (data.organization?.__typename === 'Organization') { + logger.graph('Successfully retrieved graphs', { + count: data.organization.graphs.length, + }); + return [...localGraphs, ...data.organization.graphs]; + } + } catch (e) { + logger.error('Error fetching Apollo graphs', {error: e}); + return localGraphs; } logger.error('Failed to retrieve organization graphs', { organizationId: this.organizationId, - typename: data.organization?.__typename, }); throw new Error('Unable to retrieve graphs'); } async getGraph(graphId: string): Promise { - if (process.env.USE_LOCAL_SCHEMA === 'true') { - const schemas = await this.schemaLoader.loadFromFiles(); - const graphName = graphId.replace('local-', ''); - const schema = schemas.find((s) => s.name === graphName); - - if (!schema) return null; + // Always check local schemas first + const schemas = await this.schemaLoader.loadFromFiles(); + const graphName = graphId.replace('local-', ''); + const localSchema = schemas.find((s) => s.name === graphName); + if (localSchema) { return { variants: [ { @@ -282,7 +302,7 @@ export default class Client { latestPublication: { publishedAt: new Date().toISOString(), schema: { - document: schema.schema, + document: localSchema.schema, }, }, }, @@ -293,31 +313,34 @@ export default class Client { }; } - const res = await this.apolloClient.query({ - query: GET_GRAPH, - variables: { - graphId, - filterBy: { - status: ['APPROVED', 'DRAFT', 'IMPLEMENTED', 'OPEN'], + // If not found locally and we have Apollo client initialized, try remote + if (this.apolloClient) { + const res = await this.apolloClient.query({ + query: GET_GRAPH, + variables: { + graphId, + filterBy: { + status: ['APPROVED', 'DRAFT', 'IMPLEMENTED', 'OPEN'], + }, }, - }, - }); - return res.data.graph; + }); + return res.data.graph; + } + + return null; } async getVariant( graphId: string, variantName: string ): Promise['variant']> { - if (process.env.USE_LOCAL_SCHEMA === 'true') { - const schemas = await this.schemaLoader.loadFromFiles(); - const localId = parseInt(graphId.replace('local-', '')); - const schema = schemas[localId]; - - if (!schema) return null; + const schemas = await this.schemaLoader.loadFromFiles(); + const graphName = graphId.replace('local-', ''); + const localSchema = schemas.find((s) => s.name === graphName); + if (localSchema) { return { - id: `local-${localId}@${variantName}`, + id: `local-${graphName}@${variantName}`, name: variantName, url: null, isProposal: false, @@ -327,39 +350,41 @@ export default class Client { latestPublication: { publishedAt: new Date().toISOString(), schema: { - document: schema.schema, + document: localSchema.schema, }, }, }; } - const {data} = await this.apolloClient.query({ - query: GET_VARIANT, - variables: {graphId, name: variantName}, - }); - return data.graph?.variant; + if (this.apolloClient) { + const {data} = await this.apolloClient.query({ + query: GET_VARIANT, + variables: {graphId, name: variantName}, + }); + return data.graph?.variant; + } + + return null; } async getGraphWithSubgraphs( graphId: string ): Promise { - if (process.env.USE_LOCAL_SCHEMA === 'true') { - const schemas = await this.schemaLoader.loadFromFiles(); - const localId = parseInt(graphId.replace('local-', '')); - const schema = schemas[localId]; - - if (!schema) return null; + const schemas = await this.schemaLoader.loadFromFiles(); + const graphName = graphId.replace('local-', ''); + const localSchema = schemas.find((s) => s.name === graphName); + if (localSchema) { return { variants: [ { - key: `local-${localId}@current`, + key: `local-${graphName}@current`, displayName: 'current', name: 'current', latestPublication: { publishedAt: new Date().toISOString(), schema: { - document: schema.schema, + document: localSchema.schema, }, }, }, @@ -370,31 +395,31 @@ export default class Client { }; } - const {data} = await this.apolloClient.query({ - query: GET_GRAPH_WITH_SUBGRAPHS, - variables: { - graphId, - filterBy: { - status: ['APPROVED', 'DRAFT', 'IMPLEMENTED', 'OPEN'], + if (this.apolloClient) { + const {data} = await this.apolloClient.query({ + query: GET_GRAPH_WITH_SUBGRAPHS, + variables: { + graphId, + filterBy: { + status: ['APPROVED', 'DRAFT', 'IMPLEMENTED', 'OPEN'], + }, }, - }, - }); - return data.graph; + }); + return data.graph; + } + + return null; } async getSchema(graphId: string, name: string): Promise { - if (process.env.USE_LOCAL_SCHEMA === 'true') { - const schemas = await this.schemaLoader.loadFromFiles(); - // TODO we need to think if we want to support multiple schemas loaded from files, for now we only select the first one - // const debugSchema = schemas.map((s) => s.name); - // logger.debug('Found schemas:', {debugSchema}); - // const schema = schemas.find( - // (s) => s.name === graphId.replace('local-', '') - // ); - const schema = schemas[0].schema; - logger.debug('found schema', {schema}); - // return schema ? schema.schema : null; - return schemas[0].schema; + const schemas = await this.schemaLoader.loadFromFiles(); + const localSchema = schemas.find( + (s) => s.name === graphId.replace('local-', '') + ); + + if (localSchema) { + logger.debug('Using local schema', {name: localSchema.name}); + return localSchema.schema; } try { diff --git a/backend/src/graphql/example.graphql b/backend/src/graphql/example.graphql new file mode 100644 index 0000000..1405fe4 --- /dev/null +++ b/backend/src/graphql/example.graphql @@ -0,0 +1,64 @@ +type Category { + id: ID! + name: String! + description: String + products: [Product] +} + +type Mutation { + addProduct(input: ProductInput!): Product! + updateProduct(id: ID!, input: ProductInput!): Product! + deleteProduct(id: ID!): Boolean! +} + +input PriceRangeInput { + min: Float + max: Float +} + +type Product { + id: ID! + name: String! + description: String! + price: Float! + stock: Int! + category: Category! + brand: String + reviews: [Review] +} + +input ProductFilter { + category: String + priceRange: PriceRangeInput + brand: String + name: String +} + +input ProductInput { + name: String! + description: String! + price: Float! + stock: Int! + categoryId: ID! + brand: String +} + +type Query { + product(id: ID!): Product + products(filter: ProductFilter, first: Int, skip: Int): [Product] +} + +type Review { + id: ID! + rating: Int! + comment: String + product: Product! + user: User! +} + +type User { + id: ID! + username: String! + email: String! + reviews: [Review] +} \ No newline at end of file diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 87779e5..318dc08 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -1,26 +1,27 @@ import {verifySession} from 'supertokens-node/recipe/session/framework/express'; import {middleware, errorHandler} from 'supertokens-node/framework/express'; -import express from 'express'; export const authMiddleware = { - verify: verifySession(), + verify: verifySession({ + sessionRequired: false, + }), init: middleware(), error: errorHandler(), }; -export interface AuthRequest extends Request { - session: { - getUserId(): string; - }; -} - -export const requireAuth = ( - req: AuthRequest, - res: express.Response, - next: express.NextFunction -) => { - if (!req.session) { - return res.status(401).json({error: 'Unauthorized'}); - } - next(); -}; +// export interface AuthRequest extends Request { +// session: { +// getUserId(): string; +// }; +// } +// +// export const requireAuth = ( +// req: AuthRequest, +// res: express.Response, +// next: express.NextFunction +// ) => { +// if (!req.session) { +// return res.status(401).json({error: 'Unauthorized'}); +// } +// next(); +// }; diff --git a/backend/src/routes/avatar.ts b/backend/src/routes/avatar.ts index 2f9c1df..133d1f2 100644 --- a/backend/src/routes/avatar.ts +++ b/backend/src/routes/avatar.ts @@ -8,13 +8,25 @@ router.get('/avatar', async (req: Request, res: Response) => { logger.debug('api avatar route hit'); // @ts-ignore const session = req.session; - const userId = session.getUserId(); - const {metadata} = await UserMetadata.getUserMetadata(userId); - const avatarUrl = metadata.avatarUrl; - if (avatarUrl) { - res.json({avatarUrl: avatarUrl}); - } else { - res.json({avatarUrl: null}); + + if (!session) { + return res.status(401).json({ + error: 'Unauthorized', + message: 'No session found', + }); + } + + try { + const userId = session.getUserId(); + const {metadata} = await UserMetadata.getUserMetadata(userId); + const avatarUrl = metadata.avatarUrl; + res.json({avatarUrl: avatarUrl || null}); + } catch (error) { + logger.error('Error fetching avatar', {error}); + res.status(500).json({ + error: 'Internal Server Error', + message: 'Failed to fetch avatar', + }); } }); diff --git a/backend/src/server.ts b/backend/src/server.ts index 16e47a2..da7c92b 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -13,6 +13,7 @@ import supertokens from 'supertokens-node'; import swaggerJsdoc from 'swagger-jsdoc'; import swaggerUi from 'swagger-ui-express'; import * as Undici from 'undici'; +import figlet from 'figlet'; import {getWebsiteDomain, SuperTokensConfig} from './config/supertokens'; import Client from './graphql/client'; import {authMiddleware} from './middleware/auth'; @@ -29,10 +30,22 @@ import seedGroupsRoutes from './routes/seedGroups'; import seedsRoutes from './routes/seeds'; import {logger} from './utilities/logger'; +const packageJson = require('../../package.json'); +const APP_VERSION = packageJson.version; + const isTypescript = __filename.endsWith('.ts'); const ProxyAgent = Undici.ProxyAgent; const setGlobalDispatcher = Undici.setGlobalDispatcher; +const displayBanner = () => { + const banner = figlet.textSync('Instant Mock', { + font: 'Standard', + horizontalLayout: 'default', + verticalLayout: 'default', + }); + logger.startup(`\n${banner}\nVersion: ${APP_VERSION}`); +}; + if (process.env.HTTP_PROXY) { logger.startup('HTTP_PROXY configuration detected', { proxy: process.env.HTTP_PROXY, @@ -58,6 +71,8 @@ export const DI = {} as { }; const initializeApp = async () => { + displayBanner(); + logger.startup('Initializing SuperTokens with config', { apiDomain: SuperTokensConfig.appInfo.apiDomain, websiteDomain: SuperTokensConfig.appInfo.websiteDomain, @@ -80,8 +95,13 @@ const initializeApp = async () => { DI.apolloApiKeys = DI.orm.em.getRepository(ApolloApiKey); DI.apolloClient = new Client(); - await DI.apolloClient.initializeClient(); - logger.startup('Apollo client initialized'); + + logger.startup( + 'Schema Client (apollo client + local schema support) initialization complete', + { + organizationId: DI.apolloClient.getOrganizationId(), + } + ); const em = DI.orm.em.fork(); const defaultGroup = await em @@ -136,7 +156,7 @@ const initializeApp = async () => { openapi: '3.0.0', info: { title: 'Instant Mock', - version: '0.1.0-alpha', + version: APP_VERSION, description: 'Mocks, but more instantly...', }, }, diff --git a/backend/src/utilities/logger.ts b/backend/src/utilities/logger.ts index 1353b30..64734b1 100644 --- a/backend/src/utilities/logger.ts +++ b/backend/src/utilities/logger.ts @@ -42,6 +42,15 @@ const createLogger = (options: LoggerOptions = {}): LoggerMethods => { level: isProduction ? 'info' : 'debug', timestamp: pino.stdTimeFunctions.isoTime, ...options, + hooks: { + logMethod(args, method) { + //TODO: overcome this known issue: https://github.com/pinojs/pino/issues/1983 + if (process.env.DEBUGGING === 'true') { + console.log(args[0]); + } + method.apply(this, args); + }, + }, }); const sanitizeApiKeys = (message: string): string => { diff --git a/backend/src/utilities/schemaLoader.ts b/backend/src/utilities/schemaLoader.ts index cfcb234..943f34f 100644 --- a/backend/src/utilities/schemaLoader.ts +++ b/backend/src/utilities/schemaLoader.ts @@ -10,8 +10,12 @@ export interface SchemaSource { export class SchemaLoader { private schemaDirectory: string; - // TODO it's supposed to be src/graphql for dev env - constructor(schemaDirectory: string = join(process.cwd(), 'dist/graphql/')) { + constructor( + schemaDirectory: string = join( + process.cwd(), + process.env.NODE_ENV === 'production' ? 'dist/graphql/' : 'src/graphql/' + ) + ) { this.schemaDirectory = process.env.SCHEMA_DIRECTORY || schemaDirectory; logger.startup('SchemaLoader initialized', { schemaDirectory: this.schemaDirectory, diff --git a/frontend/config-overrides.js b/frontend/config-overrides.js index fef8d08..7d072ae 100644 --- a/frontend/config-overrides.js +++ b/frontend/config-overrides.js @@ -1,12 +1,3 @@ -const path = require('path'); - module.exports = function override(config) { - // config.resolve.alias = { - // ...config.resolve.alias, - // 'use-sync-external-store/shim': path.resolve( - // __dirname, - // 'node_modules/use-sync-external-store/shim/index.js' - // ), - // }; return config; }; diff --git a/frontend/package.json b/frontend/package.json index 58e29ba..0ae28b2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -56,7 +56,7 @@ "zod": "^3.23.8" }, "scripts": { - "start": "react-app-rewired start", + "start": "PORT=${REACT_APP_FRONTEND_PORT:-3032} react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-scripts eject", diff --git a/frontend/public/anonymous-avatar.svg b/frontend/public/anonymous-avatar.svg new file mode 100644 index 0000000..ad421ca --- /dev/null +++ b/frontend/public/anonymous-avatar.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c3e4341..bcf10fc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,45 +1,28 @@ import {BrowserRouter as Router, Route, Routes} from 'react-router-dom'; -import SuperTokens, {SuperTokensWrapper} from 'supertokens-auth-react'; -import {SessionAuth} from 'supertokens-auth-react/recipe/session'; +import SuperTokens from 'supertokens-auth-react'; import CallbackHandler from './CallbackHandler'; import Home from './components/ui/home'; import Login from './components/ui/login'; import NotFound from './components/ui/not-found'; import SettingsPage from './components/ui/settings'; -import {SuperTokensConfig} from './config'; +import {SuperTokensConfig} from './config/auth'; SuperTokens.init(SuperTokensConfig); function App() { return ( - - - - } - /> - } />s - - - - } - /> - - - - } - /> - } /> - - - + + + } + /> + } /> + } /> + } /> + } /> + + ); } diff --git a/frontend/src/components/ui/graph-dashboard.tsx b/frontend/src/components/ui/graph-dashboard.tsx index c5f290f..ec8017c 100644 --- a/frontend/src/components/ui/graph-dashboard.tsx +++ b/frontend/src/components/ui/graph-dashboard.tsx @@ -1,5 +1,6 @@ +import {getApiBaseUrl} from '../../config/config'; import {ListFilter, Search} from 'lucide-react'; -import React, {useEffect, useState} from 'react'; +import {useEffect, useState} from 'react'; import {Graph} from '../../models/Graph'; import {Button} from './button'; import {DropdownMenu, DropdownMenuTrigger} from './dropdown-menu'; @@ -7,12 +8,11 @@ import {GraphCard} from './graph-card'; import {Input} from './input'; const GraphDashboard = () => { - // TODO store it somewhere globally - const apiUrl = process.env.REACT_APP_API_BASE_URL || 'http://localhost:3000'; + const apiUrl = getApiBaseUrl(); const [graphs, setGraphs] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [error] = useState(null); useEffect(() => { const fetchGraphs = async () => { diff --git a/frontend/src/components/ui/home.tsx b/frontend/src/components/ui/home.tsx index 076ef34..c63d400 100644 --- a/frontend/src/components/ui/home.tsx +++ b/frontend/src/components/ui/home.tsx @@ -6,14 +6,11 @@ import {ChevronsUpDown, LogOut, Plus, Settings, Trash} from 'lucide-react'; import React, {useCallback, useEffect, useState} from 'react'; import {useForm} from 'react-hook-form'; import {useNavigate} from 'react-router'; -import Session, { - useSessionContext, -} from 'supertokens-auth-react/recipe/session'; import {z} from 'zod'; import instant_mock_logo from '../../assets/instant_mock_logo.svg'; import narrative from '../../assets/narrative.png'; import logo from '../../assets/xolvio_logo.png'; -import {getApiBaseUrl} from '../../config'; +import {getApiBaseUrl} from '../../config/config'; import {getSeeds} from '../../services/SeedService'; import { AlertDialog, @@ -92,7 +89,6 @@ import {Toaster} from './toaster'; import {toast} from './use-toast'; const Home = () => { - const sessionContext = useSessionContext(); const navigate = useNavigate(); const [selectedTab, setSelectedTab] = useState('sandbox'); @@ -116,7 +112,7 @@ const Home = () => { const [seedArgs, setSeedArgs] = useState('{}'); const [seedResponse, setSeedResponse] = useState(''); const [operationName, setOperationName] = useState(''); - const [avatarUrl, setAvatarUrl] = useState(null); + const [avatarUrl, setAvatarUrl] = useState('/anonymous-avatar.svg'); const serverBaseUrl = getApiBaseUrl(); const handleSettingsClick = () => navigate('/settings'); @@ -127,10 +123,10 @@ const Home = () => { setIsSeedButtonVisible(false); }; - async function handleSignOut() { - await Session.signOut(); - navigate('/auth'); - } + // async function handleSignOut() { + // await Session.signOut(); + // navigate('/auth'); + // } useEffect(() => { const fetchSeedGroups = () => { @@ -355,22 +351,23 @@ const Home = () => { useEffect(() => { const fetchAvatar = async () => { try { - console.log('fetching avatar'); const response = await fetch(`${serverBaseUrl}/api/avatar`, { method: 'GET', - credentials: 'include', // Include cookies for session-based auth + credentials: 'include', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { - throw new Error(`Failed to fetch avatar: ${response.statusText}`); + return; } const data = await response.json(); - setAvatarUrl(data.avatarUrl); - } catch (err: any) { + if (data.avatarUrl) { + setAvatarUrl(data.avatarUrl); + } + } catch (err) { console.error('Error fetching avatar:', err); } }; @@ -378,14 +375,6 @@ const Home = () => { fetchAvatar(); }, []); - useEffect(() => { - console.log('Session Context:', JSON.stringify(sessionContext, null, 2)); - }, [sessionContext]); - - if (sessionContext.loading) { - return null; - } - const {setValue} = form; async function onSubmit(values: z.infer) { @@ -526,10 +515,6 @@ const Home = () => { setIsDeleteDialogOpen(true); }; - if (sessionContext.loading === true) { - return null; - } - return ( @@ -581,9 +566,9 @@ const Home = () => { - + navigate('/auth')}> - Sign out + Login diff --git a/frontend/src/components/ui/login.tsx b/frontend/src/components/ui/login.tsx index 7177ef3..2a4cfa1 100644 --- a/frontend/src/components/ui/login.tsx +++ b/frontend/src/components/ui/login.tsx @@ -10,7 +10,7 @@ import {Button} from './button'; import {Card, CardContent} from './card'; import {useAuthProviders} from '../../hooks/useAuthProviders'; import {Loader2} from 'lucide-react'; -import {getApiBaseUrl} from '../../config'; +import {getApiBaseUrl} from '../../config/config'; export default function Login() { const {providers, loading} = useAuthProviders(); diff --git a/frontend/src/components/ui/seed-details.tsx b/frontend/src/components/ui/seed-details.tsx index 57b5eaa..0ab23d1 100644 --- a/frontend/src/components/ui/seed-details.tsx +++ b/frontend/src/components/ui/seed-details.tsx @@ -1,13 +1,14 @@ 'use client'; +import {getApiBaseUrl} from '../../config/config'; import {Seed} from '@/models/Seed'; -import React, {useEffect, useRef, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; import {useParams} from 'react-router'; import {Button} from './button'; import {Card, CardContent, CardHeader, CardTitle} from './card'; export default function SeedDetails() { - const {proposalId, seedId} = useParams<{ + const {seedId} = useParams<{ proposalId: string; seedId: string; }>(); @@ -28,7 +29,7 @@ export default function SeedDetails() { ? seed.operationMatchArguments : {}; const seedResponse = seed?.seedResponse ? seed.seedResponse : {}; - const apiUrl = process.env.REACT_APP_API_BASE_URL || 'http://localhost:3000'; + const apiUrl = getApiBaseUrl(); useEffect(() => { fetchSeed(); diff --git a/frontend/src/components/ui/settings.tsx b/frontend/src/components/ui/settings.tsx index 7ebad6a..55a3c5b 100644 --- a/frontend/src/components/ui/settings.tsx +++ b/frontend/src/components/ui/settings.tsx @@ -1,8 +1,8 @@ 'use client'; +import {getApiBaseUrl} from '../../config/config'; import {CheckCircle2, Trash2} from 'lucide-react'; import {useEffect, useState} from 'react'; -import {getApiBaseUrl} from '../../config'; import {Button} from './button'; import { Card, diff --git a/frontend/src/components/ui/variant-dashboard.tsx b/frontend/src/components/ui/variant-dashboard.tsx index a164855..3807cad 100644 --- a/frontend/src/components/ui/variant-dashboard.tsx +++ b/frontend/src/components/ui/variant-dashboard.tsx @@ -1,3 +1,4 @@ +import {getApiBaseUrl} from '../../config/config'; import {Graph} from '@/models/Graph'; import {ListFilter, Search} from 'lucide-react'; import React, {useEffect, useState} from 'react'; @@ -17,7 +18,7 @@ import {ProposalCard} from './proposal-card'; import {VariantCard} from './variant-card'; const VariantDashboard = () => { - const apiUrl = process.env.REACT_APP_API_BASE_URL || 'http://localhost:3000'; + const apiUrl = getApiBaseUrl(); const [graph, setGraph] = useState(null); const [selectedStatuses, setSelectedStatuses] = useState( diff --git a/frontend/src/components/ui/variant-details.tsx b/frontend/src/components/ui/variant-details.tsx index 040a31e..9b95329 100644 --- a/frontend/src/components/ui/variant-details.tsx +++ b/frontend/src/components/ui/variant-details.tsx @@ -1,9 +1,10 @@ +import {getApiBaseUrl} from '../../config/config'; import {Seed} from '@/models/Seed'; import {ApolloSandbox} from '@apollo/sandbox/react'; import {HandleRequest} from '@apollo/sandbox/src/helpers/postMessageRelayHelpers'; import {zodResolver} from '@hookform/resolvers/zod'; import {Link, MoreHorizontal} from 'lucide-react'; -import React, {useEffect, useState} from 'react'; +import {useEffect, useState} from 'react'; import {useForm} from 'react-hook-form'; import {useNavigate, useParams} from 'react-router'; @@ -67,7 +68,7 @@ import {Toaster} from './toaster'; import {useToast} from './use-toast'; const VariantDetails = () => { - const apiUrl = process.env.REACT_APP_API_BASE_URL || 'http://localhost:3000'; + const apiUrl = getApiBaseUrl(); const navigate = useNavigate(); const {toast} = useToast(); const {graphId, variantName} = useParams(); diff --git a/frontend/src/config.ts b/frontend/src/config/auth.ts similarity index 63% rename from frontend/src/config.ts rename to frontend/src/config/auth.ts index 7305754..c88b9cf 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config/auth.ts @@ -1,15 +1,13 @@ import ThirdParty, {Github} from 'supertokens-auth-react/recipe/thirdparty'; import {ThirdPartyPreBuiltUI} from 'supertokens-auth-react/recipe/thirdparty/prebuiltui'; import Session from 'supertokens-auth-react/recipe/session'; - -export const apiDomain = `${process.env.REACT_APP_BACKEND_URL}:${process.env.REACT_APP_BACKEND_PORT}`; -const websiteDomain = `${process.env.REACT_APP_FRONTEND_URL}:${process.env.REACT_APP_FRONTEND_PORT}`; +import {config} from './config'; export const SuperTokensConfig = { appInfo: { appName: 'Instant Mock', - apiDomain, - websiteDomain, + apiDomain: config.backend.url, + websiteDomain: config.frontend.url, apiBasePath: '/auth', websiteBasePath: '/auth', }, @@ -30,5 +28,3 @@ export const ComponentWrapper = (props: { }): JSX.Element => { return props.children; }; -export const getApiBaseUrl = () => - process.env.NODE_ENV === 'development' ? websiteDomain : apiDomain; // we serve up the front end in a static bundle in prod diff --git a/frontend/src/config/config.ts b/frontend/src/config/config.ts new file mode 100644 index 0000000..b9e4c49 --- /dev/null +++ b/frontend/src/config/config.ts @@ -0,0 +1,15 @@ +export const config = { + env: process.env.NODE_ENV as 'development' | 'production' | 'test', + backend: { + url: `${process.env.REACT_APP_BACKEND_URL || 'http://localhost'}:${process.env.REACT_APP_BACKEND_PORT || '3033'}`, + }, + frontend: { + url: `${process.env.REACT_APP_FRONTEND_URL || 'http://localhost'}:${process.env.REACT_APP_FRONTEND_PORT || '3032'}`, + }, +} as const; + +export function useConfig() { + return config; +} + +export const getApiBaseUrl = () => config.backend.url; diff --git a/frontend/src/hooks/useAuthProviders.ts b/frontend/src/hooks/useAuthProviders.ts index 9331793..df37204 100644 --- a/frontend/src/hooks/useAuthProviders.ts +++ b/frontend/src/hooks/useAuthProviders.ts @@ -1,30 +1,31 @@ -import { useState, useEffect } from 'react'; -import { getApiBaseUrl } from '../config'; +import {getApiBaseUrl} from '../config/config'; +import {useState, useEffect} from 'react'; interface AuthProvider { - id: string; - name: string; + id: string; + name: string; } export function useAuthProviders() { - const [providers, setProviders] = useState([]); - const [loading, setLoading] = useState(true); + const [providers, setProviders] = useState([]); + const [loading, setLoading] = useState(true); - useEffect(() => { - const fetchProviders = async () => { - try { - const response = await fetch(`${getApiBaseUrl()}/auth-providers`); - const data = await response.json(); - setProviders(data.providers); - } catch (error) { - console.error('Failed to fetch auth providers:', error); - } finally { - setLoading(false); - } - }; + useEffect(() => { + const fetchProviders = async () => { + try { + const response = await fetch(`${getApiBaseUrl()}/auth-providers`); + const data = await response.json(); + setProviders(data.providers); + } catch (error) { + console.error('Failed to fetch auth providers:', error); + } finally { + setLoading(false); + } + }; - fetchProviders(); - }, []); + fetchProviders(); + }, []); + + return {providers, loading}; +} - return { providers, loading }; -} \ No newline at end of file diff --git a/frontend/src/services/SeedService.ts b/frontend/src/services/SeedService.ts index 1ce1046..7c897ee 100644 --- a/frontend/src/services/SeedService.ts +++ b/frontend/src/services/SeedService.ts @@ -1,4 +1,4 @@ -import {getApiBaseUrl} from '../config'; +import {getApiBaseUrl} from '../config/config'; import {Seed} from '@/models/Seed'; import axios from 'axios'; // TODO types diff --git a/backend/src/graphql/apollo-platform-api.graphql b/test/e2e/fixtures/20241114165658/apollo-platform-api.graphql similarity index 100% rename from backend/src/graphql/apollo-platform-api.graphql rename to test/e2e/fixtures/20241114165658/apollo-platform-api.graphql