From 0c36849db47e0f9ac689653a3a082ca73c9a76a8 Mon Sep 17 00:00:00 2001 From: Will Sams Date: Fri, 22 May 2026 12:29:14 -0400 Subject: [PATCH 1/3] Address full-stack code review findings - Fix CORS misconfiguration (wildcard origin + allow_credentials) - Fix fetch_available_rooms iterating dict keys instead of rooms - Fix double-decrement in sharedReducer on API error - Fix *_FAILED action type strings duplicating *_SUCCESS values - Fix DummyPool missing execute method - Fix deprecated datetime.utcnow() in auth - Fix swallowed token fetch error in baseApi - Fix createComponentReducer mutating caller's actionHandlers - Clean up settings.py duplicate declarations - Rename connectComponent config key dispatch -> mapDispatchToProps - Fix typos in cancellation dialog strings --- .gitignore | 1 + backend/src/api/__init__.py | 3 ++ backend/src/api/resolvers/data.py | 16 +++++----- backend/src/auth.py | 8 +++-- backend/src/main.py | 11 +++---- backend/src/settings.py | 7 ++--- frontend/src/screens/home/index.jsx | 2 +- .../screens/home/sagas/cancelReservation.js | 4 +-- frontend/src/screens/reservations/new.jsx | 2 +- frontend/src/shared/base/actionTypes.js | 16 +++++----- frontend/src/shared/base/baseApi.js | 4 +-- frontend/src/shared/base/connectComponent.jsx | 6 ++-- .../src/shared/base/createComponentReducer.js | 31 ++++++++++--------- 13 files changed, 55 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index ff21a49..63c68c5 100644 --- a/.gitignore +++ b/.gitignore @@ -157,6 +157,7 @@ cython_debug/ # VS Code .vscode/ +.claude/ .envrc serverless.yaml diff --git a/backend/src/api/__init__.py b/backend/src/api/__init__.py index 343e63f..2adca72 100644 --- a/backend/src/api/__init__.py +++ b/backend/src/api/__init__.py @@ -9,6 +9,9 @@ class DummyPool: async def close(self): pass + async def execute(self, *args, **kwargs): + pass + async def fetch(self, *args, **kwargs): pass diff --git a/backend/src/api/resolvers/data.py b/backend/src/api/resolvers/data.py index 93b3a76..ccf031b 100644 --- a/backend/src/api/resolvers/data.py +++ b/backend/src/api/resolvers/data.py @@ -36,7 +36,7 @@ async def create_reservation( return reservations -async def delete_reservation(db, reservation_id: str) -> Dict[str, Any]: +async def delete_reservation(db, reservation_id: int) -> Dict[str, Any]: reservation = await fetch_reservation(db, reservation_id) if reservation: await db.execute("DELETE FROM reservations WHERE id = $1", reservation_id) @@ -80,11 +80,8 @@ async def fetch_all_rows(db, entity_type) -> Dict[str, Any]: table_name = entity_type.__name__.lower() + "s" query = f"SELECT * FROM {table_name}" rows = await db.fetch(query) - if rows: - entities = [entity_type(**dict(row)) for row in rows] - return {"success": True, f"{table_name}": entities} - else: - raise ValueError("No reserved rooms found") + entities = [entity_type(**dict(row)) for row in rows] if rows else [] + return {"success": True, f"{table_name}": entities} async def fetch_by_id(db, entity_type, id) -> Dict[str, Any]: @@ -99,11 +96,14 @@ async def fetch_by_id(db, entity_type, id) -> Dict[str, Any]: async def fetch_available_rooms(db, checkin_date, checkout_date) -> Dict[str, Any]: - rooms = await fetch_all_rows(db, Room) + result = await fetch_all_rows(db, Room) + rooms = result.get("rooms", []) available_rooms = [ room for room in rooms - if await is_room_available(db, room.id, checkin_date, checkout_date) + if (await is_room_available(db, room.id, checkin_date, checkout_date))[ + "success" + ] ] return {"success": True, "rooms": available_rooms} diff --git a/backend/src/auth.py b/backend/src/auth.py index b1967fb..37354ad 100644 --- a/backend/src/auth.py +++ b/backend/src/auth.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from jose import jwt @@ -12,9 +12,11 @@ def create_token(subject: str, secret_key: str, expires_delta: timedelta) -> str: if expires_delta is not None: - expires_at = datetime.utcnow() + expires_delta + expires_at = datetime.now(timezone.utc) + expires_delta else: - expires_at = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + expires_at = datetime.now(timezone.utc) + timedelta( + minutes=ACCESS_TOKEN_EXPIRE_MINUTES + ) to_encode = {"exp": expires_at, "sub": subject} encoded_jwt = jwt.encode(to_encode, secret_key, ALGORITHM) diff --git a/backend/src/main.py b/backend/src/main.py index 033f384..236a136 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -10,14 +10,13 @@ from routes import create_app from routes.about import AboutRoute from routes.graphql import GraphqlRoute -from settings import ACCESS_TOKEN_EXPIRE_MINUTES +from settings import ACCESS_TOKEN_EXPIRE_MINUTES, ALLOWED_ORIGINS app = create_app(AboutRoute(), GraphqlRoute()) app.add_middleware( CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, + allow_origins=ALLOWED_ORIGINS, allow_methods=["*"], allow_headers=["*"], ) @@ -62,11 +61,9 @@ def main() -> None: port = int(utils.api_port()) if utils.is_debug(): uvicorn.run("main:app", host=host, port=port, reload=True) - else: # note: file logging occurs when not in debug mode - uvicorn.run(app, host=host, port=port) - - if utils.is_debug(): utils.logger_exit_message() + else: + uvicorn.run(app, host=host, port=port) if __name__ == "__main__": diff --git a/backend/src/settings.py b/backend/src/settings.py index 2217e14..dc6050b 100644 --- a/backend/src/settings.py +++ b/backend/src/settings.py @@ -4,16 +4,13 @@ API_PORT = getenv("RESERVATION_PORT") or 80 ENV = getenv("ENV") DB_URL = getenv("PG_URL") +ALLOWED_ORIGINS = getenv("ALLOWED_ORIGINS", "http://localhost:3000").split(",") IS_DEBUG = bool(int(getenv("IS_DEBUG", "0"))) or False -TOKEN_EXPIRATION_IN_MINUTES = 30 -SECRET_KEY = getenv("SECRET_KEY") ALGORITHM = "HS256" - -ACCESS_TOKEN_EXPIRE_MINUTES = 60 # 30 minutes +ACCESS_TOKEN_EXPIRE_MINUTES = 60 REFRESH_TOKEN_EXPIRE_MINUTES = 60 * 24 # 1 day -ALGORITHM = "HS256" SECRET_KEY = getenv("SECRET_KEY") REFRESH_SECRET_KEY = getenv("REFRESH_SECRET_KEY") diff --git a/frontend/src/screens/home/index.jsx b/frontend/src/screens/home/index.jsx index 6f9e7df..72a2872 100644 --- a/frontend/src/screens/home/index.jsx +++ b/frontend/src/screens/home/index.jsx @@ -86,7 +86,7 @@ const screen = connectComponent(HomeComponent, { load: { reservations: () => ({ type: actionTypes.GET_RESERVATIONS }), }, - actionCreators: (dispatch) => ({ + mapDispatchToProps: (dispatch) => ({ handleCloseAlert: () => dispatch({ type: actionTypes.CLEAR_ALERT }), handleConfirmAction: () => dispatch({ type: actionTypes.CONFIRM_CONFIRMATION_MODAL }), diff --git a/frontend/src/screens/home/sagas/cancelReservation.js b/frontend/src/screens/home/sagas/cancelReservation.js index c22b0e0..2035c44 100644 --- a/frontend/src/screens/home/sagas/cancelReservation.js +++ b/frontend/src/screens/home/sagas/cancelReservation.js @@ -11,8 +11,8 @@ import { fetchQuery, deleteReservationMutation } from '@/shared/graphql'; export function* confirmation(reservationId) { yield put({ type: actionTypes.OPEN_CONFIRMATION_MODAL, - title: 'Are you sure you?', - message: `You will not be able to reverse cancellation (id: ${reservationId}).`, + title: 'Are you sure?', + message: `You will not be able to reverse cancellation (id: ${reservationId}).`, cancellationText: 'Cancel', buttonStyle: 'danger', }); diff --git a/frontend/src/screens/reservations/new.jsx b/frontend/src/screens/reservations/new.jsx index 03d0b9d..70c1f91 100644 --- a/frontend/src/screens/reservations/new.jsx +++ b/frontend/src/screens/reservations/new.jsx @@ -126,7 +126,7 @@ const screen = connectComponent(NewReservationComponent, { load: { roomIds: () => ({ type: actionTypes.GET_ROOM_IDS }), }, - actionCreators: (dispatch) => ({ + mapDispatchToProps: (dispatch) => ({ createReservation: (formData) => dispatch({ type: actionTypes.CREATE_RESERVATION, ...formData }), handleCloseAlert: () => dispatch({ type: actionTypes.CLEAR_ALERT }), diff --git a/frontend/src/shared/base/actionTypes.js b/frontend/src/shared/base/actionTypes.js index 97c5fb9..f3cec0d 100644 --- a/frontend/src/shared/base/actionTypes.js +++ b/frontend/src/shared/base/actionTypes.js @@ -40,11 +40,11 @@ export const actionTypes = { // Site Components - Invalid Route Component Actions LOAD_HOME_COMPONENT: 'site/home/LOAD_HOME_COMPONENT', LOAD_HOME_COMPONENT_SUCCESS: 'site/home/LOAD_HOME_COMPONENT_SUCCESS', - LOAD_HOME_COMPONENT_FAILED: 'site/home/LOAD_HOME_COMPONENT_SUCCESS', + LOAD_HOME_COMPONENT_FAILED: 'site/home/LOAD_HOME_COMPONENT_FAILED', UNLOAD_HOME_COMPONENT: 'site/home/UNLOAD_HOME_COMPONENT', UNLOAD_HOME_COMPONENT_SUCCESS: 'site/home/UNLOAD_HOME_COMPONENT_SUCCESS', - UNLOAD_HOME_COMPONENT_FAILED: 'site/home/UNLOAD_HOME_COMPONENT_SUCCESS', + UNLOAD_HOME_COMPONENT_FAILED: 'site/home/UNLOAD_HOME_COMPONENT_FAILED', HOME_COMPONENT: 'site/home/HOME_COMPONENT', GET_RESERVATIONS: 'site/home/GET_RESERVATIONS', @@ -57,14 +57,14 @@ export const actionTypes = { LOAD_NEW_RESERVATION_COMPONENT_SUCCESS: 'site/reservations/LOAD_NEW_RESERVATION_COMPONENT_SUCCESS', LOAD_NEW_RESERVATION_COMPONENT_FAILED: - 'site/reservations/LOAD_NEW_RESERVATION_COMPONENT_SUCCESS', + 'site/reservations/LOAD_NEW_RESERVATION_COMPONENT_FAILED', UNLOAD_NEW_RESERVATION_COMPONENT: 'site/reservations/UNLOAD_NEW_RESERVATION_COMPONENT', UNLOAD_NEW_RESERVATION_COMPONENT_SUCCESS: 'site/reservations/UNLOAD_NEW_RESERVATION_COMPONENT_SUCCESS', UNLOAD_NEW_RESERVATION_COMPONENT_FAILED: - 'site/reservations/UNLOAD_NEW_RESERVATION_COMPONENT_SUCCESS', + 'site/reservations/UNLOAD_NEW_RESERVATION_COMPONENT_FAILED', NEW_RESERVATION_COMPONENT: 'site/reservations/NEW_RESERVATION_COMPONENT', GET_ROOM_IDS: 'site/reservations/GET_ROOM_IDS', @@ -86,14 +86,14 @@ export const actionTypes = { LOAD_EDIT_RESERVATION_COMPONENT_SUCCESS: 'site/reservations/edit/LOAD_EDIT_RESERVATION_COMPONENT_SUCCESS', LOAD_EDIT_RESERVATION_COMPONENT_FAILED: - 'site/reservations/edit/LOAD_EDIT_RESERVATION_COMPONENT_SUCCESS', + 'site/reservations/edit/LOAD_EDIT_RESERVATION_COMPONENT_FAILED', UNLOAD_EDIT_RESERVATION_COMPONENT: 'site/reservations/edit/UNLOAD_EDIT_RESERVATION_COMPONENT', UNLOAD_EDIT_RESERVATION_COMPONENT_SUCCESS: 'site/reservations/edit/UNLOAD_EDIT_RESERVATION_COMPONENT_SUCCESS', UNLOAD_EDIT_RESERVATION_COMPONENT_FAILED: - 'site/reservations/edit/UNLOAD_EDIT_RESERVATION_COMPONENT_SUCCESS', + 'site/reservations/edit/UNLOAD_EDIT_RESERVATION_COMPONENT_FAILED', EDIT_RESERVATION_COMPONENT: 'site/reservations/update/NEW_RESERVATION', @@ -108,14 +108,14 @@ export const actionTypes = { LOAD_SHOW_RESERVATION_COMPONENT_SUCCESS: 'site/reservations/show/LOAD_SHOW_RESERVATION_COMPONENT_SUCCESS', LOAD_SHOW_RESERVATION_COMPONENT_FAILED: - 'site/reservations/show/LOAD_SHOW_RESERVATION_COMPONENT_SUCCESS', + 'site/reservations/show/LOAD_SHOW_RESERVATION_COMPONENT_FAILED', UNLOAD_SHOW_RESERVATION_COMPONENT: 'site/reservations/show/UNLOAD_SHOW_RESERVATION_COMPONENT', UNLOAD_SHOW_RESERVATION_COMPONENT_SUCCESS: 'site/reservations/show/UNLOAD_SHOW_RESERVATION_COMPONENT_SUCCESS', UNLOAD_SHOW_RESERVATION_COMPONENT_FAILED: - 'site/reservations/show/UNLOAD_SHOW_RESERVATION_COMPONENT_SUCCESS', + 'site/reservations/show/UNLOAD_SHOW_RESERVATION_COMPONENT_FAILED', SHOW_RESERVATION_COMPONENT: 'site/reservations/show/RESERVATION', GET_RESERVATION: 'site/reservations/show/GET_RESERVATION', diff --git a/frontend/src/shared/base/baseApi.js b/frontend/src/shared/base/baseApi.js index be0f256..78a74e1 100644 --- a/frontend/src/shared/base/baseApi.js +++ b/frontend/src/shared/base/baseApi.js @@ -32,7 +32,6 @@ const handleResponse = (response, store) => { const handleResponseError = (error, store) => { const { message, name } = error; - store.dispatch({ type: actionTypes.API_REQUEST_DONE }); store.dispatch({ type: actionTypes.API_REQUEST_ERROR, error: { message, name }, @@ -71,8 +70,7 @@ export const createBaseApi = async (url, store) => { return instance; } catch (error) { - //console.error('Error fetching token:', error); - return error; + throw new Error(`Failed to initialize API: ${error.message}`); } }; diff --git a/frontend/src/shared/base/connectComponent.jsx b/frontend/src/shared/base/connectComponent.jsx index e7fb0b7..cbaaabd 100644 --- a/frontend/src/shared/base/connectComponent.jsx +++ b/frontend/src/shared/base/connectComponent.jsx @@ -6,7 +6,7 @@ import { useLoadComponent } from '@/shared/hooks'; /* type Config = { state: (state: any, ownProps: any) => object; - actionCreators?: (dispatch: Redux.Dispatch) => object; + mapDispatchToProps?: (dispatch: Redux.Dispatch) => object; componentName: string; load?: object; }; @@ -35,8 +35,8 @@ export const connectComponent = (WrappedComponent, config) => { }; }; const mapDispatchToProps = (dispatch) => { - const dispatchFromConfig = config.actionCreators - ? config.actionCreators(dispatch) + const dispatchFromConfig = config.mapDispatchToProps + ? config.mapDispatchToProps(dispatch) : {}; dispatchFromConfig.getDispatch = () => dispatch; diff --git a/frontend/src/shared/base/createComponentReducer.js b/frontend/src/shared/base/createComponentReducer.js index b16e5bd..8f06ded 100644 --- a/frontend/src/shared/base/createComponentReducer.js +++ b/frontend/src/shared/base/createComponentReducer.js @@ -3,20 +3,21 @@ export const createComponentReducer = ( initialState, actionHandlers, ) => { - actionHandlers[`LOAD_${componentName}`] = (state, action) => ({ - ...state, - loading: true, - }); - - actionHandlers[`LOAD_${componentName}_SUCCESS`] = (state, action) => ({ - ...state, - loading: false, - }); - - actionHandlers[`UNLOAD_${componentName}`] = (state, action) => ({ - ...initialState, - loading: true, - }); + const handlers = { + ...actionHandlers, + [`LOAD_${componentName}`]: (state, action) => ({ + ...state, + loading: true, + }), + [`LOAD_${componentName}_SUCCESS`]: (state, action) => ({ + ...state, + loading: false, + }), + [`UNLOAD_${componentName}`]: (state, action) => ({ + ...initialState, + loading: true, + }), + }; return ( state = { @@ -25,7 +26,7 @@ export const createComponentReducer = ( }, action, ) => { - const effect = actionHandlers[action.type]; + const effect = handlers[action.type]; if (effect) return effect(state, action); else return state; From a1581e4a192376275284c18932dd5efcfad30eb3 Mon Sep 17 00:00:00 2001 From: Will Sams Date: Fri, 22 May 2026 12:33:17 -0400 Subject: [PATCH 2/3] Fix broken test --- backend/specs/resolvers/when_querying_for_available_rooms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/specs/resolvers/when_querying_for_available_rooms.py b/backend/specs/resolvers/when_querying_for_available_rooms.py index b8b1be4..fbdf548 100644 --- a/backend/specs/resolvers/when_querying_for_available_rooms.py +++ b/backend/specs/resolvers/when_querying_for_available_rooms.py @@ -31,7 +31,8 @@ async def should_get_available_rooms_all_available(self, mocker): mocker.AsyncMock(return_value=mock_pool), ) mocker.patch( - "api.resolvers.data.fetch_all_rows", mocker.AsyncMock(return_value=rooms) + "api.resolvers.data.fetch_all_rows", + mocker.AsyncMock(return_value={"success": True, "rooms": rooms}), ) mocker.patch( "api.resolvers.data.fetch_room", From d8e27505a292ac0bd97272e054153e1a0dfd620c Mon Sep 17 00:00:00 2001 From: Will Sams Date: Fri, 22 May 2026 12:44:17 -0400 Subject: [PATCH 3/3] Update README.md for clarity --- README.md | 32 +++++++++++++++++--------------- tools/env.example.sh | 1 + 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index cf34331..aaef118 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ **JavaScript, Vite, Reactjs, Redux Toolkit, Redux Sagas, Python, FastAPI, GraphQL, AsyncPG, Postgres** -[![Application Unit Tests](https://github.com/WillSams/example-js-react-with-python/actions/workflows/pr-validate.yml/badge.svg)](https://github.com/WillSams/eexample-js-react-with-python/actions/workflows/pr-validate.yml) +[![Application Unit Tests](https://github.com/WillSams/example-js-react-with-python/actions/workflows/pr-validate.yml/badge.svg)](https://github.com/WillSams/example-js-react-with-python/actions/workflows/pr-validate.yml) This example contains a frontend and backend: - The frontend is a [React](https://react.dev) application using [Bootstrap4](https://getbootstrap.com/docs/4.6/getting-started/introduction/) for view designs. -- The backend is a [GraphQL API](https://graphql.org) providing the ability to create, delete, and list reservatios plus available rooms for a given date range. +- The backend is a [GraphQL API](https://graphql.org) providing the ability to create, delete, and list reservations plus available rooms for a given date range. React [Typescript](https://github.com/WillSams/example-ts-react-with-python) and [Express MVC](https://github.com/WillSams/example-mvc-expressjs-with-python) versions of this same idea are available. @@ -17,7 +17,7 @@ An [abandoned](https://github.com/WillSams/example-mvc-expressjs-with-python/tre - When a room is reserved, it cannot be reserved by another guest on overlapping dates. - Whenever there are multiple available rooms for a request, the room with the lower final price is assigned. -- Whenever a request is made for a single room, a double bed room may be assigned (if no single is available?). +- Whenever a request is made for a single room, a double bed room may be assigned if no single is available. - Smokers are not placed in non-smoking rooms. - Non-smokers are not placed in allowed smoking rooms. - Final price for reservations are determined by daily price * num of days requested, plus the cleaning fee. @@ -77,11 +77,12 @@ The below are optional but highly recommended: - [nvm](https://github.com/nvm-sh/nvm) - Used to manage NodeJS versions. - [Direnv](https://direnv.net/) - Used to manage environment variables. -- Install [direnv](https://direnv.net) for persisting environment variables needed for development. ## Getting Started -First, we'll need to set up our environment variables. You can do this by either any of the methods mentioned in [/tools/ENV.md](./tools/ENV.md) but I recommend using [Direnv](https://direnv.net/). +First, we'll need to set up our environment variables. You can do this by any of the methods mentioned in [/tools/ENV.md](./tools/ENV.md) but I recommend using [Direnv](https://direnv.net/). + +Key backend variables include `SECRET_KEY`, `REFRESH_SECRET_KEY`, `PG_URL`, and `ALLOWED_ORIGINS` (a comma-separated list of allowed frontend origins, e.g. `http://localhost:3000`). ### Install Python Packages @@ -99,11 +100,11 @@ pip install -r requirements.txt Execute the following within your terminal: ```bash -nvm use # To eliminate any issues, install/use the version listed in .nvmrc. -npm i # install the packages needed for project -cd ../frontend && npm i # install the packages needed for the frontend -cd ../db && npm i # install the packages needed for database migrations -cd .. # navigate back to the root of the repostiory +nvm use # To eliminate any issues, install/use the version listed in .nvmrc. +npm i # install the packages needed for project +cd frontend && npm i # install the packages needed for the frontend +cd ../db && npm i # install the packages needed for database migrations +cd .. # navigate back to the root of the repository ``` ### Create the database @@ -112,10 +113,11 @@ Finally, let's create and seed the databases and our Reservations and Rooms tabl ```bash # Create the databases and seed them -NODE_ENV=development | npm run refresh && npm run seed +cd db +NODE_ENV=development npm run refresh && NODE_ENV=development npm run seed ``` -During development, you can just execute `npm run dev:db-baseline` to refresh the database back to the original seed data. +During development, you can just execute `npm run dev:db-baseline` in the root of the project to refresh the database back to the original seed data. ## Development @@ -126,17 +128,17 @@ docker-compose up -d # runs the database in the background npm run dev ``` -Also, you just execute the backend via `npm run dev:backend`. to verify the backend is working: +Also, you can just execute the backend via `npm run dev:backend`. To verify the backend is working: ```bash curl http://localhost:$RESERVATION_PORT/$ENV/about ``` -You can also acces the Ariadne GraphiQL (interactive test playground) instance at [http://localhost:$RESERVATION_PORT/$ENV/graphql](http://localhost:$PLAYGROUND_PORT/$ENV/graphql). +You can also access the Ariadne GraphiQL (interactive test playground) instance at [http://localhost:$RESERVATION_PORT/$ENV/graphql](http://localhost:$RESERVATION_PORT/$ENV/graphql). ## Testing -The backend uses [Pytest](https://docs.pytest.org) and the frontend uses [Jest](https://jestjs.io/). To run these tests, simply execute `npm run test:backend` or `npm run test:frontend', respectively. +The backend uses [Pytest](https://docs.pytest.org) and the frontend uses [Jest](https://jestjs.io/). To run these tests, simply execute `npm run test:backend` or `npm run test:frontend`, respectively. ## Containerization diff --git a/tools/env.example.sh b/tools/env.example.sh index 0de7b52..3fc4db8 100644 --- a/tools/env.example.sh +++ b/tools/env.example.sh @@ -13,6 +13,7 @@ export FRONTEND_PORT=3000 export RESERVATION_PORT=8080 export RESERVATION_API=http://localhost:${RESERVATION_PORT}/${ENV} export VITE_RESERVATION_API=${RESERVATION_API} +export ALLOWED_ORIGINS=http://localhost:${FRONTEND_PORT} export PG_CLIENT=postgres export PG_USER=postgres