diff --git a/Dockerfile b/Dockerfile index dc5025f..b0026fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,8 @@ ARG REACT_APP_FRONTEND_URL ENV REACT_APP_FRONTEND_URL=$REACT_APP_FRONTEND_URL ARG REACT_APP_FRONTEND_PORT ENV REACT_APP_FRONTEND_PORT=$REACT_APP_FRONTEND_PORT +ARG REACT_APP_FRONTEND_REQUIRE_AUTH +ENV REACT_APP_FRONTEND_REQUIRE_AUTH=$REACT_APP_FRONTEND_REQUIRE_AUTH COPY frontend/package*.json ./ RUN npm ci @@ -25,12 +27,14 @@ RUN npm ci COPY backend/ . RUN npm run build +RUN cp package.json dist/ FROM node:20-alpine AS final WORKDIR /app COPY --from=backend-builder /app/backend/dist ./backend/dist +COPY --from=backend-builder /app/backend/package.json ./backend/ COPY --from=backend-builder /app/backend/node_modules ./backend/node_modules COPY --from=frontend-builder /app/frontend/build ./frontend/build diff --git a/backend/package.json b/backend/package.json index d655e1a..cb01599 100644 --- a/backend/package.json +++ b/backend/package.json @@ -3,9 +3,9 @@ "version": "1.0.0-beta.2", "main": "dist/server.js", "scripts": { - "build": "npx tsc && npm run copy-graphql", + "build": "rm -rf dist/ && npx tsc && npm run copy-graphql", "codegen:apollo": "graphql-codegen --config codegen.apollo.ts", - "copy-graphql": "cp -R src/**/*.graphql dist/graphql/", + "copy-graphql": "cp src/graphql/*.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", diff --git a/backend/src/config/config.ts b/backend/src/config/config.ts index 3711466..d24e6e2 100644 --- a/backend/src/config/config.ts +++ b/backend/src/config/config.ts @@ -17,6 +17,11 @@ const config = convict({ default: NodeEnvironment.Development, }, }, + requireAuth: { + env: 'BACKEND_REQUIRE_AUTH', + format: Boolean, + default: false, + }, server: { port: { env: 'PORT', diff --git a/backend/src/graphql/example.graphql b/backend/src/graphql/example.graphql index 1405fe4..73dfc06 100644 --- a/backend/src/graphql/example.graphql +++ b/backend/src/graphql/example.graphql @@ -44,6 +44,7 @@ input ProductInput { } type Query { + id: ID product(id: ID!): Product products(filter: ProductFilter, first: Int, skip: Int): [Product] } @@ -61,4 +62,5 @@ type User { 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 318dc08..d5b9614 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -1,27 +1,13 @@ import {verifySession} from 'supertokens-node/recipe/session/framework/express'; import {middleware, errorHandler} from 'supertokens-node/framework/express'; +import config from '../config/config'; + +const sessionConfig = { + sessionRequired: config.get('requireAuth'), +}; export const authMiddleware = { - verify: verifySession({ - sessionRequired: false, - }), + verify: verifySession(sessionConfig), 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(); -// }; diff --git a/backend/src/routes/graphql.ts b/backend/src/routes/graphql.ts index ef3a9b8..bac7791 100644 --- a/backend/src/routes/graphql.ts +++ b/backend/src/routes/graphql.ts @@ -84,13 +84,15 @@ const handleGraphQLRequest = async ( return res.status(400).json({message: 'Seed group not found'}); } - logger.graph('Handling GraphQL Request:', { - graphId, - variantName, - operationName, - // query, - // variables, - }); + if (operationName !== 'IntrospectionQuery') { + logger.graph('Handling GraphQL Request:', { + graphId, + variantName, + operationName, + // query, + // variables, + }); + } if (!operationName) { return res diff --git a/backend/src/server.ts b/backend/src/server.ts index b04df04..f47dc8b 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -29,13 +29,18 @@ import proposalsRoutes from './routes/proposals'; import seedGroupsRoutes from './routes/seedGroups'; import seedsRoutes from './routes/seeds'; import {logger} from './utilities/logger'; -import {version as APP_VERSION} from '../package.json'; +import fs from 'fs'; const isTypescript = __filename.endsWith('.ts'); const ProxyAgent = Undici.ProxyAgent; const setGlobalDispatcher = Undici.setGlobalDispatcher; const displayBanner = () => { + if (process.env.NODE_ENV === 'production') { + logger.startup(`🚀 Instant Mock v${APP_VERSION}`); + return; + } + const banner = figlet.textSync('Instant Mock', { font: 'Standard', horizontalLayout: 'default', @@ -184,6 +189,16 @@ const initializeApp = async () => { }); }; +let APP_VERSION = '0.0.0'; +try { + const packageJson = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8') + ); + APP_VERSION = packageJson.version; +} catch (error) { + console.warn('Failed to load package.json version:', error); +} + initializeApp().catch((error) => { logger.error('Failed to start the application', error); }); diff --git a/docker-compose.yml b/docker-compose.yml index 11e245a..6faeacb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,14 +6,15 @@ services: dockerfile: Dockerfile args: REACT_APP_BACKEND_URL: "${BACKEND_URL:-http://localhost}" - REACT_APP_BACKEND_PORT: "${PORT:-3033}" + REACT_APP_BACKEND_PORT: "${BACKEND_PORT:-3033}" REACT_APP_FRONTEND_URL: "${FRONTEND_URL:-http://localhost}" REACT_APP_FRONTEND_PORT: "${FRONTEND_PORT:-3033}" + REACT_APP_FRONTEND_REQUIRE_AUTH: true container_name: instant-mock environment: - PORT: "${PORT:-3033}" - APOLLO_API_KEY: "${APOLLO_API_KEY}" - NODE_ENV: "development" + REQUIRE_AUTH: true + PORT: "${BACKEND_PORT:-3033}" + NODE_ENV: "production" HOST: "0.0.0.0" SUPERTOKENS_CONNECTION_URI: "http://supertokens:3567" GITHUB_CLIENT_ID: "${GITHUB_CLIENT_ID}" @@ -21,7 +22,7 @@ services: AZURE_CLIENT_ID: "${AZURE_CLIENT_ID}" AZURE_CLIENT_SECRET: "${AZURE_CLIENT_SECRET}" ports: - - "${PORT:-3033}:${PORT:-3033}" + - "${BACKEND_PORT:-3033}:${BACKEND_PORT:-3033}" volumes: - ./backend/data:/app/backend/data networks: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bcf10fc..7c63e2d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,15 +1,18 @@ import {BrowserRouter as Router, Route, Routes} from 'react-router-dom'; -import SuperTokens from 'supertokens-auth-react'; +import SuperTokens, {SuperTokensWrapper} 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/auth'; +import {config} from './config/config'; -SuperTokens.init(SuperTokensConfig); +if (config.requireAuth) { + SuperTokens.init(SuperTokensConfig); +} -function App() { +function AppRoutes() { return ( @@ -26,4 +29,16 @@ function App() { ); } +function App() { + if (config.requireAuth) { + return ( + + + + ); + } + + return ; +} + export default App; diff --git a/frontend/src/components/ui/conditional-login-dropdown.tsx b/frontend/src/components/ui/conditional-login-dropdown.tsx new file mode 100644 index 0000000..38f3b98 --- /dev/null +++ b/frontend/src/components/ui/conditional-login-dropdown.tsx @@ -0,0 +1,106 @@ +import {config, getApiBaseUrl} from '../../config/config'; +import {LogIn, LogOut} from 'lucide-react'; +import {Avatar, AvatarFallback, AvatarImage} from './avatar'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from './dropdown-menu'; +import {useNavigate} from 'react-router'; +import {SuperTokensWrapper} from 'supertokens-auth-react'; +import Session, { + useSessionContext, +} from 'supertokens-auth-react/recipe/session'; +import {useEffect, useState} from 'react'; + +const LoginDropdownWithAuthRequired = () => { + const [avatarUrl, setAvatarUrl] = useState('/anonymous-avatar.svg'); + const navigate = useNavigate(); + + async function handleSignOut() { + await Session.signOut(); + navigate('/auth'); + } + + useEffect(() => { + const fetchAvatar = async () => { + try { + const response = await fetch(`${getApiBaseUrl()}/api/avatar`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + return; + } + + const data = await response.json(); + if (data.avatarUrl) { + setAvatarUrl(data.avatarUrl); + } + } catch (err) { + console.error('Error fetching avatar:', err); + } + }; + + fetchAvatar(); + }, []); + + const sessionContext = useSessionContext(); + if (sessionContext.loading === true) return null; + + const isLoggedIn = !!sessionContext.userId; + + return ( + + + + + + + + {isLoggedIn ? ( + + + Logout + + ) : ( + navigate('/auth')}> + + Login + + )} + + + ); +}; + +const LoginDropdownDisabled = () => { + const [avatarUrl] = useState('/anonymous-avatar.svg'); + + return ( + + + + + + + + ); +}; + +const ConditionalLoginDropdown = () => { + return config.requireAuth ? ( + + + + ) : ( + + ); +}; + +export default ConditionalLoginDropdown; diff --git a/frontend/src/components/ui/home.tsx b/frontend/src/components/ui/home.tsx index c63d400..a08237c 100644 --- a/frontend/src/components/ui/home.tsx +++ b/frontend/src/components/ui/home.tsx @@ -40,6 +40,7 @@ import { CommandList, CommandSeparator, } from './command'; +import ConditionalLoginDropdown from './conditional-login-dropdown'; import { Dialog, DialogContent, @@ -91,43 +92,42 @@ import {toast} from './use-toast'; const Home = () => { const navigate = useNavigate(); - const [selectedTab, setSelectedTab] = useState('sandbox'); - const [selectedGraph, setSelectedGraph] = useState(null); - const [selectedVariant, setSelectedVariant] = useState(null); - const [variants, setVariants] = useState([]); - const [proposals, setProposals] = useState([]); - const [seedGroups, setSeedGroups] = useState([]); - const [selectedSeedGroup, setSelectedSeedGroup] = useState(null); - const [graphs, setGraphs] = useState([]); - const [open, setOpen] = useState(false); + const [avatarUrl, setAvatarUrl] = useState('/anonymous-avatar.svg'); + const [dialogOpen, setDialogOpen] = React.useState(false); - const [newGroupName, setNewGroupName] = React.useState(''); - const [seedWithArguments, setSeedWithArguments] = useState(false); - const [seeds, setSeeds] = useState([]); + const [graphs, setGraphs] = useState([]); + + const [isCreateSeedView, setIsCreateSeedView] = useState(true); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [seedToDelete, setSeedToDelete] = useState(null); - const [seedToView, setSeedToView] = useState(null); const [isSeedButtonVisible, setIsSeedButtonVisible] = useState(false); - const [isCreateSeedView, setIsCreateSeedView] = useState(true); + const [newGroupName, setNewGroupName] = React.useState(''); + const [open, setOpen] = useState(false); + const [operationName, setOperationName] = useState(''); + const [proposals, setProposals] = useState([]); + const [seedArgs, setSeedArgs] = useState('{}'); + const [seedGroups, setSeedGroups] = useState([]); const [seedResponse, setSeedResponse] = useState(''); - const [operationName, setOperationName] = useState(''); - const [avatarUrl, setAvatarUrl] = useState('/anonymous-avatar.svg'); + const [seedToDelete, setSeedToDelete] = useState(null); + const [seedToView, setSeedToView] = useState(null); + const [seedWithArguments, setSeedWithArguments] = useState(false); + const [seeds, setSeeds] = useState([]); + + const [selectedGraph, setSelectedGraph] = useState(null); + const [selectedSeedGroup, setSelectedSeedGroup] = useState(null); + const [selectedTab, setSelectedTab] = useState('sandbox'); + const [selectedVariant, setSelectedVariant] = useState(null); + const [variants, setVariants] = useState([]); + const serverBaseUrl = getApiBaseUrl(); const handleSettingsClick = () => navigate('/settings'); - const handleCreateSeedClick = () => { populateSeedForm(); setSelectedTab('seeds'); setIsSeedButtonVisible(false); }; - // async function handleSignOut() { - // await Session.signOut(); - // navigate('/auth'); - // } - useEffect(() => { const fetchSeedGroups = () => { console.log('Fetching seed groups...'); @@ -348,33 +348,6 @@ const Home = () => { }, }); - useEffect(() => { - const fetchAvatar = async () => { - try { - const response = await fetch(`${serverBaseUrl}/api/avatar`, { - method: 'GET', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - return; - } - - const data = await response.json(); - if (data.avatarUrl) { - setAvatarUrl(data.avatarUrl); - } - } catch (err) { - console.error('Error fetching avatar:', err); - } - }; - - fetchAvatar(); - }, []); - const {setValue} = form; async function onSubmit(values: z.infer) { @@ -558,20 +531,7 @@ const Home = () => { className="h-5 w-5 text-gray-500 cursor-pointer" onClick={handleSettingsClick} /> - - - - - ? - - - - navigate('/auth')}> - - Login - - - +
diff --git a/frontend/src/config/config.ts b/frontend/src/config/config.ts index b9e4c49..5b21cf0 100644 --- a/frontend/src/config/config.ts +++ b/frontend/src/config/config.ts @@ -6,6 +6,7 @@ export const config = { frontend: { url: `${process.env.REACT_APP_FRONTEND_URL || 'http://localhost'}:${process.env.REACT_APP_FRONTEND_PORT || '3032'}`, }, + requireAuth: process.env.REACT_APP_FRONTEND_REQUIRE_AUTH || false, } as const; export function useConfig() { diff --git a/package-lock.json b/package-lock.json index 7661295..773c9dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "date-fns": "^4.1.0", "dotenv": "^16.4.5", "globals": "^15.12.0", + "prettier": "^3.3.3", "ts-node": "^10.9.2", "typescript-eslint": "^8.13.0", "wait-on": "^7.0.0" @@ -3374,6 +3375,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", diff --git a/package.json b/package.json index 7bf5bec..c4bd494 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "main": "index.js", "module": "true", "scripts": { - "start": "concurrently \"npm --prefix frontend start\" \"npm --prefix backend run dev\"", + "dev": "npm run install:all && concurrently -n backend,frontend -c yellow,cyan \"yes | npm --prefix backend run dev\" \"wait-on http://localhost:3033 && npm --prefix frontend start\"", + "start": "npm run install:all && npm --prefix frontend run build && npm --prefix backend run build && NODE_ENV=production npm --prefix backend start", "install:all": "npm install && npm --prefix frontend install && npm --prefix backend install", - "start:auth": "docker compose up", - "start:auth:build": "docker compose up --build", + "start:auth": "docker compose up --build", "cypress:basic": "concurrently -k -s first \"NODE_ENV=development PORT=3032 npm --prefix backend start\" \"wait-on http://localhost:3032 && CYPRESS_BASE_URL=http://localhost:3032 cypress run --spec 'cypress/e2e/basic-functionality.cy.ts'\"", "cypress:open": "concurrently --kill-others --success first --color always \"FORCE_COLOR=1 npm run start:auth\" \"wait-on http://localhost:3032 && FORCE_COLOR=1 CYPRESS_BASE_URL=http://localhost:3032 cypress open\"", "db:auth": "docker exec -it supertokens_db psql -U supertokens_user -d supertokens", @@ -26,6 +26,7 @@ "date-fns": "^4.1.0", "dotenv": "^16.4.5", "globals": "^15.12.0", + "prettier": "^3.3.3", "ts-node": "^10.9.2", "typescript-eslint": "^8.13.0", "wait-on": "^7.0.0" @@ -34,4 +35,4 @@ "node": "20", "npm": "10" } -} \ No newline at end of file +}