From db818289a5cbcb929ac91509d9e5ab4d0ac49b76 Mon Sep 17 00:00:00 2001 From: SAM Jubayer Date: Wed, 14 Jun 2023 17:05:34 +0900 Subject: [PATCH 1/2] Initial commit. Environment Settings and Load --- fastapi-react-project/.idea/.gitignore | 3 + .../.idea/fastapi-react-project.iml | 8 + .../inspectionProfiles/profiles_settings.xml | 6 + fastapi-react-project/.idea/modules.xml | 8 + fastapi-react-project/.idea/vcs.xml | 6 + fastapi-react-project/.prettierignore | 1 + fastapi-react-project/README.md | 159 ++++++++++++++++ fastapi-react-project/backend/Dockerfile | 13 ++ fastapi-react-project/backend/app/__init__.py | 0 .../app/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 111 bytes .../app/__pycache__/main.cpython-38.pyc | Bin 0 -> 1631 bytes .../app/__pycache__/tasks.cpython-38.pyc | Bin 0 -> 360 bytes .../backend/app/alembic/README | 1 + .../backend/app/alembic/__init__.py | 0 .../alembic/__pycache__/env.cpython-38.pyc | Bin 0 -> 1801 bytes .../backend/app/alembic/env.py | 81 +++++++++ .../backend/app/alembic/script.py.mako | 24 +++ .../91979b40eb38_create_users_table.py | 34 ++++ ...9b40eb38_create_users_table.cpython-38.pyc | Bin 0 -> 1001 bytes .../backend/app/api/__init__.py | 0 .../api/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 115 bytes .../backend/app/api/api_v1/__init__.py | 0 .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 122 bytes .../app/api/api_v1/routers/__init__.py | 0 .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 130 bytes .../routers/__pycache__/auth.cpython-38.pyc | Bin 0 -> 1620 bytes .../routers/__pycache__/users.cpython-38.pyc | Bin 0 -> 2176 bytes .../backend/app/api/api_v1/routers/auth.py | 63 +++++++ .../app/api/api_v1/routers/tests/__init__.py | 0 .../app/api/api_v1/routers/tests/test_auth.py | 66 +++++++ .../api/api_v1/routers/tests/test_users.py | 110 ++++++++++++ .../backend/app/api/api_v1/routers/users.py | 107 +++++++++++ .../backend/app/api/dependencies/__init__.py | 0 .../backend/app/core/__init__.py | 0 .../core/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 116 bytes .../app/core/__pycache__/auth.cpython-38.pyc | Bin 0 -> 2094 bytes .../__pycache__/celery_app.cpython-38.pyc | Bin 0 -> 279 bytes .../core/__pycache__/config.cpython-38.pyc | Bin 0 -> 257 bytes .../core/__pycache__/security.cpython-38.pyc | Bin 0 -> 1237 bytes .../backend/app/core/auth.py | 75 ++++++++ .../backend/app/core/celery_app.py | 5 + .../backend/app/core/config.py | 7 + .../backend/app/core/security.py | 31 ++++ .../backend/app/db/__init__.py | 0 .../backend/app/db/session.py | 21 +++ .../backend/app/initial_data.py | 26 +++ fastapi-react-project/backend/app/tasks.py | 6 + .../backend/app/tests/__init__.py | 0 .../backend/app/tests/test_main.py | 4 + .../backend/app/tests/test_tasks.py | 6 + fastapi-react-project/backend/conftest.py | 169 ++++++++++++++++++ fastapi-react-project/backend/pyproject.toml | 2 + .../backend/requirements.txt | 19 ++ fastapi-react-project/docker-compose.yml | 71 ++++++++ fastapi-react-project/frontend/.dockerignore | 2 + fastapi-react-project/frontend/.eslintrc.js | 51 ++++++ fastapi-react-project/frontend/.prettierrc.js | 5 + fastapi-react-project/frontend/Dockerfile | 16 ++ fastapi-react-project/frontend/README.md | 68 +++++++ fastapi-react-project/frontend/package.json | 62 +++++++ .../frontend/public/favicon.ico | Bin 0 -> 3150 bytes .../frontend/public/index.html | 43 +++++ .../frontend/public/logo192.png | Bin 0 -> 5347 bytes .../frontend/public/logo512.png | Bin 0 -> 9664 bytes .../frontend/public/manifest.json | 25 +++ .../frontend/public/robots.txt | 3 + fastapi-react-project/frontend/run.sh | 18 ++ fastapi-react-project/frontend/src/App.tsx | 6 + fastapi-react-project/frontend/src/Routes.tsx | 54 ++++++ .../frontend/src/__tests__/home.test.tsx | 12 ++ .../frontend/src/__tests__/login.test.tsx | 11 ++ .../frontend/src/admin/Users/UserCreate.tsx | 21 +++ .../frontend/src/admin/Users/UserEdit.tsx | 22 +++ .../frontend/src/admin/Users/UserList.tsx | 24 +++ .../frontend/src/admin/Users/index.ts | 3 + .../frontend/src/admin/authProvider.ts | 55 ++++++ .../frontend/src/admin/index.ts | 1 + .../frontend/src/config/index.tsx | 3 + fastapi-react-project/frontend/src/decs.d.ts | 1 + fastapi-react-project/frontend/src/index.css | 13 ++ fastapi-react-project/frontend/src/index.tsx | 12 ++ fastapi-react-project/frontend/src/logo.svg | 7 + .../frontend/src/react-app-env.d.ts | 1 + .../frontend/src/utils/api.ts | 13 ++ .../frontend/src/utils/auth.ts | 118 ++++++++++++ .../frontend/src/utils/index.ts | 2 + .../frontend/src/views/Home.tsx | 66 +++++++ .../frontend/src/views/Login.tsx | 151 ++++++++++++++++ .../frontend/src/views/PrivateRoute.tsx | 25 +++ .../frontend/src/views/Protected.tsx | 5 + .../frontend/src/views/SignUp.tsx | 138 ++++++++++++++ .../frontend/src/views/index.ts | 5 + fastapi-react-project/frontend/tsconfig.json | 19 ++ fastapi-react-project/nginx/nginx.conf | 22 +++ fastapi-react-project/scripts/build.sh | 16 ++ fastapi-react-project/scripts/test.sh | 7 + fastapi-react-project/scripts/test_backend.sh | 6 + pyramid_scaffold/.coveragerc | 2 + pyramid_scaffold/.gitignore | 22 +++ pyramid_scaffold/CHANGES.txt | 4 + pyramid_scaffold/MANIFEST.in | 5 + pyramid_scaffold/README.txt | 30 ++++ pyramid_scaffold/development.ini | 59 ++++++ pyramid_scaffold/production.ini | 53 ++++++ pyramid_scaffold/pyramid_scaffold/__init__.py | 11 ++ pyramid_scaffold/pyramid_scaffold/routes.py | 3 + .../pyramid_scaffold/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../pyramid_scaffold/static/pyramid.png | Bin 0 -> 12901 bytes .../pyramid_scaffold/static/theme.css | 157 ++++++++++++++++ .../pyramid_scaffold/templates/404.jinja2 | 8 + .../pyramid_scaffold/templates/layout.jinja2 | 64 +++++++ .../templates/mytemplate.jinja2 | 8 + .../pyramid_scaffold/views/__init__.py | 0 .../pyramid_scaffold/views/default.py | 6 + .../pyramid_scaffold/views/notfound.py | 7 + pyramid_scaffold/pytest.ini | 6 + pyramid_scaffold/setup.py | 52 ++++++ pyramid_scaffold/testing.ini | 53 ++++++ pyramid_scaffold/tests/__init__.py | 0 pyramid_scaffold/tests/conftest.py | 76 ++++++++ pyramid_scaffold/tests/test_functional.py | 7 + pyramid_scaffold/tests/test_views.py | 13 ++ 122 files changed, 2910 insertions(+) create mode 100644 fastapi-react-project/.idea/.gitignore create mode 100644 fastapi-react-project/.idea/fastapi-react-project.iml create mode 100644 fastapi-react-project/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 fastapi-react-project/.idea/modules.xml create mode 100644 fastapi-react-project/.idea/vcs.xml create mode 100644 fastapi-react-project/.prettierignore create mode 100644 fastapi-react-project/README.md create mode 100644 fastapi-react-project/backend/Dockerfile create mode 100644 fastapi-react-project/backend/app/__init__.py create mode 100644 fastapi-react-project/backend/app/__pycache__/__init__.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/__pycache__/main.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/__pycache__/tasks.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/alembic/README create mode 100644 fastapi-react-project/backend/app/alembic/__init__.py create mode 100644 fastapi-react-project/backend/app/alembic/__pycache__/env.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/alembic/env.py create mode 100644 fastapi-react-project/backend/app/alembic/script.py.mako create mode 100644 fastapi-react-project/backend/app/alembic/versions/91979b40eb38_create_users_table.py create mode 100644 fastapi-react-project/backend/app/alembic/versions/__pycache__/91979b40eb38_create_users_table.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/api/__init__.py create mode 100644 fastapi-react-project/backend/app/api/__pycache__/__init__.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/api/api_v1/__init__.py create mode 100644 fastapi-react-project/backend/app/api/api_v1/__pycache__/__init__.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/api/api_v1/routers/__init__.py create mode 100644 fastapi-react-project/backend/app/api/api_v1/routers/__pycache__/__init__.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/api/api_v1/routers/__pycache__/auth.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/api/api_v1/routers/__pycache__/users.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/api/api_v1/routers/auth.py create mode 100644 fastapi-react-project/backend/app/api/api_v1/routers/tests/__init__.py create mode 100644 fastapi-react-project/backend/app/api/api_v1/routers/tests/test_auth.py create mode 100644 fastapi-react-project/backend/app/api/api_v1/routers/tests/test_users.py create mode 100644 fastapi-react-project/backend/app/api/api_v1/routers/users.py create mode 100644 fastapi-react-project/backend/app/api/dependencies/__init__.py create mode 100644 fastapi-react-project/backend/app/core/__init__.py create mode 100644 fastapi-react-project/backend/app/core/__pycache__/__init__.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/core/__pycache__/auth.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/core/__pycache__/celery_app.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/core/__pycache__/config.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/core/__pycache__/security.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/core/auth.py create mode 100644 fastapi-react-project/backend/app/core/celery_app.py create mode 100644 fastapi-react-project/backend/app/core/config.py create mode 100644 fastapi-react-project/backend/app/core/security.py create mode 100644 fastapi-react-project/backend/app/db/__init__.py create mode 100644 fastapi-react-project/backend/app/db/session.py create mode 100644 fastapi-react-project/backend/app/initial_data.py create mode 100644 fastapi-react-project/backend/app/tasks.py create mode 100644 fastapi-react-project/backend/app/tests/__init__.py create mode 100644 fastapi-react-project/backend/app/tests/test_main.py create mode 100644 fastapi-react-project/backend/app/tests/test_tasks.py create mode 100644 fastapi-react-project/backend/conftest.py create mode 100644 fastapi-react-project/backend/pyproject.toml create mode 100644 fastapi-react-project/backend/requirements.txt create mode 100644 fastapi-react-project/docker-compose.yml create mode 100644 fastapi-react-project/frontend/.dockerignore create mode 100644 fastapi-react-project/frontend/.eslintrc.js create mode 100644 fastapi-react-project/frontend/.prettierrc.js create mode 100644 fastapi-react-project/frontend/Dockerfile create mode 100644 fastapi-react-project/frontend/README.md create mode 100644 fastapi-react-project/frontend/package.json create mode 100644 fastapi-react-project/frontend/public/favicon.ico create mode 100644 fastapi-react-project/frontend/public/index.html create mode 100644 fastapi-react-project/frontend/public/logo192.png create mode 100644 fastapi-react-project/frontend/public/logo512.png create mode 100644 fastapi-react-project/frontend/public/manifest.json create mode 100644 fastapi-react-project/frontend/public/robots.txt create mode 100644 fastapi-react-project/frontend/run.sh create mode 100644 fastapi-react-project/frontend/src/App.tsx create mode 100644 fastapi-react-project/frontend/src/Routes.tsx create mode 100644 fastapi-react-project/frontend/src/__tests__/home.test.tsx create mode 100644 fastapi-react-project/frontend/src/__tests__/login.test.tsx create mode 100644 fastapi-react-project/frontend/src/admin/Users/UserCreate.tsx create mode 100644 fastapi-react-project/frontend/src/admin/Users/UserEdit.tsx create mode 100644 fastapi-react-project/frontend/src/admin/Users/UserList.tsx create mode 100644 fastapi-react-project/frontend/src/admin/Users/index.ts create mode 100644 fastapi-react-project/frontend/src/admin/authProvider.ts create mode 100644 fastapi-react-project/frontend/src/admin/index.ts create mode 100644 fastapi-react-project/frontend/src/config/index.tsx create mode 100644 fastapi-react-project/frontend/src/decs.d.ts create mode 100644 fastapi-react-project/frontend/src/index.css create mode 100644 fastapi-react-project/frontend/src/index.tsx create mode 100644 fastapi-react-project/frontend/src/logo.svg create mode 100644 fastapi-react-project/frontend/src/react-app-env.d.ts create mode 100644 fastapi-react-project/frontend/src/utils/api.ts create mode 100644 fastapi-react-project/frontend/src/utils/auth.ts create mode 100644 fastapi-react-project/frontend/src/utils/index.ts create mode 100644 fastapi-react-project/frontend/src/views/Home.tsx create mode 100644 fastapi-react-project/frontend/src/views/Login.tsx create mode 100644 fastapi-react-project/frontend/src/views/PrivateRoute.tsx create mode 100644 fastapi-react-project/frontend/src/views/Protected.tsx create mode 100644 fastapi-react-project/frontend/src/views/SignUp.tsx create mode 100644 fastapi-react-project/frontend/src/views/index.ts create mode 100644 fastapi-react-project/frontend/tsconfig.json create mode 100644 fastapi-react-project/nginx/nginx.conf create mode 100755 fastapi-react-project/scripts/build.sh create mode 100755 fastapi-react-project/scripts/test.sh create mode 100644 fastapi-react-project/scripts/test_backend.sh create mode 100644 pyramid_scaffold/.coveragerc create mode 100644 pyramid_scaffold/.gitignore create mode 100644 pyramid_scaffold/CHANGES.txt create mode 100644 pyramid_scaffold/MANIFEST.in create mode 100644 pyramid_scaffold/README.txt create mode 100644 pyramid_scaffold/development.ini create mode 100644 pyramid_scaffold/production.ini create mode 100644 pyramid_scaffold/pyramid_scaffold/__init__.py create mode 100644 pyramid_scaffold/pyramid_scaffold/routes.py create mode 100644 pyramid_scaffold/pyramid_scaffold/static/pyramid-16x16.png create mode 100644 pyramid_scaffold/pyramid_scaffold/static/pyramid.png create mode 100644 pyramid_scaffold/pyramid_scaffold/static/theme.css create mode 100644 pyramid_scaffold/pyramid_scaffold/templates/404.jinja2 create mode 100644 pyramid_scaffold/pyramid_scaffold/templates/layout.jinja2 create mode 100644 pyramid_scaffold/pyramid_scaffold/templates/mytemplate.jinja2 create mode 100644 pyramid_scaffold/pyramid_scaffold/views/__init__.py create mode 100644 pyramid_scaffold/pyramid_scaffold/views/default.py create mode 100644 pyramid_scaffold/pyramid_scaffold/views/notfound.py create mode 100644 pyramid_scaffold/pytest.ini create mode 100644 pyramid_scaffold/setup.py create mode 100644 pyramid_scaffold/testing.ini create mode 100644 pyramid_scaffold/tests/__init__.py create mode 100644 pyramid_scaffold/tests/conftest.py create mode 100644 pyramid_scaffold/tests/test_functional.py create mode 100644 pyramid_scaffold/tests/test_views.py diff --git a/fastapi-react-project/.idea/.gitignore b/fastapi-react-project/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/fastapi-react-project/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/fastapi-react-project/.idea/fastapi-react-project.iml b/fastapi-react-project/.idea/fastapi-react-project.iml new file mode 100644 index 00000000..d0876a78 --- /dev/null +++ b/fastapi-react-project/.idea/fastapi-react-project.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/fastapi-react-project/.idea/inspectionProfiles/profiles_settings.xml b/fastapi-react-project/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/fastapi-react-project/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/fastapi-react-project/.idea/modules.xml b/fastapi-react-project/.idea/modules.xml new file mode 100644 index 00000000..59819c27 --- /dev/null +++ b/fastapi-react-project/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/fastapi-react-project/.idea/vcs.xml b/fastapi-react-project/.idea/vcs.xml new file mode 100644 index 00000000..6c0b8635 --- /dev/null +++ b/fastapi-react-project/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/fastapi-react-project/.prettierignore b/fastapi-react-project/.prettierignore new file mode 100644 index 00000000..412c2574 --- /dev/null +++ b/fastapi-react-project/.prettierignore @@ -0,0 +1 @@ +docker-compose.yml \ No newline at end of file diff --git a/fastapi-react-project/README.md b/fastapi-react-project/README.md new file mode 100644 index 00000000..42406d8d --- /dev/null +++ b/fastapi-react-project/README.md @@ -0,0 +1,159 @@ +# fastapi-react-project + +## Features + +- **FastAPI** with Python 3.8 +- **React 16** with Typescript, Redux, and react-router +- Postgres +- SqlAlchemy with Alembic for migrations +- Pytest for backend tests +- Jest for frontend tests +- Perttier/Eslint (with Airbnb style guide) +- Docker compose for easier development +- Nginx as a reverse proxy to allow backend and frontend on the same port + +## Development + +The only dependencies for this project should be docker and docker-compose. + +### Quick Start + +Starting the project with hot-reloading enabled +(the first time it will take a while): + +```bash +docker-compose up -d +``` + +To run the alembic migrations (for the users table): + +```bash +docker-compose run --rm backend alembic upgrade head +``` + +And navigate to http://localhost:8000 + +_Note: If you see an Nginx error at first with a `502: Bad Gateway` page, you may have to wait for webpack to build the development server (the nginx container builds much more quickly)._ + +Auto-generated docs will be at +http://localhost:8000/api/docs + +### Rebuilding containers: + +``` +docker-compose build +``` + +### Restarting containers: + +``` +docker-compose restart +``` + +### Bringing containers down: + +``` +docker-compose down +``` + +### Frontend Development + +Alternatively to running inside docker, it can sometimes be easier +to use npm directly for quicker reloading. To run using npm: + +``` +cd frontend +npm install +npm start +``` + +This should redirect you to http://localhost:3000 + +### Frontend Tests + +``` +cd frontend +npm install +npm test +``` + +## Migrations + +Migrations are run using alembic. To run all migrations: + +``` +docker-compose run --rm backend alembic upgrade head +``` + +To create a new migration: + +``` +alembic revision -m "create users table" +``` + +And fill in `upgrade` and `downgrade` methods. For more information see +[Alembic's official documentation](https://alembic.sqlalchemy.org/en/latest/tutorial.html#create-a-migration-script). + +## Testing + +There is a helper script for both frontend and backend tests: + +``` +./scripts/test.sh +``` + +### Backend Tests + +``` +docker-compose run backend pytest +``` + +any arguments to pytest can also be passed after this command + +### Frontend Tests + +``` +docker-compose run frontend test +``` + +This is the same as running npm test from within the frontend directory + +## Logging + +``` +docker-compose logs +``` + +Or for a specific service: + +``` +docker-compose logs -f name_of_service # frontend|backend|db +``` + +## Project Layout + +``` +backend +└── app + ├── alembic + │ └── versions # where migrations are located + ├── api + │ └── api_v1 + │ └── endpoints + ├── core # config + ├── db # db models + ├── tests # pytest + └── main.py # entrypoint to backend + +frontend +└── public +└── src + ├── components + │ └── Home.tsx + ├── config + │ └── index.tsx # constants + ├── __tests__ + │ └── test_home.tsx + ├── index.tsx # entrypoint + └── App.tsx # handles routing +``` diff --git a/fastapi-react-project/backend/Dockerfile b/fastapi-react-project/backend/Dockerfile new file mode 100644 index 00000000..10aef33a --- /dev/null +++ b/fastapi-react-project/backend/Dockerfile @@ -0,0 +1,13 @@ + +FROM python:3.8 + +RUN mkdir /app +WORKDIR /app + +RUN apt update && \ + apt install -y postgresql-client + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . \ No newline at end of file diff --git a/fastapi-react-project/backend/app/__init__.py b/fastapi-react-project/backend/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastapi-react-project/backend/app/__pycache__/__init__.cpython-38.pyc b/fastapi-react-project/backend/app/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39e0fa853a6fcd02cfbc5655df014023d983e247 GIT binary patch literal 111 zcmWIL<>g`kf=x4PQ$X}%5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HBequoZ7{|wF c=4F<|$LkeT-r}&y%}*)KNwov1{tUzn0F6u(tN;K2 literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/__pycache__/main.cpython-38.pyc b/fastapi-react-project/backend/app/__pycache__/main.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d6efc4df90754fd4a508bdcb338928d1d8799f5 GIT binary patch literal 1631 zcmZ`3O>f&aRHP(Jwq?h0obR+v+a2Z}oE5tbL$PA(&<gWMA}HdEqm+_dlFCve zI}GVHz<$6E&2j%=_x%fAciLYl25gVCmlzvLfzPRqgd@x@P!`Hu64K^9j7 zk#**H8xLUccAYI< z67X2>7s8fV|BZJ};tiy4vXx8125j|>gOn{xSwqS?+pv^v7##0Fz*gAim;i<4b227u z>jI;(oF?9}jg*}WcTQ=e?2RkR4R-S-{h5e+;2+;)x7aSb&F-+fMxVLQo#6+6noJI< z4!%sKdieB-4!(d1luQnZ4xYiQ8l>7sII1NGnTV=ZAP{e-wK}>+qq0)4z;0w!d6
    >!b|+$2TL7>6HH0Rt44As!BGkuHgd*hF|5 z*esmf&kSdAT?J!w7-2Lg=&ZZ1g92@*V|dqo44hZH-&cY&>&t(S4b;@0s4BICkhJNd z!obsLe(j#A#sBX1P{bzESWuWSb3eA8dtnC8W4`sJu|HkI*hjXUBREZq35UdBVjK0a zQeG;?Qfwij16gMoIzFVLZSbbVUYrS=G%mPqy^a>m-9`avd~#@yie3C`-*zpwOk*P8 zFhA9vO2<-1%=S2xSq>7f$5zG*@1TabYt?Kw>o7@7v65sOn*S#l#WeV6WWN{rZzzwJ zy`xGh-4MX5lxc5L2{pW>gF_rA>_}mz^WlpT!b%{XiF&c8Av zYudcPdgY3CJ*cxXxyTWbk%^u@`}V6xpT9`eJ23jMoh2 z4~fZmnHG>F+7)%VP;Fa$Tk`PJqGC0N2l&`K@&O(_oRW}6crJV{B_2*X{k!3Zq!)yw V<>3xILhQ=CgWl-t5hIoGOm5)f29`e#@{QhKCD=JK=;1p;0YN0TjVZqsHhJD540> zyT$>=FXkDh28UNFPkvaTatLWn4cTQ`_5C%E2UR_HWm9>|buf3G#SmNa3wv9GDE>lv z&A4PZxe?(}y{Qk)tM%mJ$dmJ#uiQM_ie2`}W>)UC@qbz$p0eu(LUl#I3?sU)+Ph>L;PDK$ zr(bF>6p0Hf&fL-?SN;V4jJa~+UqB#u_9Xoh9*w{K-t&7uzc+(Hf}s5N%LV&AM(7VW zxLpDmd;!1t2`YvdmMC>?AaNd~0e1bCY~^7Z`hF40@u`~yliSeGTxZn7ThL%YSp<%27vzvr8m2qP8oE!Q&q3JnK; zf<@?Hi0fo3B;PO8RLo%1$qHrpiG}VqS2Lmbc&dwh4Aaequq+A*tKl9sJaa;Bmlh26 z;Wy4N7A-NuS594UkoKSLzTVy2ef50&=J3m5PzQynqZzkc9sP=Q7xw$=uWeeEPpRbj zL}X84aa5kx9oRRnwEWPCc1}fD_n^L`AH#$bSuS75aI@_u5zBmaoyL=qq%HzCo-F7Dsd+1Rhq3~A;({BVh{9cYcJfKS7RPPtAw;-JLrls12Hs_WyYH2-?!g5XJcp?-tb)N|! zA+zJ6wB}$K`Jn3-#J28U`&)0i@q-BmTE|vXWoYKYRd47@jc=ki#CijE-k_!(r1hh_ zyT+^degtb~8!D7UIK+YfqQ9d~g7rg~&D_$uIE$RHefZ5BDu=#Bi{J_afUQe(4zF$p z*$~>mwigj=y=`6KbG+;Tgr8glHePhk1DgPxkQHpa?5*I8m;JkN-pBOp$Nytg-AD9= zf*1i)CgX}~Q2<2^4=&ga8DL>e<29qym?9H?%@Ls>65!Vc>0699aX#BH-$^R7InPf= z5Ztrgtueg!sMLHaPU@~pAejW&Fqzzl^Zwjm^{3X ziFfg5u+w}16>9f!vZ5sXCyEls3<~~@Lg0nK`fyu!ph-7mF#}L%qej1W*{trY@?C>= za^pCCeZ6!5d2z%hBbRoh(Gi@}@4LfWE7!O@K6cNmj@R$0xwgxco>^~5y(!EZ^05!$wB{#C>~uvAI@xy!2lhB593eGFgF58+l(6vtuw2tFJK{{i1`+%y0H literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/alembic/env.py b/fastapi-react-project/backend/app/alembic/env.py new file mode 100644 index 00000000..5cbeca06 --- /dev/null +++ b/fastapi-react-project/backend/app/alembic/env.py @@ -0,0 +1,81 @@ +import os +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from app.db.models import Base + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_url(): + return os.getenv("DATABASE_URL") + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + Calls to context.execute() here emit the given string to the + script output. + """ + # url = config.get_main_option("sqlalchemy.url") + url = get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + In this scenario we need to create an Engine + and associate a connection with the context. + """ + configuration = config.get_section(config.config_ini_section) + configuration["sqlalchemy.url"] = get_url() + connectable = engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/fastapi-react-project/backend/app/alembic/script.py.mako b/fastapi-react-project/backend/app/alembic/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/fastapi-react-project/backend/app/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/fastapi-react-project/backend/app/alembic/versions/91979b40eb38_create_users_table.py b/fastapi-react-project/backend/app/alembic/versions/91979b40eb38_create_users_table.py new file mode 100644 index 00000000..af661b44 --- /dev/null +++ b/fastapi-react-project/backend/app/alembic/versions/91979b40eb38_create_users_table.py @@ -0,0 +1,34 @@ +"""create users table + +Revision ID: 91979b40eb38 +Revises: +Create Date: 2020-03-23 14:53:53.101322 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "91979b40eb38" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "user", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("email", sa.String(50), nullable=False), + sa.Column("first_name", sa.String(100)), + sa.Column("last_name", sa.String(100)), + sa.Column("address", sa.String(100)), + sa.Column("hashed_password", sa.String(100), nullable=False), + sa.Column("is_active", sa.Boolean, nullable=False), + sa.Column("is_superuser", sa.Boolean, nullable=False), + ) + + +def downgrade(): + op.drop_table("user") diff --git a/fastapi-react-project/backend/app/alembic/versions/__pycache__/91979b40eb38_create_users_table.cpython-38.pyc b/fastapi-react-project/backend/app/alembic/versions/__pycache__/91979b40eb38_create_users_table.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81e69554c78f001c9b76bd763e31082029df539a GIT binary patch literal 1001 zcmZuvy>HYo6pxeK<)e3P5eN`d7;k8MA4Skpg%DREb?HE1$zu8POfSXxz|OTgBsNx7 zB(~nj%Aezvseb_y6VJ(2Jpp#!i+_Ih?|u1gw;NhGeti1`6UVZC8n8ZW1TRq3Au6z9 z3kcYPWOi&5vv!0NyWqwxaN;(KhvGvE+MlgB01tfphv1xAz2GBDBxe<{}p{n5^F6b<&rPe%Cd z4*G-9a2STy7^KxB+AhkIxUF%E_zQQ|F>-cq@C^sgO+BL<}b_#DW zt&LKq8Iue8o-cHZXDk)hLk#~F+WdSjgej>G-=$JjG-nyV2DIuxuoc!G1CU%Py)|QM z#(|bhsmnrw4pK!~Ql%GMcW|lZC6~r`&y(g2=uXo-TDMKxis|;G5c4e8-f>>>GmNr* zTFEp&)85OX5S-=TEqNcW*6R1+9xKa61ka{vvUh>602{CN*84Y4wtEFvGF?rms~QYaEivC2(~D^B%6FpS)R-&_K^#vL*OOPLD3?w6RZ9FH!S}!D_}18bAtS88g`kf=x4PQ$X}%5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!H7equoZ6ldzk e$7kkcmc+;F6;$5hu*uC&Da}c>1L^n-#0&uHTNW7r literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/api/api_v1/__init__.py b/fastapi-react-project/backend/app/api/api_v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastapi-react-project/backend/app/api/api_v1/__pycache__/__init__.cpython-38.pyc b/fastapi-react-project/backend/app/api/api_v1/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a0bdceca9eb78850065f01ca3c47a65db80c3db GIT binary patch literal 122 zcmWIL<>g`kf=x4PQ$X}%5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!H#equoZ6lVhI k%=j`x{rLFIyv&mLc)fzkTO2mI`6;D2sdgZZpMjVG0GfChj{pDw literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/api/api_v1/routers/__init__.py b/fastapi-react-project/backend/app/api/api_v1/routers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastapi-react-project/backend/app/api/api_v1/routers/__pycache__/__init__.cpython-38.pyc b/fastapi-react-project/backend/app/api/api_v1/routers/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f972deb2b84fec45c4e53ca4586db9223bc2419 GIT binary patch literal 130 zcmWIL<>g`kf=x4PQ$X}%5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HzequoZ6lVhI s%=j`x{i6KRlGLJN{rLFIyv&mLc)fzkTO2mI`6;D2sdgX(J_9iW0EuH8+5i9m literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/api/api_v1/routers/__pycache__/auth.cpython-38.pyc b/fastapi-react-project/backend/app/api/api_v1/routers/__pycache__/auth.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b32a1c7885852c0ba095963c88272a6b44b8a3f5 GIT binary patch literal 1620 zcmd5+OK%)S5T5RN?CkFBdX3{if(Xky%flv!3lc&x&PK5UYs+5CXfC7iblVv-FS>h( zZS7t#SD!c`>X<4v-{c*~E zXjs6iH*|9^^o-ul8o3|(Ms~7h-U?er zcC&UKgn^O0tdn=cZax>z5sTX=mScszaGuZq^a5-AOt`~aCx9o`9_57j&j@R>*7xoS z2^Wk_`(JGW)-g6WSa-`B%>4l|TLYqRt*_MT$wwQplut{+Hu*O-m+E0D@`0n9D;sN@ zWv#f-jR(Bq1(Uk-XguB+9wxj}X<2Abs#w($JT+CCbH+0j!^PX@Dq_1J`I0BKNY&AR z>iHPn;)P0+Sn;Tq;I}8!{UWNXsNheH=Ga?S<=4DGk9!bC#9|dE6vLe@4^HI%H`tN^ zC~IO($N_;(PwXk3P9p70il2Qnss6NJC#5wPmqF>Fpd*0jG+4%tJ zyi8GhpYvF7ac1lG1y4~@GOj(wRh(wJ@r1{W3wcI#BTow;T530DAZiC+IHTH@^{#GL zT;!>gK)3{*gOunFKde&0Wn?(l-tNUyR0naAa4920qi&faQb!dZ1Ohu+`qgNmIdKcWzY1_>iFe_6 z{>E}#RkNMKnMU8Lyohic z;T42C08sfh%C8~3j&N1)`B}X`iB{J~55HJj9beU2Aba91gtrmiLAZ;6wYY%yK!V?Z zp$)(PcgB~%dNf!JdwVfVN0lyJ{wf5HyKcrbkRea2<=p*RHh#UhIlPkleJ0M|1>XIo$=2I eSeTRp@)8MXKmyXEE!(G_<(yMO&%3v;MrTT zs(dJvOAqY})O~@*%wXCuUi~5-hS}7OtvP;Y#;pORj8M!V{$*ZE4HWgn?{M*fTC2QNCcJBC0P76Be{+MNO{EvFm_h!WT;~ z8TfCLzrNsKjK$?0v3$WrL#$A=yMS)P|F5E5M=y(2K(C2)Lf-@QeQ`r<*$~YOTWp9; z!ao4~_Dy)=a!YIx`k~-oS)JCOcv78$DShmtLPl~nDM``K@g2vM4`AJ6 zf*KF?Przv$)QpSrX_j`lX=2=7rj$(5ZrDrvr_;!-H#l@Qlq!R@qv9TZ^w$ux2vb$Qs*_Vtdpqw_$uL({>m|ZQ?_gk@N1(`+K9><3W%AHMzW>3e&Mj#cN0*d3mkGOg)G zeVsO7mBt3OY93!@>NaSjQV!jVWe+-bK2XP(wvKriTd+MgTAy!k@I0aIk%1U>nA!v+ z>A7gZ)VRo?8_S0<(Hw+TPeF9;M}WRKhjVk)tRAPv9{zkb3ZTpn#D@tS2!|u# z=g!z6es|{l!sLwjwS`Q>ex%6(6yer_9#D7TquMAoP#|&R;6GodUnL^bAUgol#Kz`ZccCiU%*v8CT-DMRf8`Xou}YKEFyPkVHJx0g=SWS0rDjS0Q?N_k9e= zJi}%603+B7+a!XRz}Y|@NzxHlK>>pHn2q@aE18Y*R2~ck^mSXlfCB0#N7q>5qHC$V znQDos;&t#87Uo22Y8&+bk+_`I6S`pD%&LN{rnzf_{5_{6S@E?5Rvpr&IOO=_;hD zFoVH-Y42FZp;k5YV0Eh{6m<}$hLL&$C3nLtJyy%;z z4OZu6+hLq}j9)r#krgjFg`kf=x4PQ$X}%5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HdequoZ7$@f! hrRvAWXXa&=#K-FuRNmsS$<0qG%}KQbY55Gq3;+dD7Ks1= literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/core/__pycache__/auth.cpython-38.pyc b/fastapi-react-project/backend/app/core/__pycache__/auth.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31cc6347b63c65bfa2027439e3d8ada042e39a3b GIT binary patch literal 2094 zcmbVNOK%)S5bo}ooyWescI+gIqOd?9ED#3}+)zYfJ2*T{l&obkm(h5;Km{GAAQ>C8}87&Ai-?{5*()Jd8ps zt7O%@7S-~4RJVICYvj$SNr_-09LyY0csB>F1Nwl8S?)ikJmBG@N;C)ZIgnD}56yvb+aY(dO|jTY=@`P?HnI(xc5!{=dtj?;Ujv+z5hB5NJT_{(A} zhFqD(YPWl5<(r-u>vS|Up3;e)s17yNJG-B4bXTMtNznK55f_;UZyTDiU=%E<)7g2~Ag$ZJj;19L$lOg+>=z2j4Z_qek`+nhDTi;n*K7=k@+q<+h znq<5^9O?FUlBFB~wgD#>L!Bm>+G}lWY+SuD(R~#3;M90GMIr^puU5u2?m-xh$A!iy z`F)XaA#JpiY-gUQhYO-l+Vkn33$ocY5If}tZ)X-Q>HGXq;McjZ0%OZEo;@5`OLPE>4V+)!tYP_xf{xBaN0R9v1yF|Ac2WCP9@Iqf98=L9gzne zz6c`&tJ}INF(jr|iWhrw!poE)789TtXF)lrJS;H2X&NKU!3VMZ!x1nPwY1%BT!d}g z#a5Qhf%R<)q)lhrTK^dSxh9#oj*YJ{qye3$bv931wC)BZI0Ae}^$Q-n-J^PF-_(6x IueQR!0g!9)Pyhe` literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/core/__pycache__/celery_app.cpython-38.pyc b/fastapi-react-project/backend/app/core/__pycache__/celery_app.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cb9f38d61b748ec82ce475154833ed1fbe6935a6 GIT binary patch literal 279 zcmWIL<>g`kf=x4PQ>Fsx#~=g`kf=x4PQ|f{AV-N=!FatRbKwK;bBvKes7;_k+7*d#`7*iP*Fr_drWQ<}? zVToc%VGL%_WPJ%#!{ApXnwD5xl30+bTa=oZT%uc0l%JKFToT3O;uzxSB literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/core/__pycache__/security.cpython-38.pyc b/fastapi-react-project/backend/app/core/__pycache__/security.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4213edd3ea0996416ef1be4efb292897937c953a GIT binary patch literal 1237 zcmZ8gOK;pZ5GE-~`|!TJPGYnLTC~VT7aK6po{FN*Vv!hW)`8asT3!TMWV}kHl|XUp zY$4xrHIQonAH2t0d+bl)wI~0Do;t%_8$l^>I5V0dKhAtS>vr1&#$O+=6aAg_InP3!kFY|IPxYK#&=f3dsKm?R1CVd%PQ_)adHZG`a%GNa# zP1zPLxh2}NBeqmWb!7L3$)4=L=c0?Y>dC>4C%5GgJbg*!&J9%q705>r_gFr8ABb&9 zrzG6H15{)ZQv2xmaA9T#uj8t^EVO*4Vy!gzJ0ra;&8V0g_0ELMHf3y-NpodeD5c6w z{AYVVF4KKe{I2GZ(sat1&O~sYV7BG)!W1DBjVhU`TvfI$RjE}1TS?KW7Nycrr4p@- z@Lx<1zW?FwYk-BPl^Z-ZfS(IJPbdQ34Ai0KWCXLqHW+eCYqElyeWDXc-zqOHHzg_AyS$2Z!E*xWwgP9t_lx6Ap6+lr`p>uGo}lbc9TIVX?s8;Hr@OowX%Mon(Xid?ap z>MsqwWh+wCU2^5khil7tz$5EsPuXiEET~V}55pAM_btT}%xNI50R{t;P4sZ~TiQ*(t zRTViR<7QP41ffmJX^#zQj}EAQ3Z8JNzkx*z-o%xGH?VilAdNK@edJG;p6_iqs6Fty z+qqkbZUGNtb`Y(fg=@S17DK+n?5_?-qw#baogTj&PonX!ua8c~(W|4$+39#{y~=3E zLd(yKBGcGz=xv%92kf(pInFq`G=IzsxyaN{_;4zuK$CXh8?ZL(F%FmG-hdDJf8!=I AfdBvi literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/core/auth.py b/fastapi-react-project/backend/app/core/auth.py new file mode 100644 index 00000000..0b404b2f --- /dev/null +++ b/fastapi-react-project/backend/app/core/auth.py @@ -0,0 +1,75 @@ +import jwt +from fastapi import Depends, HTTPException, status +from jwt import PyJWTError + +from app.db import models, schemas, session +from app.db.crud import get_user_by_email, create_user +from app.core import security + + +async def get_current_user( + db=Depends(session.get_db), token: str = Depends(security.oauth2_scheme) +): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode( + token, security.SECRET_KEY, algorithms=[security.ALGORITHM] + ) + email: str = payload.get("sub") + if email is None: + raise credentials_exception + permissions: str = payload.get("permissions") + token_data = schemas.TokenData(email=email, permissions=permissions) + except PyJWTError: + raise credentials_exception + user = get_user_by_email(db, token_data.email) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: models.User = Depends(get_current_user), +): + if not current_user.is_active: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +async def get_current_active_superuser( + current_user: models.User = Depends(get_current_user), +) -> models.User: + if not current_user.is_superuser: + raise HTTPException( + status_code=403, detail="The user doesn't have enough privileges" + ) + return current_user + + +def authenticate_user(db, email: str, password: str): + user = get_user_by_email(db, email) + if not user: + return False + if not security.verify_password(password, user.hashed_password): + return False + return user + + +def sign_up_new_user(db, email: str, password: str): + user = get_user_by_email(db, email) + if user: + return False # User already exists + new_user = create_user( + db, + schemas.UserCreate( + email=email, + password=password, + is_active=True, + is_superuser=False, + ), + ) + return new_user diff --git a/fastapi-react-project/backend/app/core/celery_app.py b/fastapi-react-project/backend/app/core/celery_app.py new file mode 100644 index 00000000..8355ef0d --- /dev/null +++ b/fastapi-react-project/backend/app/core/celery_app.py @@ -0,0 +1,5 @@ +from celery import Celery + +celery_app = Celery("worker", broker="redis://redis:6379/0") + +celery_app.conf.task_routes = {"app.tasks.*": "main-queue"} diff --git a/fastapi-react-project/backend/app/core/config.py b/fastapi-react-project/backend/app/core/config.py new file mode 100644 index 00000000..3a6f2852 --- /dev/null +++ b/fastapi-react-project/backend/app/core/config.py @@ -0,0 +1,7 @@ +import os + +PROJECT_NAME = "fastapi-react-project" + +SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL") + +API_V1_STR = "/api/v1" diff --git a/fastapi-react-project/backend/app/core/security.py b/fastapi-react-project/backend/app/core/security.py new file mode 100644 index 00000000..eb0cffce --- /dev/null +++ b/fastapi-react-project/backend/app/core/security.py @@ -0,0 +1,31 @@ +import jwt +from fastapi.security import OAuth2PasswordBearer +from passlib.context import CryptContext +from datetime import datetime, timedelta + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token") + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +SECRET_KEY = "super_secret" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def create_access_token(*, data: dict, expires_delta: timedelta = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt diff --git a/fastapi-react-project/backend/app/db/__init__.py b/fastapi-react-project/backend/app/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastapi-react-project/backend/app/db/session.py b/fastapi-react-project/backend/app/db/session.py new file mode 100644 index 00000000..d7e2f6c5 --- /dev/null +++ b/fastapi-react-project/backend/app/db/session.py @@ -0,0 +1,21 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +from app.core import config + +engine = create_engine( + config.SQLALCHEMY_DATABASE_URI, +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/fastapi-react-project/backend/app/initial_data.py b/fastapi-react-project/backend/app/initial_data.py new file mode 100644 index 00000000..dc781698 --- /dev/null +++ b/fastapi-react-project/backend/app/initial_data.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +from app.db.session import get_db +from app.db.crud import create_user +from app.db.schemas import UserCreate +from app.db.session import SessionLocal + + +def init() -> None: + db = SessionLocal() + + create_user( + db, + UserCreate( + email="admin@fastapi-react-project.com", + password="password", + is_active=True, + is_superuser=True, + ), + ) + + +if __name__ == "__main__": + print("Creating superuser admin@fastapi-react-project.com") + init() + print("Superuser created") diff --git a/fastapi-react-project/backend/app/tasks.py b/fastapi-react-project/backend/app/tasks.py new file mode 100644 index 00000000..c17d5063 --- /dev/null +++ b/fastapi-react-project/backend/app/tasks.py @@ -0,0 +1,6 @@ +from app.core.celery_app import celery_app + + +@celery_app.task(acks_late=True) +def example_task(word: str) -> str: + return f"test task returns {word}" diff --git a/fastapi-react-project/backend/app/tests/__init__.py b/fastapi-react-project/backend/app/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastapi-react-project/backend/app/tests/test_main.py b/fastapi-react-project/backend/app/tests/test_main.py new file mode 100644 index 00000000..024cf9e5 --- /dev/null +++ b/fastapi-react-project/backend/app/tests/test_main.py @@ -0,0 +1,4 @@ +def test_read_main(client): + response = client.get("/api/v1") + assert response.status_code == 200 + assert response.json() == {"message": "Hello World"} diff --git a/fastapi-react-project/backend/app/tests/test_tasks.py b/fastapi-react-project/backend/app/tests/test_tasks.py new file mode 100644 index 00000000..7f49f1ca --- /dev/null +++ b/fastapi-react-project/backend/app/tests/test_tasks.py @@ -0,0 +1,6 @@ +from app import tasks + + +def test_example_task(): + task_output = tasks.example_task("Hello World") + assert task_output == "test task returns Hello World" diff --git a/fastapi-react-project/backend/conftest.py b/fastapi-react-project/backend/conftest.py new file mode 100644 index 00000000..ecd831dc --- /dev/null +++ b/fastapi-react-project/backend/conftest.py @@ -0,0 +1,169 @@ +import pytest +from sqlalchemy import create_engine, event +from sqlalchemy.orm import sessionmaker +from sqlalchemy_utils import database_exists, create_database, drop_database +from fastapi.testclient import TestClient +import typing as t + +from app.core import config, security +from app.db.session import Base, get_db +from app.db import models +from app.main import app + + +def get_test_db_url() -> str: + return f"{config.SQLALCHEMY_DATABASE_URI}_test" + + +@pytest.fixture +def test_db(): + """ + Modify the db session to automatically roll back after each test. + This is to avoid tests affecting the database state of other tests. + """ + # Connect to the test database + engine = create_engine( + get_test_db_url(), + ) + + connection = engine.connect() + trans = connection.begin() + + # Run a parent transaction that can roll back all changes + test_session_maker = sessionmaker( + autocommit=False, autoflush=False, bind=engine + ) + test_session = test_session_maker() + test_session.begin_nested() + + @event.listens_for(test_session, "after_transaction_end") + def restart_savepoint(s, transaction): + if transaction.nested and not transaction._parent.nested: + s.expire_all() + s.begin_nested() + + yield test_session + + # Roll back the parent transaction after the test is complete + test_session.close() + trans.rollback() + connection.close() + + +@pytest.fixture(scope="session", autouse=True) +def create_test_db(): + """ + Create a test database and use it for the whole test session. + """ + + test_db_url = get_test_db_url() + + # Create the test database + assert not database_exists( + test_db_url + ), "Test database already exists. Aborting tests." + create_database(test_db_url) + test_engine = create_engine(test_db_url) + Base.metadata.create_all(test_engine) + + # Run the tests + yield + + # Drop the test database + drop_database(test_db_url) + + +@pytest.fixture +def client(test_db): + """ + Get a TestClient instance that reads/write to the test database. + """ + + def get_test_db(): + yield test_db + + app.dependency_overrides[get_db] = get_test_db + + yield TestClient(app) + + +@pytest.fixture +def test_password() -> str: + return "securepassword" + + +def get_password_hash() -> str: + """ + Password hashing can be expensive so a mock will be much faster + """ + return "supersecrethash" + + +@pytest.fixture +def test_user(test_db) -> models.User: + """ + Make a test user in the database + """ + + user = models.User( + email="fake@email.com", + hashed_password=get_password_hash(), + is_active=True, + ) + test_db.add(user) + test_db.commit() + return user + + +@pytest.fixture +def test_superuser(test_db) -> models.User: + """ + Superuser for testing + """ + + user = models.User( + email="fakeadmin@email.com", + hashed_password=get_password_hash(), + is_superuser=True, + ) + test_db.add(user) + test_db.commit() + return user + + +def verify_password_mock(first: str, second: str) -> bool: + return True + + +@pytest.fixture +def user_token_headers( + client: TestClient, test_user, test_password, monkeypatch +) -> t.Dict[str, str]: + monkeypatch.setattr(security, "verify_password", verify_password_mock) + + login_data = { + "username": test_user.email, + "password": test_password, + } + r = client.post("/api/token", data=login_data) + tokens = r.json() + a_token = tokens["access_token"] + headers = {"Authorization": f"Bearer {a_token}"} + return headers + + +@pytest.fixture +def superuser_token_headers( + client: TestClient, test_superuser, test_password, monkeypatch +) -> t.Dict[str, str]: + monkeypatch.setattr(security, "verify_password", verify_password_mock) + + login_data = { + "username": test_superuser.email, + "password": test_password, + } + r = client.post("/api/token", data=login_data) + tokens = r.json() + a_token = tokens["access_token"] + headers = {"Authorization": f"Bearer {a_token}"} + return headers diff --git a/fastapi-react-project/backend/pyproject.toml b/fastapi-react-project/backend/pyproject.toml new file mode 100644 index 00000000..627a23c9 --- /dev/null +++ b/fastapi-react-project/backend/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 80 \ No newline at end of file diff --git a/fastapi-react-project/backend/requirements.txt b/fastapi-react-project/backend/requirements.txt new file mode 100644 index 00000000..7f8389d3 --- /dev/null +++ b/fastapi-react-project/backend/requirements.txt @@ -0,0 +1,19 @@ +alembic==1.4.3 +Authlib==0.14.3 +fastapi==0.65.2 +celery==5.0.0 +redis==3.5.3 +httpx==0.15.5 +ipython==7.31.1 +itsdangerous==1.1.0 +Jinja2==2.11.3 +psycopg2==2.8.6 +pytest==6.1.0 +requests==2.24.0 +SQLAlchemy==1.3.19 +uvicorn==0.12.1 +passlib==1.7.2 +bcrypt==3.2.0 +sqlalchemy-utils==0.36.8 +python-multipart==0.0.5 +pyjwt==1.7.1 \ No newline at end of file diff --git a/fastapi-react-project/docker-compose.yml b/fastapi-react-project/docker-compose.yml new file mode 100644 index 00000000..b1a41e33 --- /dev/null +++ b/fastapi-react-project/docker-compose.yml @@ -0,0 +1,71 @@ +version: '3.7' +services: + nginx: + image: nginx:1.17 + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf + ports: + - 8000:80 + depends_on: + - backend + - frontend + + redis: + image: redis + ports: + - 6379:6379 + + postgres: + image: postgres:12 + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - '5432:5432' + volumes: + - db-data:/var/lib/postgresql/data:cached + + worker: + build: + context: backend + dockerfile: Dockerfile + command: celery --app app.tasks worker --loglevel=DEBUG -Q main-queue -c 1 + + flower: + image: mher/flower + command: celery flower --broker=redis://redis:6379/0 --port=5555 + ports: + - 5555:5555 + depends_on: + - "redis" + + backend: + build: + context: backend + dockerfile: Dockerfile + command: python app/main.py + tty: true + volumes: + - ./backend:/app/:cached + - ./.docker/.ipython:/root/.ipython:cached + environment: + PYTHONPATH: . + DATABASE_URL: 'postgresql://postgres:password@postgres:5432/postgres' + depends_on: + - "postgres" + + frontend: + build: + context: frontend + dockerfile: Dockerfile + stdin_open: true + volumes: + - './frontend:/app:cached' + - './frontend/node_modules:/app/node_modules:cached' + environment: + - NODE_ENV=development + + +volumes: + db-data: diff --git a/fastapi-react-project/frontend/.dockerignore b/fastapi-react-project/frontend/.dockerignore new file mode 100644 index 00000000..25c8fdba --- /dev/null +++ b/fastapi-react-project/frontend/.dockerignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json \ No newline at end of file diff --git a/fastapi-react-project/frontend/.eslintrc.js b/fastapi-react-project/frontend/.eslintrc.js new file mode 100644 index 00000000..428e4222 --- /dev/null +++ b/fastapi-react-project/frontend/.eslintrc.js @@ -0,0 +1,51 @@ +let rules = { + 'max-len': ['error', 80, 2, { ignoreUrls: true }], + 'no-console': [0], + 'no-restricted-syntax': 'off', + 'no-continue': 'off', + 'no-underscore-dangle': 'off', + 'import/extensions': 'off', + 'import/no-unresolved': 'off', + 'operator-linebreak': 'off', + 'implicit-arrow-linebreak': 'off', + 'react/destructuring-assignment': 'off', + 'jsx-a11y/click-events-have-key-events': 'off', + 'jsx-a11y/no-static-element-interactions': 'off', + 'react/jsx-one-expression-per-line': 'off', + 'react/jsx-filename-extension': [2, { extensions: ['.ts', '.tsx'] }], + 'lines-between-class-members': [ + 'error', + 'always', + { exceptAfterSingleLine: true }, + ], +}; + +module.exports = { + extends: ['airbnb', 'plugin:prettier/recommended', 'prettier/react'], + parser: 'babel-eslint', + rules, + env: { + browser: true, + commonjs: true, + node: true, + jest: true, + es6: true, + }, + plugins: ['react', 'react-hooks', 'jsx-a11y'], + settings: { + ecmascript: 6, + jsx: true, + 'import/resolver': { + node: { + paths: ['src'], + }, + }, + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], + }, + react: { + pragma: 'React', + version: '16.8', + }, + }, +}; diff --git a/fastapi-react-project/frontend/.prettierrc.js b/fastapi-react-project/frontend/.prettierrc.js new file mode 100644 index 00000000..158883bf --- /dev/null +++ b/fastapi-react-project/frontend/.prettierrc.js @@ -0,0 +1,5 @@ +module.exports = { + printWidth: 80, + singleQuote: true, + trailingComma: 'es5', +}; diff --git a/fastapi-react-project/frontend/Dockerfile b/fastapi-react-project/frontend/Dockerfile new file mode 100644 index 00000000..d398ff4f --- /dev/null +++ b/fastapi-react-project/frontend/Dockerfile @@ -0,0 +1,16 @@ +FROM node:12 + +ADD package.json /package.json + +ENV NODE_PATH=/node_modules +ENV PATH=$PATH:/node_modules/.bin +RUN npm install + +WORKDIR /app +ADD . /app + +EXPOSE 8000 +EXPOSE 35729 + +ENTRYPOINT ["/bin/bash", "/app/run.sh"] +CMD ["start"] diff --git a/fastapi-react-project/frontend/README.md b/fastapi-react-project/frontend/README.md new file mode 100644 index 00000000..54ef0943 --- /dev/null +++ b/fastapi-react-project/frontend/README.md @@ -0,0 +1,68 @@ +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.
    +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
    +You will also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.
    +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.
    +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
    +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting + +### Analyzing the Bundle Size + +This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size + +### Making a Progressive Web App + +This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app + +### Advanced Configuration + +This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration + +### Deployment + +This section has moved here: https://facebook.github.io/create-react-app/docs/deployment + +### `npm run build` fails to minify + +This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify diff --git a/fastapi-react-project/frontend/package.json b/fastapi-react-project/frontend/package.json new file mode 100644 index 00000000..fa17afa2 --- /dev/null +++ b/fastapi-react-project/frontend/package.json @@ -0,0 +1,62 @@ +{ + "name": "fastapi-react", + "version": "0.1.0", + "private": true, + "dependencies": { + "@material-ui/lab": "^4.0.0-alpha.54", + "jwt-decode": "^3.0.0", + "ra-data-json-server": "^3.5.2", + "ra-data-simple-rest": "^3.3.2", + "react": "^16.13.1", + "react-admin": "^3.5.2", + "react-dom": "^16.13.1", + "react-router-dom": "^5.1.2", + "react-scripts": "3.4.3", + "react-truncate": "^2.4.0", + "standard": "^14.3.3" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "CI=true react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": "airbnb" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@testing-library/jest-dom": "^5.11.1", + "@testing-library/react": "^11.0.4", + "@testing-library/user-event": "^12.0.11", + "@types/jest": "^26.0.3", + "@types/jwt-decode": "^2.2.1", + "@types/node": "^14.0.1", + "@types/react": "^16.9.19", + "@types/react-dom": "^16.9.5", + "@types/react-router-dom": "^5.1.3", + "@typescript-eslint/eslint-plugin": "^2.24.0", + "@typescript-eslint/parser": "^2.24.0", + "eslint-config-airbnb": "^18.1.0", + "eslint-config-react-app": "^5.2.1", + "eslint-plugin-flowtype": "^4.6.0", + "eslint-plugin-import": "^2.20.1", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-react": "^7.19.0", + "eslint-plugin-react-hooks": "^2.5.1", + "prettier": "^2.0.5", + "react-test-renderer": "^16.13.1", + "typescript": "^4.0.2" + } +} diff --git a/fastapi-react-project/frontend/public/favicon.ico b/fastapi-react-project/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bcd5dfd67cd0361b78123e95c2dd96031f27f743 GIT binary patch literal 3150 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 MDeX2GIKR{Qf5okjU;qFB literal 0 HcmV?d00001 diff --git a/fastapi-react-project/frontend/public/index.html b/fastapi-react-project/frontend/public/index.html new file mode 100644 index 00000000..aa069f27 --- /dev/null +++ b/fastapi-react-project/frontend/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
    + + + diff --git a/fastapi-react-project/frontend/public/logo192.png b/fastapi-react-project/frontend/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/fastapi-react-project/frontend/public/manifest.json b/fastapi-react-project/frontend/public/manifest.json new file mode 100644 index 00000000..080d6c77 --- /dev/null +++ b/fastapi-react-project/frontend/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/fastapi-react-project/frontend/public/robots.txt b/fastapi-react-project/frontend/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/fastapi-react-project/frontend/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/fastapi-react-project/frontend/run.sh b/fastapi-react-project/frontend/run.sh new file mode 100644 index 00000000..5d2b8435 --- /dev/null +++ b/fastapi-react-project/frontend/run.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +case $1 in + start) + # The '| cat' is to trick Node that this is an non-TTY terminal + # then react-scripts won't clear the console. + npm start | cat + ;; + build) + npm build + ;; + test) + npm test $@ + ;; + *) + npm "$@" + ;; +esac \ No newline at end of file diff --git a/fastapi-react-project/frontend/src/App.tsx b/fastapi-react-project/frontend/src/App.tsx new file mode 100644 index 00000000..f41354b7 --- /dev/null +++ b/fastapi-react-project/frontend/src/App.tsx @@ -0,0 +1,6 @@ +import React, { FC } from 'react'; +import { Routes } from './Routes'; + +const App: FC = () => ; + +export default App; diff --git a/fastapi-react-project/frontend/src/Routes.tsx b/fastapi-react-project/frontend/src/Routes.tsx new file mode 100644 index 00000000..44cf96d2 --- /dev/null +++ b/fastapi-react-project/frontend/src/Routes.tsx @@ -0,0 +1,54 @@ +import React, { FC } from 'react'; +import { Switch, Route } from 'react-router-dom'; +import { useHistory } from 'react-router'; +import { makeStyles } from '@material-ui/core/styles'; + +import { Home, Login, SignUp, Protected, PrivateRoute } from './views'; +import { Admin } from './admin'; +import { logout } from './utils/auth'; + +const useStyles = makeStyles((theme) => ({ + app: { + textAlign: 'center', + }, + header: { + backgroundColor: '#282c34', + minHeight: '100vh', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + fontSize: 'calc(10px + 2vmin)', + color: 'white', + }, +})); + +export const Routes: FC = () => { + const classes = useStyles(); + const history = useHistory(); + + return ( + + + + + +
    +
    + + + { + logout(); + history.push('/'); + return null; + }} + /> + + +
    +
    +
    + ); +}; diff --git a/fastapi-react-project/frontend/src/__tests__/home.test.tsx b/fastapi-react-project/frontend/src/__tests__/home.test.tsx new file mode 100644 index 00000000..ef03bdef --- /dev/null +++ b/fastapi-react-project/frontend/src/__tests__/home.test.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { Home } from '../views/Home'; + +it('Home renders correctly', () => { + const home = render(); + expect(home.getByText('Admin Dashboard')).toBeInTheDocument(); + expect(home.getByText('Protected Route')).toBeInTheDocument(); + expect(home.getByText('Login')).toBeInTheDocument(); + expect(home.getByText('Sign Up')).toBeInTheDocument(); +}); diff --git a/fastapi-react-project/frontend/src/__tests__/login.test.tsx b/fastapi-react-project/frontend/src/__tests__/login.test.tsx new file mode 100644 index 00000000..1dae198d --- /dev/null +++ b/fastapi-react-project/frontend/src/__tests__/login.test.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { Login } from '../views'; + +it('Login renders correctly', () => { + const login = render(); + expect(login.getByText('Email')).toBeInTheDocument(); + expect(login.getByText('Password')).toBeInTheDocument(); + expect(login.getByText('Login')).toBeInTheDocument(); +}); diff --git a/fastapi-react-project/frontend/src/admin/Users/UserCreate.tsx b/fastapi-react-project/frontend/src/admin/Users/UserCreate.tsx new file mode 100644 index 00000000..466a5182 --- /dev/null +++ b/fastapi-react-project/frontend/src/admin/Users/UserCreate.tsx @@ -0,0 +1,21 @@ +import React, { FC } from 'react'; +import { + Create, + SimpleForm, + TextInput, + PasswordInput, + BooleanInput, +} from 'react-admin'; + +export const UserCreate: FC = (props) => ( + + + + + + + + + + +); diff --git a/fastapi-react-project/frontend/src/admin/Users/UserEdit.tsx b/fastapi-react-project/frontend/src/admin/Users/UserEdit.tsx new file mode 100644 index 00000000..7925d192 --- /dev/null +++ b/fastapi-react-project/frontend/src/admin/Users/UserEdit.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; +import { + Edit, + SimpleForm, + TextInput, + PasswordInput, + BooleanInput, +} from 'react-admin'; + +export const UserEdit: FC = (props) => ( + + + + + + + + + + + +); diff --git a/fastapi-react-project/frontend/src/admin/Users/UserList.tsx b/fastapi-react-project/frontend/src/admin/Users/UserList.tsx new file mode 100644 index 00000000..dce27f7e --- /dev/null +++ b/fastapi-react-project/frontend/src/admin/Users/UserList.tsx @@ -0,0 +1,24 @@ +// in src/users.js +import React, { FC } from 'react'; +import { + List, + Datagrid, + TextField, + BooleanField, + EmailField, + EditButton, +} from 'react-admin'; + +export const UserList: FC = (props) => ( + + + + + + + + + + + +); diff --git a/fastapi-react-project/frontend/src/admin/Users/index.ts b/fastapi-react-project/frontend/src/admin/Users/index.ts new file mode 100644 index 00000000..999f7e00 --- /dev/null +++ b/fastapi-react-project/frontend/src/admin/Users/index.ts @@ -0,0 +1,3 @@ +export * from './UserEdit'; +export * from './UserList'; +export * from './UserCreate'; diff --git a/fastapi-react-project/frontend/src/admin/authProvider.ts b/fastapi-react-project/frontend/src/admin/authProvider.ts new file mode 100644 index 00000000..1e0fe3ae --- /dev/null +++ b/fastapi-react-project/frontend/src/admin/authProvider.ts @@ -0,0 +1,55 @@ +import decodeJwt from 'jwt-decode'; + +type loginFormType = { + username: string; + password: string; +}; + +const authProvider = { + login: ({ username, password }: loginFormType) => { + let formData = new FormData(); + formData.append('username', username); + formData.append('password', password); + const request = new Request('/api/token', { + method: 'POST', + body: formData, + }); + return fetch(request) + .then((response) => { + if (response.status < 200 || response.status >= 300) { + throw new Error(response.statusText); + } + return response.json(); + }) + .then(({ access_token }) => { + const decodedToken: any = decodeJwt(access_token); + if (decodedToken.permissions !== 'admin') { + throw new Error('Forbidden'); + } + localStorage.setItem('token', access_token); + localStorage.setItem('permissions', decodedToken.permissions); + }); + }, + logout: () => { + localStorage.removeItem('token'); + localStorage.removeItem('permissions'); + return Promise.resolve(); + }, + checkError: (error: { status: number }) => { + const status = error.status; + if (status === 401 || status === 403) { + localStorage.removeItem('token'); + return Promise.reject(); + } + return Promise.resolve(); + }, + checkAuth: () => + localStorage.getItem('token') ? Promise.resolve() : Promise.reject(), + getPermissions: () => { + const role = localStorage.getItem('permissions'); + return role ? Promise.resolve(role) : Promise.reject(); + // localStorage.getItem('token') ? Promise.resolve() : Promise.reject(), + }, +}; + +export default authProvider; diff --git a/fastapi-react-project/frontend/src/admin/index.ts b/fastapi-react-project/frontend/src/admin/index.ts new file mode 100644 index 00000000..c956a8fd --- /dev/null +++ b/fastapi-react-project/frontend/src/admin/index.ts @@ -0,0 +1 @@ +export * from './Admin'; diff --git a/fastapi-react-project/frontend/src/config/index.tsx b/fastapi-react-project/frontend/src/config/index.tsx new file mode 100644 index 00000000..31dc509f --- /dev/null +++ b/fastapi-react-project/frontend/src/config/index.tsx @@ -0,0 +1,3 @@ +export const BASE_URL: string = 'http://localhost:8000'; +export const BACKEND_URL: string = + 'http://localhost:8000/api/v1'; diff --git a/fastapi-react-project/frontend/src/decs.d.ts b/fastapi-react-project/frontend/src/decs.d.ts new file mode 100644 index 00000000..5557bb84 --- /dev/null +++ b/fastapi-react-project/frontend/src/decs.d.ts @@ -0,0 +1 @@ +declare module 'react-admin'; diff --git a/fastapi-react-project/frontend/src/index.css b/fastapi-react-project/frontend/src/index.css new file mode 100644 index 00000000..ec2585e8 --- /dev/null +++ b/fastapi-react-project/frontend/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/fastapi-react-project/frontend/src/index.tsx b/fastapi-react-project/frontend/src/index.tsx new file mode 100644 index 00000000..9a6816b7 --- /dev/null +++ b/fastapi-react-project/frontend/src/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter as Router } from 'react-router-dom'; +import './index.css'; +import App from './App'; + +ReactDOM.render( + + + , + document.getElementById('root') +); diff --git a/fastapi-react-project/frontend/src/logo.svg b/fastapi-react-project/frontend/src/logo.svg new file mode 100644 index 00000000..6b60c104 --- /dev/null +++ b/fastapi-react-project/frontend/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/fastapi-react-project/frontend/src/react-app-env.d.ts b/fastapi-react-project/frontend/src/react-app-env.d.ts new file mode 100644 index 00000000..6431bc5f --- /dev/null +++ b/fastapi-react-project/frontend/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/fastapi-react-project/frontend/src/utils/api.ts b/fastapi-react-project/frontend/src/utils/api.ts new file mode 100644 index 00000000..6b7e2f0a --- /dev/null +++ b/fastapi-react-project/frontend/src/utils/api.ts @@ -0,0 +1,13 @@ +import { BACKEND_URL } from '../config'; + +export const getMessage = async () => { + const response = await fetch(BACKEND_URL); + + const data = await response.json(); + + if (data.message) { + return data.message; + } + + return Promise.reject('Failed to get message from backend'); +}; diff --git a/fastapi-react-project/frontend/src/utils/auth.ts b/fastapi-react-project/frontend/src/utils/auth.ts new file mode 100644 index 00000000..bbcf39eb --- /dev/null +++ b/fastapi-react-project/frontend/src/utils/auth.ts @@ -0,0 +1,118 @@ +import decodeJwt from 'jwt-decode'; + +export const isAuthenticated = () => { + const permissions = localStorage.getItem('permissions'); + if (!permissions) { + return false; + } + return permissions === 'user' || permissions === 'admin' ? true : false; +}; + +/** + * Login to backend and store JSON web token on success + * + * @param email + * @param password + * @returns JSON data containing access token on success + * @throws Error on http errors or failed attempts + */ +export const login = async (email: string, password: string) => { + // Assert email or password is not empty + if (!(email.length > 0) || !(password.length > 0)) { + throw new Error('Email or password was not provided'); + } + const formData = new FormData(); + // OAuth2 expects form data, not JSON data + formData.append('username', email); + formData.append('password', password); + + const request = new Request('/api/token', { + method: 'POST', + body: formData, + }); + + const response = await fetch(request); + + if (response.status === 500) { + throw new Error('Internal server error'); + } + + const data = await response.json(); + + if (response.status > 400 && response.status < 500) { + if (data.detail) { + throw data.detail; + } + throw data; + } + + if ('access_token' in data) { + const decodedToken: any = decodeJwt(data['access_token']); + localStorage.setItem('token', data['access_token']); + localStorage.setItem('permissions', decodedToken.permissions); + } + + return data; +}; + +/** + * Sign up via backend and store JSON web token on success + * + * @param email + * @param password + * @returns JSON data containing access token on success + * @throws Error on http errors or failed attempts + */ +export const signUp = async ( + email: string, + password: string, + passwordConfirmation: string +) => { + // Assert email or password or password confirmation is not empty + if (!(email.length > 0)) { + throw new Error('Email was not provided'); + } + if (!(password.length > 0)) { + throw new Error('Password was not provided'); + } + if (!(passwordConfirmation.length > 0)) { + throw new Error('Password confirmation was not provided'); + } + + const formData = new FormData(); + // OAuth2 expects form data, not JSON data + formData.append('username', email); + formData.append('password', password); + + const request = new Request('/api/signup', { + method: 'POST', + body: formData, + }); + + const response = await fetch(request); + + if (response.status === 500) { + throw new Error('Internal server error'); + } + + const data = await response.json(); + if (response.status > 400 && response.status < 500) { + if (data.detail) { + throw data.detail; + } + throw data; + } + + if ('access_token' in data) { + const decodedToken: any = decodeJwt(data['access_token']); + localStorage.setItem('token', data['access_token']); + localStorage.setItem('permissions', decodedToken.permissions); + } + + return data; +}; + +export const logout = () => { + localStorage.removeItem('token'); + localStorage.removeItem('permissions'); +}; diff --git a/fastapi-react-project/frontend/src/utils/index.ts b/fastapi-react-project/frontend/src/utils/index.ts new file mode 100644 index 00000000..abb0c9d6 --- /dev/null +++ b/fastapi-react-project/frontend/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './auth'; +export * from './api'; diff --git a/fastapi-react-project/frontend/src/views/Home.tsx b/fastapi-react-project/frontend/src/views/Home.tsx new file mode 100644 index 00000000..2a239743 --- /dev/null +++ b/fastapi-react-project/frontend/src/views/Home.tsx @@ -0,0 +1,66 @@ +import React, { FC, useState } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; + +import { getMessage } from '../utils/api'; +import { isAuthenticated } from '../utils/auth'; + +const useStyles = makeStyles((theme) => ({ + link: { + color: '#61dafb', + }, +})); + +export const Home: FC = () => { + const [message, setMessage] = useState(''); + const [error, setError] = useState(''); + const classes = useStyles(); + + const queryBackend = async () => { + try { + const message = await getMessage(); + setMessage(message); + } catch (err) { + setError(String(err)); + } + }; + + return ( + <> + {!message && !error && ( + queryBackend()}> + Click to make request to backend + + )} + {message && ( +

    + {message} +

    + )} + {error && ( +

    + Error: {error} +

    + )} + + Admin Dashboard + + + Protected Route + + {isAuthenticated() ? ( + + Logout + + ) : ( + <> + + Login + + + Sign Up + + + )} + + ); +}; diff --git a/fastapi-react-project/frontend/src/views/Login.tsx b/fastapi-react-project/frontend/src/views/Login.tsx new file mode 100644 index 00000000..26298c8a --- /dev/null +++ b/fastapi-react-project/frontend/src/views/Login.tsx @@ -0,0 +1,151 @@ +import React, { FC, useState } from 'react'; +import { + Paper, + Grid, + TextField, + Button, + FormControlLabel, + Checkbox, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { Face, Fingerprint } from '@material-ui/icons'; +import { Alert } from '@material-ui/lab'; +import { Redirect } from 'react-router-dom'; +import { useHistory } from 'react-router'; + +import { login, isAuthenticated } from '../utils/auth'; + +const useStyles = makeStyles((theme) => ({ + margin: { + margin: theme.spacing(2), + }, + padding: { + padding: theme.spacing(1), + }, + button: { + textTransform: 'none', + }, + marginTop: { + marginTop: 10, + }, +})); + +export const Login: FC = () => { + const classes = useStyles(); + const history = useHistory(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + + const handleSubmit = async (_: React.MouseEvent) => { + setError(''); + try { + const data = await login(email, password); + + if (data) { + history.push('/'); + } + } catch (err) { + if (err instanceof Error) { + // handle errors thrown from frontend + setError(err.message); + } else { + // handle errors thrown from backend + setError(String(err)); + } + } + }; + + return isAuthenticated() ? ( + + ) : ( + +
    + + + + + + ) => + setEmail(e.currentTarget.value) + } + fullWidth + autoFocus + required + /> + + + + + + + + ) => + setPassword(e.currentTarget.value) + } + fullWidth + required + /> + + +
    + + {error && ( + + {error} + + )} + + + + } + label="Remember me" + /> + + + + + + + {' '} + {' '} +   + + +
    +
    + ); +}; diff --git a/fastapi-react-project/frontend/src/views/PrivateRoute.tsx b/fastapi-react-project/frontend/src/views/PrivateRoute.tsx new file mode 100644 index 00000000..285fc5fd --- /dev/null +++ b/fastapi-react-project/frontend/src/views/PrivateRoute.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import { Route, Redirect } from 'react-router-dom'; + +import { isAuthenticated } from '../utils/auth'; + +type PrivateRouteType = { + component: React.ComponentType; + path?: string | string[]; +}; + +export const PrivateRoute: FC = ({ + component, + ...rest +}: any) => ( + + isAuthenticated() === true ? ( + React.createElement(component, props) + ) : ( + + ) + } + /> +); diff --git a/fastapi-react-project/frontend/src/views/Protected.tsx b/fastapi-react-project/frontend/src/views/Protected.tsx new file mode 100644 index 00000000..078414ad --- /dev/null +++ b/fastapi-react-project/frontend/src/views/Protected.tsx @@ -0,0 +1,5 @@ +import React, { FC } from 'react'; + +export const Protected: FC = () => { + return

    This component is protected

    ; +}; diff --git a/fastapi-react-project/frontend/src/views/SignUp.tsx b/fastapi-react-project/frontend/src/views/SignUp.tsx new file mode 100644 index 00000000..0ede11ee --- /dev/null +++ b/fastapi-react-project/frontend/src/views/SignUp.tsx @@ -0,0 +1,138 @@ +import React, { FC, useState } from 'react'; +import { Paper, Grid, TextField, Button } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { Face, Fingerprint } from '@material-ui/icons'; +import { Alert } from '@material-ui/lab'; +import { Redirect } from 'react-router-dom'; +import { useHistory } from 'react-router'; + +import { signUp, isAuthenticated } from '../utils/auth'; + +const useStyles = makeStyles((theme) => ({ + margin: { + margin: theme.spacing(2), + }, + padding: { + padding: theme.spacing(1), + }, + button: { + textTransform: 'none', + }, + marginTop: { + marginTop: 10, + }, +})); + +export const SignUp: FC = () => { + const classes = useStyles(); + const history = useHistory(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [passwordConfirmation, setPasswordConfirmation] = useState(''); + const [error, setError] = useState(''); + + const handleSubmit = async (_: React.MouseEvent) => { + // Password confirmation validation + if (password !== passwordConfirmation) setError('Passwords do not match'); + else { + setError(''); + try { + const data = await signUp(email, password, passwordConfirmation); + + if (data) { + history.push('/'); + } + } catch (err) { + if (err instanceof Error) { + // handle errors thrown from frontend + setError(err.message); + } else { + // handle errors thrown from backend + setError(String(err)); + } + } + } + }; + + return isAuthenticated() ? ( + + ) : ( + +
    + + + + + + ) => + setEmail(e.currentTarget.value) + } + fullWidth + autoFocus + required + /> + + + + + + + + ) => + setPassword(e.currentTarget.value) + } + fullWidth + required + /> + + + + + + + + ) => + setPasswordConfirmation(e.currentTarget.value) + } + fullWidth + required + /> + + +
    + + {error && ( + + {error} + + )} + + + + +
    +
    + ); +}; diff --git a/fastapi-react-project/frontend/src/views/index.ts b/fastapi-react-project/frontend/src/views/index.ts new file mode 100644 index 00000000..797586c5 --- /dev/null +++ b/fastapi-react-project/frontend/src/views/index.ts @@ -0,0 +1,5 @@ +export * from './Home'; +export * from './Login'; +export * from './SignUp'; +export * from './Protected'; +export * from './PrivateRoute'; diff --git a/fastapi-react-project/frontend/tsconfig.json b/fastapi-react-project/frontend/tsconfig.json new file mode 100644 index 00000000..4a41017b --- /dev/null +++ b/fastapi-react-project/frontend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": ["src", "decs.d.ts"] +} diff --git a/fastapi-react-project/nginx/nginx.conf b/fastapi-react-project/nginx/nginx.conf new file mode 100644 index 00000000..10a3d32d --- /dev/null +++ b/fastapi-react-project/nginx/nginx.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name fastapi-react-project; + + location / { + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_pass http://frontend:3000; + + proxy_redirect off; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location /api { + proxy_pass http://backend:8888/api; + } +} diff --git a/fastapi-react-project/scripts/build.sh b/fastapi-react-project/scripts/build.sh new file mode 100755 index 00000000..77a36455 --- /dev/null +++ b/fastapi-react-project/scripts/build.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Exit in case of error +set -e + +# Build and run containers +docker-compose up -d + +# Hack to wait for postgres container to be up before running alembic migrations +sleep 5; + +# Run migrations +docker-compose run --rm backend alembic upgrade head + +# Create initial data +docker-compose run --rm backend python3 app/initial_data.py \ No newline at end of file diff --git a/fastapi-react-project/scripts/test.sh b/fastapi-react-project/scripts/test.sh new file mode 100755 index 00000000..9f1d0d33 --- /dev/null +++ b/fastapi-react-project/scripts/test.sh @@ -0,0 +1,7 @@ +#! /usr/bin/env bash + +# Exit in case of error +set -e + +docker-compose run backend pytest +docker-compose run frontend test \ No newline at end of file diff --git a/fastapi-react-project/scripts/test_backend.sh b/fastapi-react-project/scripts/test_backend.sh new file mode 100644 index 00000000..b5fb2c2e --- /dev/null +++ b/fastapi-react-project/scripts/test_backend.sh @@ -0,0 +1,6 @@ +#! /usr/bin/env bash + +# Exit in case of error +set -e + +docker-compose run backend pytest $@ \ No newline at end of file diff --git a/pyramid_scaffold/.coveragerc b/pyramid_scaffold/.coveragerc new file mode 100644 index 00000000..12edc761 --- /dev/null +++ b/pyramid_scaffold/.coveragerc @@ -0,0 +1,2 @@ +[run] +source = pyramid_scaffold diff --git a/pyramid_scaffold/.gitignore b/pyramid_scaffold/.gitignore new file mode 100644 index 00000000..e9336274 --- /dev/null +++ b/pyramid_scaffold/.gitignore @@ -0,0 +1,22 @@ +*.egg +*.egg-info +*.pyc +*$py.class +*~ +.coverage +coverage.xml +build/ +dist/ +.tox/ +nosetests.xml +env*/ +tmp/ +Data*.fs* +*.sublime-project +*.sublime-workspace +.*.sw? +.sw? +.DS_Store +coverage +test +*.sqlite diff --git a/pyramid_scaffold/CHANGES.txt b/pyramid_scaffold/CHANGES.txt new file mode 100644 index 00000000..14b902fd --- /dev/null +++ b/pyramid_scaffold/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version. diff --git a/pyramid_scaffold/MANIFEST.in b/pyramid_scaffold/MANIFEST.in new file mode 100644 index 00000000..1687278c --- /dev/null +++ b/pyramid_scaffold/MANIFEST.in @@ -0,0 +1,5 @@ +include *.txt *.ini *.cfg *.rst +recursive-include pyramid_scaffold *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 +recursive-include tests * +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/pyramid_scaffold/README.txt b/pyramid_scaffold/README.txt new file mode 100644 index 00000000..2fcd679c --- /dev/null +++ b/pyramid_scaffold/README.txt @@ -0,0 +1,30 @@ +Pyramid Scaffold +================ + +Getting Started +--------------- + +- Change directory into your newly created project if not already there. Your + current directory should be the same as this README.txt file and setup.py. + + cd pyramid_scaffold + +- Create a Python virtual environment, if not already created. + + python3 -m venv env + +- Upgrade packaging tools, if necessary. + + env/bin/pip install --upgrade pip setuptools + +- Install the project in editable mode with its testing requirements. + + env/bin/pip install -e ".[testing]" + +- Run your project's tests. + + env/bin/pytest + +- Run your project. + + env/bin/pserve development.ini diff --git a/pyramid_scaffold/development.ini b/pyramid_scaffold/development.ini new file mode 100644 index 00000000..c69db2ca --- /dev/null +++ b/pyramid_scaffold/development.ini @@ -0,0 +1,59 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:pyramid_scaffold + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = localhost:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, pyramid_scaffold + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_pyramid_scaffold] +level = DEBUG +handlers = +qualname = pyramid_scaffold + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/pyramid_scaffold/production.ini b/pyramid_scaffold/production.ini new file mode 100644 index 00000000..c105c815 --- /dev/null +++ b/pyramid_scaffold/production.ini @@ -0,0 +1,53 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:pyramid_scaffold + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = *:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, pyramid_scaffold + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_pyramid_scaffold] +level = WARN +handlers = +qualname = pyramid_scaffold + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/pyramid_scaffold/pyramid_scaffold/__init__.py b/pyramid_scaffold/pyramid_scaffold/__init__.py new file mode 100644 index 00000000..a3d5a646 --- /dev/null +++ b/pyramid_scaffold/pyramid_scaffold/__init__.py @@ -0,0 +1,11 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + with Configurator(settings=settings) as config: + config.include('pyramid_jinja2') + config.include('.routes') + config.scan() + return config.make_wsgi_app() diff --git a/pyramid_scaffold/pyramid_scaffold/routes.py b/pyramid_scaffold/pyramid_scaffold/routes.py new file mode 100644 index 00000000..25504ad4 --- /dev/null +++ b/pyramid_scaffold/pyramid_scaffold/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') diff --git a/pyramid_scaffold/pyramid_scaffold/static/pyramid-16x16.png b/pyramid_scaffold/pyramid_scaffold/static/pyramid-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..979203112e76ba4cfdb8cd6f108f4275e987d99a GIT binary patch literal 1319 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xd_B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxR5#hc&_u!9QqR!T z(8R(}N5ROz&{*HVSl`fC*U-qyz|zXlQ~?TIxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5 zFD$Tv3bSNU;+l1ennz|zM-B0$V)JVzP|XC=H|jx7ncO3BHWAB;NpiyW)Z+ZoqGVvir744~DzI`cN=+=uFAB-e&w+(vKt_H^esM;Afr7KMf`)Hma%LWg zuL;)R>ucqiS6q^qmz?V9Vygr+LN7Bj#mdRl*wM}0&C<--)xyxw)!5S9(bdAp)YZVy zz`)Yd%><^`B|o_|H#M&WrZ)wl*Ab^)P+G_>0NU)5T9jFqn&MWJpQ`}&vsET;x0vHJ z52`l>w_7Z5>eUB2MjsTjNHGl)0wy026P|8?9C*r4%>yR)B4E1CQ{KVEz`!`m)5S5Q z;#SXOUk#T)k>lxKj4~&v95M;sSJp8lNikLV)bX~~P1`qaNLZPZ<6^QgASli8jmk<% zZdJ~1%}JBkHhu^^q;ghbe{lMjv_0X)ug#yI+xz_S{hiM(g*saf_f6Pt#!wis>2u+! z{;RjXUlmP?^Kq|yJMn}2uJ~!L3EU-*{+zn6Ya6$jYueOB4G#Lx+=R;oM4sqe*Sq0$ zQ2Nf{-BFQxlX@Dog;`l=SkyQh`W)n22F}BoCTQYYC=p8g*kiK(1`FEnD$cY`NZX8<>GJicU)2$Vj5iRl-7k*od z3K)m{Gsp@4JM)eDo7z=gtG;>hu{QvcXxLU8eD@D+$BKJ@RM`zyYKvG z-8a3ur|aAMt1Vr-yH|CEDauQLkO+_f002lzQdIf%fB4Ui0QY*V)U3(^0FZ<%L_`#& zL`1-fj&^1i)}{b}Bq%eIA9uoG!laCfFcwoR zb`OTl9xm%u?u}UK6Z+-0LfvF1uNzRlu;BSs+a-xXQEAzvn#Z125}lrEE$o@!cYog? z@lko^ANF`uyQDsu%o2*s(%P^-sbKEJ1>90;Svb?qHr@sbgo4>hFv21pO(baNe1U?G_am z$%uaYhJupCKc=2k-Lpftu1m0%A~@dHZKRf6W*s6Qm&D`7K|3 zP8#?(KABe7=AZNd-k*6CTcqHJ?f3yA6ws8mf*wHc;}7VpNW)zn=9RJ4PSI>0zxN+V zk#)jtw`7ILRrYRCqD>sB@)+LaZvtH~TpCmeT z5;T(}&;kNeCnT`+Is{plpj-ki?E!QC9#b�i5=5IxreNAbVsKKM4p@aIXvt)VjX~ zLcj$&PM%O%3~m8hs_+6jp*DiMh>#*THuP7Kuo(0>$o&*`2|it5S+0m8|22g(K^uZ@ z;6o1l6qp_E8Ol2dBLz5X2wDO(`F*c>PlO=RH?}G2hLZu0*R!%E-GVEC+T4e?MR);V z_^jU-j{q4)fSwlDL?FBr6^_xQgu)=RiX|@qmWrjtpcW9eMoGpx>_EeXqAIZV+wop(eQ& zddcwQJrU|q&zm1a_C786I&8KaRWQwHi;?Yq$Niu!>Pxo{x^?XH0JL7G3nMSGE+k(f zUy_Yz(!p+;7({Its{k~zBrv5lr7AiB!al-t5Jn%nl7ESUGkGw&`+$zo+uAQnLLE{> z)bjDzQo)pX%9L+Y8~jzJEXj4L`Kdd};zxK*BpmUzAbJW_l-Xc?DzrF3#ROVvYz1i| zG2!p>JkqTYcZj=4p)#n%c22V_r7crip;Odb+M8J-{$29VK@ZtjSD$_LrTfkfWNmFpri8%bWfq{-bz; zG=eUIHw0<~$?St1Z_;ejM$&fE_SuIT%(amlVYGL(_Z#(C5>wBQegW7bxl~NRGd`Qh@8sO z+`6hk+hoHeiq)PuHG4Tn`%qrZs+LxT_(Bd(Ki{xdzI*yTJu-iUW<)0L8m>OWDT4~* zF$1aATP;{kn}(yBhyLY(G%H_1)}q=hRjbx3!NSzR4{{?Yj)v46H5je}8Uyq(_rMi`oz6O&CSQn6^7ABOjKl`T{3!jW>_L33Rec#ReVI^tJu7RoS3IrvY1S=CWBV} zj(DVYB)Etlmy{64lhVbp^w-RqOvv`h52Wogrgu6?^(V`Yjk~2|lT|VLy;=@*B!r~I z8|W`#Sbe3tvQ^jmt**N;i}CFtk8%5h^!rhlx_72eu`tO&bwSgj$pgA!#!^*MI8xg{ z1);{xPj&iN{yU`!F$wu^-<3|6j#~sZ+%?P!QyGTW(CfbAr|D$wXU}I5X&beeKU2fX zgG|TD(mH9GwWoafEqfywNtsR+sD)f_S-1XC!ZdqS=^Mu0^-kK3?HKXM&yhzT4l@qd zPanHneg{AGa-3PAR(@Wn(phPhch&7}+q&sGjVCclej0Ri%*4SHsn< zivG#tyrZ`6kG}f8qNkFVv6B*?B?^c7qCd^QpIhWA;Y#4_i;5ep-F6tVd)~Ye@x&@W zRD74;dI!Tz#&h{&=#KO}3x)5yd$@PmAOxpk0jGthtmnp|-)tuF z1Tmvv`is|f*M_*fh^p4)UD+SflPZC8Hjg7w~i z(0ycHzisp0{qmAY2ps|UaK_Z-`J%VVf9SpbJPluprYHE#gZtV1+4y8Tj|NGBE~`wi z@_GJl(X6!d`Xp!3V6r~+V{~wf2=hzgeYHYA>}2UAy?BH8kwm4$WaNG1nn&&R*Nd^p z+GwW(`UQ`^uUfv~m z>;IhlXnZ{sdw8O7r;wN(CFtsf_;lq)ZDY2#@hj-(BO9-l&+9uSqP?V+699mW^=F3y zq-Ed(05Fsms+!K4an_%9V_D}HiKIYqFDouet3gNdDqgq~Fhlhumg^ihwjqz23(aGJ`+0c#A)`{X@o%~NfqNYy9ju!UL z7IwDaKm8gS*?n^6Cnx`7=s&-I`RQz7_P>^Fo&FuxYkS4<|x%%;|+Hm0`DPOm)H|7z|vxBnsje@?m?+W*VgUrGE|YPxyZ`@-LQ%osGStsgu(yO@QOyl)q#D)Ytr9GXh*} z|0et${3k)d(c(2y!#{rg$EUwz|J2v|ZwCGj{*CY_^}LD}Zl>0nq86_S{VNJK78X9{ z|0?+>Q^d~N&QZnQ(Ae~kXMa)t2K`g}FFRWQr=7n^{>C&h=5_jHWNB*b{I~1%de#0K z{lbPHng0g!G5=R>zSpt9D`#h7VdgGs=xi#$#=^?Z$im9V@=leFg_nhumy?^1`5!ue z^Wcv}#L?8y+0Ieb&dyrkuP|)>G{NtfUNiMi`M;@r%zx_WZ*}#rqWuefty%%3SLXlR z0R)f+r_;Fr0E#pzQ6W_~sMAcu7M!n%L-`n@_FrK&I5Ctcs4eqYZOO14!psI+MDrsT(i5x*g|To^~3a zG(P=0{jmS|yL#iajCX&owCt=*MaK8c$v*)0zil)1J#>d*NO7`^HAY{0d&~02KKt_%Xg6cfO#nv8b)Ua-40z&0(uXf(uOcr&ku?L3B3P?hlUS z>VhrlISxGTaoh|P?^iWWhV%lXOrhv(^;xDR%1buy`MO;#8IECW(wBj%OM06l;o&Dg z_t{hIHs-|9QteQX6_vQ46z;7-9BzVR&x{29(n4cJ4FDWf=&N)~)lGI=E7`02B6go) z=iiKw-6unW%A7Be3W)ESUXn($gAMbhoKh88cjXAs*zc36EQ}lgSmAairK0&GdX3ZA zwh(VLk^Kt7kZ%j{M3uh4)DPeep>H;(#PQ7%c$oa4rY89lnqJwZvz`woVWhWTw+Yt4 zK4Z?lOIC7fdL{7TL>YLd1VYhMkf)>>sG7<6sCi@j;dDj?-g`#>pw+-!OoAG9N4i|~ zeV<%)x8PqKB+Fotdk_^bJG$@J!o42!876Z5@8~BdYV^iQA4M~MF4<$54r~0Ff_Q1&*4xzmrX1KOYSRpELIw>?DZ)mm zFK_GBShr5fn}g46mJx_Pas|Y>PqDsQE4&b>`1w%aGo;@X@s;Nl*lk5$5MGxo&fZa~ z?)Y9Rmi-z7&KQGc_gS>J^@R5LdoGtFY8iZj&|=_6q0RQ1g;q89e5r$5_4S0&a)DQ` z7*pE~UrO~c)K@vEAM(}0siTPqLN|~uSWfh>==;JS=?6%x67xnVLg0SX0;dw)(QYaD z!fQ;uWtNbQKGxM3j!x(;)P87Q6_D+-sl;lR%V{3cV~^olt7GJBzJR;b=~9(!Rb>Wf zO@=*jvZGFZCSDq<2iV0<23iTJvt#ydEoHlX#ZxXcgrYkL-o%LE_o$6FtCN9 zlcTA@S;BN?S9l{x?dSoWnVOEvAClv9$$|Pd-7H34>`>0(90|@(*RN^71(;U|IgZZ) zZug2l923m((p(=P&l2{WO{6xPmUIw_2oyDR68pd+CusR0wZ5CG&93)<%Lx8~uxYBm ze-G zNw<06f}q@gVA8Qb(}fP=Zu`?e&__018nWFT8S_5OBn%=uSYRS5z!Bb0v0gC5z?M+* z_hR*MbOQQ+cgez@-}LxHbl-C%xo<)%N|8DmlT8`Ch}#1YLRj4BQiMS>rL+&$SErCb zds6l{MUU?;ZrUiKy`0766{bKXSIGo^Ur-X?36(qO3!!T2I~D=KRMED9r}@R{l2M(Nv?cylDF1VI^29xSqn9TeS38h9L$J4&L>Ec<7Uza28R zX>DFt@0RIAo;Z~C(DO?P2CgmMFc7o>af=(<_XSJ7XHM%!_z8lwM47LPb15t<5}EP5 z(mBmYkczz4-Y%%-gr%#24WEK|C<>&1R1{AK*K5Ez1`cC0Dki|i<&CI^$DQ{58q!G1 zPkF)4^*3=x|5^040+&pK3i-8JQXY?kV@V~fF8-~4m7G1M^$kG_!tL0U*FBzY5Ku26 zH+A2H_I;>)FHp=JtWv9jOA~xBFp&B-K?yxJ_m9=}9v>~|dgrdW<2OmV=$Qe3usLq( zLW6Q?4BnLGv923wp)FSzT=P4)zN}C*){RW?sYu>A#ATVSzWl9_XRhmuA0o;OD7 zLxy8cz=xcz4KN*P)($VF2fn0LjQ~pO@)};rCNAwaQ9Olf)P#9+`_O$hLg<%%bMP47 zSgn!5??!W#2%1kVL8EGD-Kmf-L2nP*GpusVngEF=P8VpK5!C(M5ual@B5=GPporHm z=t{(2cJ{dK73HYOa@-jpVoMmZ@KsX#8t(7s2bzMWOw}%oFQ{3_Y;e>?kl;tZ6SSLO zmcn?H9Wl^ru#<={zr-R7C#&^*-!wLmsgvRU#*0Ulnv1C#@Tu4Sf-_WxH&KaiVUmUZ zR5X7Q9J8~eocTMC^+S$XGXTdBFQi;5u< zuUs!xDLz2~RK=mVMtBYMpijukBfad#z-48x%E81X&rY9`$0U%1^mg^? z>TX2JO+)D9AXLYF{F&M<9#$!4+xse7j^4^-9YedAJ4L^F-PJ{;L=WaM z;CK&Bt-!AJK9kVQfq1dGvtVdg#zaF|VRwZD9#FJ8iXkihP* zM@V+&9q|$&`z|z3lYcs6E*-+uM>Hcf>=|$57C!!~BW_baR7XD@psemUQ5y%kr>yd1 zm8R927JKP-M5?1q?ZnPx~YXH0ZFCq=^@gs+bs?4^}Op`zA_CBxkPj6Az z;MI_gpZMYpTd<|@Mb}Fa{lJ|9ss`EgWag+hOWFkr`ZK4QWV<~AlI>!wr0lSSbV~5M?-~y9)8}?rjttq? z?^rV9;r(VVgQc|x4RH8HiBL$Xx&jpLq#W=yr1G14m#KFleBO;R`i+iqmIV?i2Qg|H z!YA+}qi!5KM-5*Ct%AhX7L=b!Fk;WZUfQA=B?fYSdi{{^&I)6!1IbRk93xWB*ioWC z-?tV_L00i(^fhn8M_lA)Ko!YJ()=M8^^mw{;%=H}o12}4K5crtox2ij!9g_=0Xe3< zID(mRnOuw1VR-}i9O*)uJe?#bf3vhkA@etj43hODQnYmrv0UzhO`^lFJ-1}P;Ac?$ zhiHPOZ>h5;`B~^>#-nzw0Fl9#U{xfJP*TACY!(t=8uUk?VW*nFi3j;vW| zxKM;M)HFBQDik}!miY#WErQKTN!Q*z?wcS*9tmabvs|u;E>15Q2dUyGN-b@LD@g*q zxa4N5vkh>HdiAekp$y&A`I`z15?W+r#REcsM!bqP%YJ80Y%MQ7@{cJM z${FeHRrCiGz;e}b{EqW5)pyhjnT+AUs*_%3>c*71W<-mp=jrq=n-|I;DRkz0NMxgPIsuX!Y zR7vp%sdh$oStk(aqcqV?4H9HKi(0<1#1S=HfO_w@=65S|gGcS03Fw+|mgy%zoO{()m0} z>pAmc+1M0RbgWr&WvFv|yn%jBRqVrr_U;2-)vrD~xJ6k6;)b#u<|!;Kc*Tw_WsJMh zh#POWb=0nym|w~>*-(?kSXUNZJJ@{-n~a>(h&!cg!s(_6AI94!uX?&F-##~?~` z-~KI!D`FZ@9lPFp_&LWAt5Ug{V(<6!o2?iv1fuQ-p)HK6;`KD^3gqU5==q~=+u<_{}p9aZZg%uQAoDv{B!5p!x? z?Yz%z9yZ?P(tX@%>`f?*m$JrC*0rn*Sn7>p}n{_>4w}@QdRjgr$IbLT@0B z0Oc`npAp|qbd?1666a>DtY=5;QPnFpX+E7#RCSOyY}y_At!bo}rNiExdV($9kFsJ# z5(^aR!wwzNf+!vZO%`?N+MBYG_=G{CA*6n@*p>QRKVC>-UYv`gn*zqWSE)qvor{J0Y39m3U+bmeAu*LrMJx-`c@_H+Md^&Uao z*%a0orrd6}m9!vAH36E1zL;zOUHC1E`^ECUZDZ_0A~E17Dw>4q33h5q)O+fK&PwW| zpPZl0($)I-E}bki!H0=GZ7=few9+nw7V;7D0u*BJZ#PPu` zlA(UtVA`W21{#bu8Yk`(qWRel%T%s*TEls6i1R98B+yaoxK}IMIrP@NMsRqyf1?yM zVnSn2At^0mnDY#-4Qpjg$drVpRBz3Rs?#6U&N_mc-!h?S-62gqi14bK}%x=*@5ABC$@^)0|}3t=BxoOn@pl_}$J#d;E@;1%Ww zwT%8|h*_+45u3nzqKub-PLF!;LA-(HA_s1gQ+7MFcviUpD8hPx%s0Zb1__vV)25W5 zS8{t2u3;1^9m!;F=SKmH&O9g)-TOhZF0c~7o7DNtsbdX0Tn;$h`NWy72*Mr#gHQS# zxa;YG>k!+`V6gK%<|AxR0=w-BYvnhxV_;6*wa{Yk+{0;B$x3X}7Ts=)lDo|UGy%$3 ze(nY-aNI$*KmL+r5rTu;W1F^>jJy^sKai!dL>Y>OxAuj4P4bm5R=V>w`O3fgBeEjm zd~78>4=abld8DYhhvcp(qB}w@gIuMGrdY2^bMsi?&x!AEHF{hc zg+DNk`;;y^5U@qHYv=l>ylIkSKv`8XcdBRg;rpD%iSyg|$79xK4KALACso;aG|J4{ zM=o}BWcpcJ_KWUFN?+hr{n&QgXhF{Bw41yMii;`_47xsc=oiVa{2v8tGY5O%13zW5 zMwu0SaukgGf@_5T!7pGgvWky^G(b8|<3Q8c?2Uxz;%QICsI2NeBU(~c(>lU$tH6(C z!?)hYYB0_>NwsFik*o@lw6(twlS4|#5OmRiv&TD9A6z@RJWNVz&4kkC_)Ex;=+C8% zhHW9oet8E%$zeE0Lx)l| z=wi>IIXK<#^2zteGFqb+r$okBf*p0SN|+e-y7uB{@}g!jRmHwcAD#4Zq9gfyip+yb zg$p2O6nybR$|(m`%9&h-k;~b0WPC*SWDv0 z1&u*-B!%bl&r%mubHAmK3X&*R)ku>#Sn8&-cW zK2P^rNE3`Q0+iXeUh@BCMUQms&JW=t9&VE&B=j{C_Mt z$mL|laTbhQgi+&2b%Qj`DcB8l_!W28*z<;=$}o~q$YZ5ib#+OCo=#EfVrgPSawAWd z%WVsIlIueGb^Y3(soWx^r_q}!j>S~mK`W5qoTfHg!!)Hpx3H`rxF;vV&q!5gyXfe} z0w`eJ`B@bPHZN8O{n}7ct|M7Y)O;n&H{BRtSwk_lvB@p=mUnMu$2+*_ZDam<*g2lV)D*CN+d5a+`ovbJ*R=J07&JhgO=jzjd`-bU(l369g@NNrgdz_k zKoFKRlMxmvxJ37=Bl>ZIpqmbal3z$ZE^S5i&CR0o139PaX1W0jev36_8BKBDBWBgY z+%U+5sdYYFRpUB=UK}(m(IhK;P9vq0l@!jGw&92`OV>^M^F>373n5EmP=f%-E$UZMe?68(8}P<^m%x{pAz(AgGxC!4Ig1;|%a<*AzsN%*Uyc2owx7h3Fy+Jzq=sO^hwGFuo^~ zc{n)Li1;l8scyJNbWB?Usx}BfKosHBm}Jx1lq&Bi_P^1ZB6Q=hU%}Lr@(6cSFhF3B zQL_L=1zoL|{=*JeurG~%BX~^>5)_xqCEUEh>~aQBbQ%)&Ts2gu(hZp+EKRf>%`opE z*rmwaJt+>Mnv1~lc@PIm0c7WFc2l$3h0%AxBnqzgoF!)#MxJ8YmbTp&<@5YFFG3`n zRK)lKO;%E~Gd#h`ByiHOT05kr608$S{`H=nP8or#9#R1(yyX(?t)HXo<_Q?L9UaSu z%VWW5L6SQ5+_`r{T}8_(05a^QeSC0DhQ=4=d@hDHk$`fyPl6_l6ZA%!QeyK~R$5X<@EBPs z6mr#>@yPjP8Aes{u7xe2wZqgBavJ&=D0JT;%Au~&D1+|+O|2bK=&FF;;1CCNjvM6Y2rA1#PB>ldCu^Ct^?5 zjC#04phl(9-(H_Sde@MxP!O|LS0mcfLz5Vu1n6OmnD)X=iqC@>x(L#NvCQ-qo09hM zf0;W)QMG_V`AHR1FjhM=`sw$yR-1(C=)?~$j|$*5_7@oysinfSvsF@)5mlV-Jwt?` zO4MsO^fJy-0uS1*DCG15#`uDLhk5*WF${jJCX$_&vpLZij=-9%Qx{$B1AFBF6n&B9 zvc%BGkJ?v>?M=E|^in=s&8#&xM7x$6#DrASZ_h2tJ!)$teX+Y?t<#m^3PNsCCZSvl zDeIF`JQ2BJt(EA7qaK%&SuoL58S%<(wlzfRf3n|t z!#tm7n;`Nka^>_JNY&)=i1Q9d*Jj&#$i}O2Ib|LL}gk)s3 zZAKLWg#nkq>onjIOpnBUE3gGjyq5YPf|9!OI>C>E9Df}8=GFeSJv{kU1@|1{tDB~c9FCD4zW$$KV*<%O!``!3xt zBX2aSMkfYlXYO$QF!(&hV%3;zU!%?Vk(GI!MolHcs-63phqvybJKhd*jeJ%PyIz=F zxl+8=`GMP2u;N(R)O=P0)A;8^Q&6e<29GZHmKI=`GBo}gxcCa&Um`VpXjpa1J;gdm zBE_l24tcG0WX;7R(gt5Sau=sIsJau`N+Y1}V#T*WntsL4$^nCO2u+=xDKOFBsl&q+c*?onp_Ig$2xVgMqVTsR zA~SQu=?fQ((O*cZ6rfBN(b8z7z5Ob zv;^yp+3g+pGEyAGPyJ)Bz@&f##wb~8Fkugu` zSVE>qA?d=z3+dg3yv`Sxd4dc`a_J^$^43+TWMmub-`(I^rrOr@-gO2y=o#XyoE=$`zXVKEqG0;D@1eH2^^P95q-@m z?VTX;b!IM1ni4IdgWjXef=wpy^j>w@BQc$JKFrzU!2I|uQwI&U9a=5?#{Z;}D|nE> zIP4KYBYTc?FZ}@HV5e62A+-3=mfm&Ctgb8v4w`Ja3mA8_fF3n<9?8vMC z64R^afvmMB*F>Q;Q&+5DK3s2q8H8DB1hM`ukk+R+J^{8I>558d4%IB-C8ca4y`8_?B3 z>Dl+_wKI%`l{_G_My6lTc|h#N>N=pIC(K{@Q!qEhZlw8EAD^> zZT8CnaH6nr&{z*|R^kiBQ+F~Xc#k)Zy@qx=9X;1owIH&e11>e87B%t5{HdoXS2-to zr#8ZScpc~&xA-Rzj|7=3lm?zNxvYzQFKs}`i7PHh*@@1f#4bY=*JAGO$B`KswU6qk z4pv|D(EcXX?%Kgk61Rp87zrFONG1qP5O2PD5;%um4G&L#&VdIksYvht7=x@A%-0}) z#M0klAu-FByr+33Z>(rhsl9r{hKy(dGIb0NO@Upvu zF|hY1SW4V1-iLMxtBVWRX{0+t#+3TzYB|i!p-tu;%KcE(?OVhTZAcDp3R2|PDiGg} zmQ!0&b4?b@50a{16wL8ddl(&o4ZNGif}F!QvwPq4tjrRx^QQg=y*}#STZ+-TgYO3N z>n8i!y?A01eJmuz7c+$#v1s_y%&Dw0zk9lwW>rQ>9Mj!Qi}%{U!*_vy4|JDp04i(n zH?m{#z{^5aANtNPZ6C!y^sYAVmnHpn$N+j6S;F=gHKtH^yZ|^Au9_5R*O_?Qj;zem zxaZs0$F~cllF440*kOy9khe)tt|^BDNJ8Tcu{-DD>$IZ_Kc|8u4!r56T3aoqK?q06 zi?@8kkW6&x!?7cg@NoQyCY%D##F^$x2} zmJs7q`ZtUjtlKHFFz4p;aWhHjF6Um@rktsn?oI1D*-Hd!(Df5N>jAUh#W*oc<&Bi7 zHo%dei>%VPt3hk6MxNi_Es7TtGMdasl<|~f(O$o~A?FarcW*)Fe*vwclnHDZ?}+SP zgYR&kn8RYb9O9><=RQuodIuwAN)ulS;SGy)PX}i^~1UYf0k$b`JnqicQgxToaq?rolg6VA7$>(o?TbE z6jEI3BBqxbUgL{6t@896dly!z7dXPugQXkT$}T@PWqm_3(f}$Agk`G%;50L{=*mi| zTE(qgB%svciozkc)B +

    Pyramid Starter project

    +

    404 Page Not Found

    + +{% endblock content %} diff --git a/pyramid_scaffold/pyramid_scaffold/templates/layout.jinja2 b/pyramid_scaffold/pyramid_scaffold/templates/layout.jinja2 new file mode 100644 index 00000000..860ceabe --- /dev/null +++ b/pyramid_scaffold/pyramid_scaffold/templates/layout.jinja2 @@ -0,0 +1,64 @@ + + + + + + + + + + + Cookiecutter Starter project for the Pyramid Web Framework + + + + + + + + + + + + + +
    +
    +
    +
    + +
    +
    + {% block content %} +

    No content

    + {% endblock content %} +
    +
    + +
    + +
    +
    +
    + + + + + + + + diff --git a/pyramid_scaffold/pyramid_scaffold/templates/mytemplate.jinja2 b/pyramid_scaffold/pyramid_scaffold/templates/mytemplate.jinja2 new file mode 100644 index 00000000..f2e7283f --- /dev/null +++ b/pyramid_scaffold/pyramid_scaffold/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
    +

    Pyramid Starter project

    +

    Welcome to {{project}}, a Pyramid application generated by
    Cookiecutter.

    +
    +{% endblock content %} diff --git a/pyramid_scaffold/pyramid_scaffold/views/__init__.py b/pyramid_scaffold/pyramid_scaffold/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyramid_scaffold/pyramid_scaffold/views/default.py b/pyramid_scaffold/pyramid_scaffold/views/default.py new file mode 100644 index 00000000..35ad5774 --- /dev/null +++ b/pyramid_scaffold/pyramid_scaffold/views/default.py @@ -0,0 +1,6 @@ +from pyramid.view import view_config + + +@view_config(route_name='home', renderer='pyramid_scaffold:templates/mytemplate.jinja2') +def my_view(request): + return {'project': 'Pyramid Scaffold'} diff --git a/pyramid_scaffold/pyramid_scaffold/views/notfound.py b/pyramid_scaffold/pyramid_scaffold/views/notfound.py new file mode 100644 index 00000000..0f6a35b1 --- /dev/null +++ b/pyramid_scaffold/pyramid_scaffold/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='pyramid_scaffold:templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/pyramid_scaffold/pytest.ini b/pyramid_scaffold/pytest.ini new file mode 100644 index 00000000..6e3b7498 --- /dev/null +++ b/pyramid_scaffold/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +addopts = --strict-markers + +testpaths = + pyramid_scaffold + tests diff --git a/pyramid_scaffold/setup.py b/pyramid_scaffold/setup.py new file mode 100644 index 00000000..aef7c6b9 --- /dev/null +++ b/pyramid_scaffold/setup.py @@ -0,0 +1,52 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'plaster_pastedeploy', + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'waitress', +] + +tests_require = [ + 'WebTest', + 'pytest', + 'pytest-cov', +] + +setup( + name='pyramid_scaffold', + version='0.0', + description='Pyramid Scaffold', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + 'Programming Language :: Python', + 'Framework :: Pyramid', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(exclude=['tests']), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points={ + 'paste.app_factory': [ + 'main = pyramid_scaffold:main', + ], + }, +) diff --git a/pyramid_scaffold/testing.ini b/pyramid_scaffold/testing.ini new file mode 100644 index 00000000..f5107c59 --- /dev/null +++ b/pyramid_scaffold/testing.ini @@ -0,0 +1,53 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:pyramid_scaffold + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = localhost:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, pyramid_scaffold + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_pyramid_scaffold] +level = DEBUG +handlers = +qualname = pyramid_scaffold + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/pyramid_scaffold/tests/__init__.py b/pyramid_scaffold/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyramid_scaffold/tests/conftest.py b/pyramid_scaffold/tests/conftest.py new file mode 100644 index 00000000..ebc06407 --- /dev/null +++ b/pyramid_scaffold/tests/conftest.py @@ -0,0 +1,76 @@ +import os +from pyramid.paster import get_appsettings +from pyramid.scripting import prepare +from pyramid.testing import DummyRequest, testConfig +import pytest +import webtest + +from pyramid_scaffold import main + + +def pytest_addoption(parser): + parser.addoption('--ini', action='store', metavar='INI_FILE') + +@pytest.fixture(scope='session') +def ini_file(request): + # potentially grab this path from a pytest option + return os.path.abspath(request.config.option.ini or 'testing.ini') + +@pytest.fixture(scope='session') +def app_settings(ini_file): + return get_appsettings(ini_file) + +@pytest.fixture(scope='session') +def app(app_settings): + return main({}, **app_settings) + +@pytest.fixture +def testapp(app): + testapp = webtest.TestApp(app, extra_environ={ + 'HTTP_HOST': 'example.com', + }) + + return testapp + +@pytest.fixture +def app_request(app): + """ + A real request. + + This request is almost identical to a real request but it has some + drawbacks in tests as it's harder to mock data and is heavier. + + """ + with prepare(registry=app.registry) as env: + request = env['request'] + request.host = 'example.com' + yield request + +@pytest.fixture +def dummy_request(): + """ + A lightweight dummy request. + + This request is ultra-lightweight and should be used only when the request + itself is not a large focus in the call-stack. It is much easier to mock + and control side-effects using this object, however: + + - It does not have request extensions applied. + - Threadlocals are not properly pushed. + + """ + request = DummyRequest() + request.host = 'example.com' + + return request + +@pytest.fixture +def dummy_config(dummy_request): + """ + A dummy :class:`pyramid.config.Configurator` object. This allows for + mock configuration, including configuration for ``dummy_request``, as well + as pushing the appropriate threadlocals. + + """ + with testConfig(request=dummy_request) as config: + yield config diff --git a/pyramid_scaffold/tests/test_functional.py b/pyramid_scaffold/tests/test_functional.py new file mode 100644 index 00000000..bac5d63f --- /dev/null +++ b/pyramid_scaffold/tests/test_functional.py @@ -0,0 +1,7 @@ +def test_root(testapp): + res = testapp.get('/', status=200) + assert b'Pyramid' in res.body + +def test_notfound(testapp): + res = testapp.get('/badurl', status=404) + assert res.status_code == 404 diff --git a/pyramid_scaffold/tests/test_views.py b/pyramid_scaffold/tests/test_views.py new file mode 100644 index 00000000..1cfaa9ac --- /dev/null +++ b/pyramid_scaffold/tests/test_views.py @@ -0,0 +1,13 @@ +from pyramid_scaffold.views.default import my_view +from pyramid_scaffold.views.notfound import notfound_view + + +def test_my_view(app_request): + info = my_view(app_request) + assert app_request.response.status_int == 200 + assert info['project'] == 'Pyramid Scaffold' + +def test_notfound_view(app_request): + info = notfound_view(app_request) + assert app_request.response.status_int == 404 + assert info == {} From 36e9eb3e6687857a0e0ea8cb812683f803e57c17 Mon Sep 17 00:00:00 2001 From: SAM Jubayer Date: Wed, 14 Jun 2023 17:06:10 +0900 Subject: [PATCH 2/2] Feature: Implement CRUD operations for Notes - Add Note model in the backend with properties: id, title, and description. - Create Note endpoints in the backend: get, post, put, delete. - Create Note components in the frontend: NoteForm for create and update, NoteList for displaying all notes. - Handle frontend routing with react-router-dom. --- fastapi-react-project/backend/alembic.ini | 82 ++++++++++++ fastapi-react-project/backend/app/alembic.ini | 85 +++++++++++++ ...505_removed_updated_at_column_from_note.py | 28 +++++ .../6405440953df_create_notes_table.py | 32 +++++ ...updated_at_column_from_note.cpython-38.pyc | Bin 0 -> 899 bytes ...440953df_create_notes_table.cpython-38.pyc | Bin 0 -> 1013 bytes ...7c620_updated_note_to_notes.cpython-38.pyc | Bin 0 -> 1358 bytes .../d3fd14d7c620_updated_note_to_notes.py | 46 +++++++ .../routers/__pycache__/notes.cpython-38.pyc | Bin 0 -> 1986 bytes .../backend/app/api/api_v1/routers/notes.py | 93 ++++++++++++++ .../db/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 114 bytes .../app/db/__pycache__/crud.cpython-38.pyc | Bin 0 -> 3538 bytes .../app/db/__pycache__/models.cpython-38.pyc | Bin 0 -> 1212 bytes .../app/db/__pycache__/schemas.cpython-38.pyc | Bin 0 -> 2964 bytes .../app/db/__pycache__/session.cpython-38.pyc | Bin 0 -> 588 bytes fastapi-react-project/backend/app/db/crud.py | 119 ++++++++++++++++++ .../backend/app/db/models.py | 28 +++++ .../backend/app/db/schemas.py | 72 +++++++++++ fastapi-react-project/backend/app/main.py | 51 ++++++++ .../frontend/src/admin/Admin.tsx | 41 ++++++ .../frontend/src/admin/Notes/NoteCreate.tsx | 15 +++ .../frontend/src/admin/Notes/NoteEdit.tsx | 16 +++ .../frontend/src/admin/Notes/NoteList.tsx | 18 +++ .../frontend/src/admin/Notes/index.ts | 3 + 24 files changed, 729 insertions(+) create mode 100644 fastapi-react-project/backend/alembic.ini create mode 100644 fastapi-react-project/backend/app/alembic.ini create mode 100644 fastapi-react-project/backend/app/alembic/versions/0078de844505_removed_updated_at_column_from_note.py create mode 100644 fastapi-react-project/backend/app/alembic/versions/6405440953df_create_notes_table.py create mode 100644 fastapi-react-project/backend/app/alembic/versions/__pycache__/0078de844505_removed_updated_at_column_from_note.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/alembic/versions/__pycache__/6405440953df_create_notes_table.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/alembic/versions/__pycache__/d3fd14d7c620_updated_note_to_notes.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/alembic/versions/d3fd14d7c620_updated_note_to_notes.py create mode 100644 fastapi-react-project/backend/app/api/api_v1/routers/__pycache__/notes.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/api/api_v1/routers/notes.py create mode 100644 fastapi-react-project/backend/app/db/__pycache__/__init__.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/db/__pycache__/crud.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/db/__pycache__/models.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/db/__pycache__/schemas.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/db/__pycache__/session.cpython-38.pyc create mode 100644 fastapi-react-project/backend/app/db/crud.py create mode 100644 fastapi-react-project/backend/app/db/models.py create mode 100644 fastapi-react-project/backend/app/db/schemas.py create mode 100644 fastapi-react-project/backend/app/main.py create mode 100644 fastapi-react-project/frontend/src/admin/Admin.tsx create mode 100644 fastapi-react-project/frontend/src/admin/Notes/NoteCreate.tsx create mode 100644 fastapi-react-project/frontend/src/admin/Notes/NoteEdit.tsx create mode 100644 fastapi-react-project/frontend/src/admin/Notes/NoteList.tsx create mode 100644 fastapi-react-project/frontend/src/admin/Notes/index.ts diff --git a/fastapi-react-project/backend/alembic.ini b/fastapi-react-project/backend/alembic.ini new file mode 100644 index 00000000..53e6f13f --- /dev/null +++ b/fastapi-react-project/backend/alembic.ini @@ -0,0 +1,82 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = app/alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +timezone = America/Los_Angeles + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks=black +# black.type=console_scripts +# black.entrypoint=black +# black.options=-l 79 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S \ No newline at end of file diff --git a/fastapi-react-project/backend/app/alembic.ini b/fastapi-react-project/backend/app/alembic.ini new file mode 100644 index 00000000..bfcc3c7f --- /dev/null +++ b/fastapi-react-project/backend/app/alembic.ini @@ -0,0 +1,85 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks=black +# black.type=console_scripts +# black.entrypoint=black +# black.options=-l 79 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/fastapi-react-project/backend/app/alembic/versions/0078de844505_removed_updated_at_column_from_note.py b/fastapi-react-project/backend/app/alembic/versions/0078de844505_removed_updated_at_column_from_note.py new file mode 100644 index 00000000..44fba5f5 --- /dev/null +++ b/fastapi-react-project/backend/app/alembic/versions/0078de844505_removed_updated_at_column_from_note.py @@ -0,0 +1,28 @@ +"""Removed updated_at column from Note + +Revision ID: 0078de844505 +Revises: d3fd14d7c620 +Create Date: 2023-06-12 04:41:52.475432-07:00 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '0078de844505' +down_revision = 'd3fd14d7c620' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('notes', 'updated_at') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('notes', sa.Column('updated_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True)) + # ### end Alembic commands ### diff --git a/fastapi-react-project/backend/app/alembic/versions/6405440953df_create_notes_table.py b/fastapi-react-project/backend/app/alembic/versions/6405440953df_create_notes_table.py new file mode 100644 index 00000000..4103ea0d --- /dev/null +++ b/fastapi-react-project/backend/app/alembic/versions/6405440953df_create_notes_table.py @@ -0,0 +1,32 @@ +"""create notes table + +Revision ID: 6405440953df +Revises: 91979b40eb38 +Create Date: 2023-06-09 09:53:27.798080 + +""" +from alembic import op +import sqlalchemy as sa +import datetime # Add this line + + +# revision identifiers, used by Alembic. +revision = '6405440953df' +down_revision = '91979b40eb38' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'notes', + sa.Column('id', sa.Integer, primary_key=True, index=True), + sa.Column('title', sa.String(200), nullable=False), + sa.Column('description', sa.String(500)), + sa.Column('created_at', sa.DateTime, default=datetime.datetime.utcnow), + sa.Column('user_id', sa.Integer, sa.ForeignKey('user.id')) + ) + + +def downgrade(): + op.drop_table('notes') diff --git a/fastapi-react-project/backend/app/alembic/versions/__pycache__/0078de844505_removed_updated_at_column_from_note.cpython-38.pyc b/fastapi-react-project/backend/app/alembic/versions/__pycache__/0078de844505_removed_updated_at_column_from_note.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4df763ed171caaa99497c6d284a8d94fb26bd5ed GIT binary patch literal 899 zcmZuvJ&)5s5M6)7cANwOSJ7E?E|NHjb2&u_fpQ?ONQCY}41B3D%m~+3|Yjy*GZ>>DUC3F}(!^xN81KsPTtBOvv zDcL%U5g5%%VP%^niB<`9jhe9?0qh_OwTOfaxj>%UKQRy@k$>kx_11m_4erx_F>%PJAMMDSn0*0YqsCZW$2iT5!&-E1qC($;jP IdNj-U4VW_ETL1t6 literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/alembic/versions/__pycache__/6405440953df_create_notes_table.cpython-38.pyc b/fastapi-react-project/backend/app/alembic/versions/__pycache__/6405440953df_create_notes_table.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67fe85966bc5b06b75f4dff6758d321a21e672a4 GIT binary patch literal 1013 zcmZuvO;6iE5Z(26oTMOx)K)z-*Bk(sgbzokssutpl{gf*_+mMpC7AlFyBlb@RpQEj zAjkfb{)WBw#Jwj@9ovzJ)UGw-ncb)H+xL9Y?Yacl#r+Q;Iw8NSW_fAYe8Lb%n1BQX zC}=6ov_PX(t*3fm)NeB|!3Zoc0~_?f!DxX6_B{z)aG-_nHt1)h@7`rGXOU#RypT-v zWHe5h>waN3iAajPcXBl9?GC-2;n4GU25{M&Ffr=+AN)OkJoMOjur8r4bSg+{%B_~+TPpT^Y^`d&vl=WIO$WR73FErS)LqpmM6v)Zh5`J<`6@qm?U|m zK&5uAgNE4v9UtStsCP`MJ(^(6DcImV=-BDds`uPEomY4r+L$eAU)$FXbQYLyH3@Mo zc!fB9ZlR<1?cXhFuK;Pw+->TE#!`d39iAY(8 zMLNxLWuN4dO&C|!ndC`6QBGyRd6F^Z09LZ9tf`Fi;#RrG1!u`5|IB7T34e`~>-2oE z6_sTZDa*!5ymiBP<-OQiwkKS$sFi5AY?d=*T~2reY#rx`RZN5$RHJKjovvc6Kl8V^ z_m7)8Zq{yAwl;3g=|XSqlvl@nt+~DnyeOLwzOZhpHwaTLC~otb!+UH+B^jao9p-*V z*FS0OseQlVU6CCfpC lMPA>o{@8!HIR{w*)0BNg--%emWY`_*P{%M(CVX_K{TC1Q1Oxy8 literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/alembic/versions/__pycache__/d3fd14d7c620_updated_note_to_notes.cpython-38.pyc b/fastapi-react-project/backend/app/alembic/versions/__pycache__/d3fd14d7c620_updated_note_to_notes.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f49eacae300a56b38c6882f18cf7ea3def5ba73 GIT binary patch literal 1358 zcmZuxO>fjj81~p++w1ibS`aNMl~$;iJ%l*PCWsXRLMQ^M3Q`(+*vrWAJ4qb*ii?N5C;kFRh<9eauz}h;&&>O=?~LE)v0wK4p@r9fAKyUNv#fX8n7?c^ z9^nx85x|;QK)}vPVNYzL*G}$CTyQ5H=uA9tCO(b;Jn;XqCS3@ii*pFhpVlz^`?#qg zkqic9CD}k$W)@+%!;UkNRpsFMlkou7_F-iNchim4cp711JlGh;>!VS8Z+#8+!!6FR z%-{*ac(59;t}VqIODn5`cr?b}%KDuc(^p1I@!fG8hv9oXxiuupuIlYcG+$pu^L105 zH-80aY~c|9B1r3;0Fm~Q1Gco!-3tpGaL=3z?1Qw9@cF?*zi0gZnft{XeCPGC7Ix-> z;D5pc>>>2p{AnaIkv>MQ%p&Jq%w5oXfrY)ix~)38i9&^KPiZ1|@fm}%$@63{XUc5^ zN>L%&G_CTcD3vV|_m}$)dODDZQ5Il|AN&Ge0aE=)b(`atk}!a)1V)cYyERqLkFRynE$99 zx_A6Kg7W{`+}V1zxuZfunBkA#)ZrT%e$ue0;R}P))h+a?E>y2eL&j}1TQY8w>v+$I zn9ck*%oz5RPbpL>r6|2A{jKoCt31imLsp!c!>NF`UlrBYo6xW39#6{jkm75?a(sb+ m)vN?TtJ3go_W9q#2KS2!nw&i#`m9$GTt6a#>k@N2!NO+}4OI&O literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/alembic/versions/d3fd14d7c620_updated_note_to_notes.py b/fastapi-react-project/backend/app/alembic/versions/d3fd14d7c620_updated_note_to_notes.py new file mode 100644 index 00000000..b710bce0 --- /dev/null +++ b/fastapi-react-project/backend/app/alembic/versions/d3fd14d7c620_updated_note_to_notes.py @@ -0,0 +1,46 @@ +"""updated note to notes + +Revision ID: d3fd14d7c620 +Revises: 6405440953df +Create Date: 2023-06-12 04:04:15.064014-07:00 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd3fd14d7c620' +down_revision = '6405440953df' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('notes', sa.Column('updated_at', sa.DateTime(), nullable=True)) + op.alter_column('user', 'is_active', + existing_type=sa.BOOLEAN(), + nullable=True) + op.alter_column('user', 'is_superuser', + existing_type=sa.BOOLEAN(), + nullable=True) + op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True) + op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False) + op.drop_column('user', 'address') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('address', sa.VARCHAR(length=100), autoincrement=False, nullable=True)) + op.drop_index(op.f('ix_user_id'), table_name='user') + op.drop_index(op.f('ix_user_email'), table_name='user') + op.alter_column('user', 'is_superuser', + existing_type=sa.BOOLEAN(), + nullable=False) + op.alter_column('user', 'is_active', + existing_type=sa.BOOLEAN(), + nullable=False) + op.drop_column('notes', 'updated_at') + # ### end Alembic commands ### diff --git a/fastapi-react-project/backend/app/api/api_v1/routers/__pycache__/notes.cpython-38.pyc b/fastapi-react-project/backend/app/api/api_v1/routers/__pycache__/notes.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79bd0f6367d64116a3edabf10e1f214047284b43 GIT binary patch literal 1986 zcmaJ?-EQPG6t_sHDg&QQK6#{k%8%OWlF<4CZZ|YH)5IC=KclaE4*dUJAl3m`Tq~y z;;Vpe^EHFs1@t|BOK!2sI~NvT=Nks!1N{Bl@Pz}D3dKL6xk zSmZ)#_dtAI2$gI1nHY(TEA1T!H5z70JP)XLjz!+*he1VG(aMIo0L0wX^??*oE{tE- zxk$u(P!&8b@qwjl&mr!qfoc!!k6==}Xnj`X0n=T~I4GnPS>BHZdHhoJ3kCFcZkSNT zNJyL=JNt&vSCGM%GuHid2t4%m^>JUk8YBfrrkQ|sdGqOkg__Ko4PPAp3a{!xmy-#d z(sPT`oKCDM{fYd{CghZH_Jm9xp0WMb<9GMR&8NdGhgCg3 zh_Yi5Ski^G+Db&G-4hXUAyr_@HFPq5s2g(;%<3y9-7iFL!TcX@>_?-~?2X|Y_g}uf zFHIR$Da79BOxH|IKZ#Y|hA4#xjWj*hp>i9najS&xe~Y+&%+EnQSI#%V%sD_M=B)z8 zLKPndpb7(B*ttEiK?6G8U22e>84Y?|~bzNUgpt`qB&PO)Beylp6zo3R)5GVzTe zToPrn3c;Gi9ExJd;x8=8{+njq&@2zZJAhOSI#QGOp@&*8murZkkn@K2f@lKq+O8R2N;du4LE2w6i9baKeWgUX9NqRfgs-cRf@T|AqS$za&x?Jf z(9g^{znK+=%(c5-=$o}G?~E@Nlp69OW_YOGqe$hj$t6lEk&;BcBnqdU{A>hzXqsyu z4#gfn+*3lSc$i7t@47y74rIa2i8C9}6jvfcBVM=&9h`_XQWDD`o7myb(M?P;7)r4h z75Rx=!HE`5n9ZsB<g`kf=x4PQ$X}%5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HxequoZ7^fuZ f$H!;pWtPOp>lIYq;;_lhPbtkwwF7DR48#lo&rcPk literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/db/__pycache__/crud.cpython-38.pyc b/fastapi-react-project/backend/app/db/__pycache__/crud.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e561f9e2799b9fe84c0a1ed050383e34cf2380c8 GIT binary patch literal 3538 zcmcImNpllN6rP@4i*0#fu`$N%h$Mz2R8~j`7(ysYq{{Y1m6S48k74AIC3;!}SIHO5 zAs22raE(SNzlFQ66xWQR$)LEI;&9cd8Y{pjIF59|q#ZKAfHg{sL?w9>~rkv69Zk(;> z$~k6)c9`ehE0g;?^TIBVU_2UTYGXY6%Hlbm$6SG5ASwLa+BkR<(*_@XW%Ds!c;S>M z!OPU9_&D~M;FFL)(%W;2AHn#jUY)5OgWPd`6f2MM#ST&tQB?ug}!xA$gjg#YyM*dCZ^T){;@Yv`jx9j2autO=KWjNs%dUDNGVfmG)s-l63109>$6ClIr8I9+1rVqfl0w zL6ZF15WMm@NFGDZ==^O%w4u)(YI!|**cA! zU1Q5^o5_N7IpZd`UYgHY+l1bBY5#ZVb1=Gs1Lijxa(=DR+TeSp%B7lDs?buoJd{Bc z7d_=XX@z1_P@E{67AbcviY2~g#Feuai9`z0a?cVOd<4atvPu6vLmb24VDd`PZ0g2W zuT({gFElrmPgf@?T9o)82Ag9x%QG>JelgumIj~d|D_4ZFB{l}kL5KFtF^9ps%&}zb zu~N|$1<2{kh;iyo5Sbw|1yVHAE9ePQUF$E7qnEmh^sTIJR}>#DP+PQDysvivJuonm5GBy>aKQ) z%0x*esLJSRsPgD0t!60518R&iCFH%#Ddq(!sr+s#9rEybFvJBS+N&3-cZr0pfOF+m z8+CX{c_LgBA<{2(pCChsB8ho?Rfp-YF>_TB1~Sw<+BqR&uq;Bj|2Gd@Kg)c~iYwp_ zn3nwC3t!4+m=?y}Hg)FCN=62z?A5tzq!FQwi0sCfve_yLfI323jNyYAq14gYC}?{% zLEu!mcJ*4N^l-Uy_u)!uky1Mjk-s03=n*1Ak)BHh^vy1VpU@HM%|>w(pNUT~?plh{ zJvG-GQSO1|py}Xwj!y4}R@+L3ZwU0_7PnurEmWAcz3z%d>9oz7yUp4*cdr|u&JEBV zZ`W8fexCZt+;X;D>9?)6({{HlW;}WDuR-Uf786&Y&hj2pBjK}Z+~V-#1|<{SSNk0y zHE2umF<8pxQB~?-)1u0WWLPIi2V6=i#0RuqTlORLie4WQQyVSNCb?FVQ|N#M%32RM zmGd-+0kcDn$b>vfF_H|P2AD+wHp=p*59*o)HfnYP>^dEo0+{ju?BV+@%ES%KDx35izP7Fm#P}W}gO%YHX8ung{gzZaO=O

    m}(vJ;&9;{BaS?H@QQtu9tp+M}wT2D(2Yi~1t7uJZkjExei zkVXg6%)eZx6K&+1_W@_$U|*e_05kG-AIx+D3IfVB!ERD-$WLA9BJF{dytiB3uMEU+vnVv5E&^9S5sV(%9Kx9&zy;3W5ori|bq(~- zDsK%%JZMIJ$EnfeNgTxV=-pgsh`OMxP_Eo;MjMY5ldAI|qjFIm$5U~k+7RJF5>{Ix zlACF1x1tTH5m{MyZzOB}MIwx@n*`PcE(Z^hv^WTfOi uNCEF!A84Jo3f>eU!rvw{4g6X5b>5vQ6tXnJ!{K#)sz7g<0sd6J&;JJj;l|kj literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/db/__pycache__/models.cpython-38.pyc b/fastapi-react-project/backend/app/db/__pycache__/models.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6997e373a5d4e0b09bc68c5a5486c228299328fe GIT binary patch literal 1212 zcmah}&2AJ&5T2g@o!MQl9T5eR0}>#y7yAWJWRb`SA^C#2^kvfCF6@DxA6L(WY_3M) z8bgRFuulld5zMg%vSQG)ZB>1Fnq9C|oa<|1q4ry3TpaU&T z&{ z&xp@FU+Cm@ur5B+O~E>K+4HV5pjwyy)|s(sUG}ma;0(~EuLrE#GnfCa1~{o3aK%xF zP@lE0V7nG>j$qVKcdb3I`_F=p6E1$PEF5q6@VhM~a!A9r1Q1A}0~u-|BZwiNCV{b2Ehtd^#M*fv9DAm%%Z!F~# zkAx@Jp(}f=8-?5IJ#1Z4w;KLBT1PG&+xlVz+@TO%-j2qoQv=rmcgb-rT&Cfz8qMJ1 z<8C0oTb`=T`>OByk?H{Uc)jcLs%ljORppARYC1g{w&zuKF)H(QW>HlGpFB~d_YfdI z02kI86E+fPRBc@GwnjVn1KrH1f6bk9WzRuZy|VU0hZ;lPGplW-Hbea;XAp z%>9Wig>0__d`!&4Klct)-}|9Y4=7YHcE8_ZNefslyGM|VI9!UA$f?hL_w)agm!m%9 zO@9!4M_!gToTMfB*k*YPDBqRu=3ZKsGl8Wh=`TaM1FKs>cPByd9$kZv2u^T8c!V4* z{y^||#!2r}B{>oNk>Gf}hd(j7&+vfZXNKFt<1b97$`AEmz?B;82J4<*H*z&xu1Vs7 zKZ0}|Hf{Go78?Ze=}HZl@e+S?MK^QBan5jC?a6w=K8~FuwM&Ajz+BpuScZOXy>x}W zFv@Jsq1k$I?}W-x`iQ1Gm;bHY@35J~&Ma8Vk@KGpK4tL5@KyUpNG^XiJ=I_NBeuNj Kp%*(3SBozls~yb% literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/db/__pycache__/schemas.cpython-38.pyc b/fastapi-react-project/backend/app/db/__pycache__/schemas.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5df526d853bf242dc10c23d121430d5fb18e5650 GIT binary patch literal 2964 zcmbVO%W@k<6rHE$p^+s!j$=DKiZ=sQ4jUF!8OlXMvSPBp-ppodqV9wVBaP@DL78n> z@GJf!suyfn@e8atw?{8DQbmqb-MXir_ntm|TUWhaXy7yb=nwXnZ5V$OSp7Hv7bx;i zRLo#zX^hO&G|9F~E47~(%x2E-26MRi%1#|%mvw+U+yQohJ>~=Z8h3yL76OMFd%#^5 z0Y@79z&*AHyrBk6~~lHJOoGR|dDa!_0Hg zk-8i~GaI&(dfIkjcT!)E@w6Ri+b0_v2g<*aT+pt{<)gePFI7;;EFadzFZ{8JV9V)* ziz!AWDtulDS!d&X#8pt{GcyjApXtUdQ(=~kDmE=S>?q5Ap5`SD2eM@?R6on|@wlq< zx~Rr7%Y=(yl=G~rO3{HSJgR(DH-_kwP1V0g5Aw-G7xwId9R3Jb<=Ny~5GS;PBo&3G zZ$|p7+gk*h7;`~`BUEz&nF$&!4%I-}1nq1dG{Fg8d3d#~k?Ks*wD6D0p6TyTqZZpT42hr#E1@yZwgU$nu&)iQQm`{#GSQu1kovo4M77oEhg@u zgE*6PpK(r>ZfV7}VC)RT=5bx&)~68pOUV1g;2yKi4RANR_2ACeXXrLk{*D!OBFO93 z>aSz1Ppr}W1!v+e*4Y>kMS34)E&#%Du+9v*q0CzOPbZ}JyxfJWV@#ql47F^h;T`lYB{R>8 z`WJ#>Hm;YW+{@}GK2D>2IOI}hH6a!1pRB%~@FaHE&v2>kO^JX!{CLGnQ&7uxFKO)3 z%SPDTB}p?oMfQg6z^0_+sz7`DD6jKG(8W>Cl+Le;BTf6`6fNaFgfn zq2qIunam7);M&ktyqDb54u<%J9i>ehbNRNW_q8Svl@+QWWf8P8l`-To( V(!GjI-yZk_XW;3tJD{&a_&>fM-uVCk literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/db/__pycache__/session.cpython-38.pyc b/fastapi-react-project/backend/app/db/__pycache__/session.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..084c804f2f4b9e95fca9bea5373becb6c28fa599 GIT binary patch literal 588 zcmYjOJ#Q2-5FLBJ?lyZ-L;(^N?KYQ+4k0cO1kw=+j_~1T<+Yv5g1tWMJw#Aa9W6hA z4oX@Y{>!#h`~?~qZ$R8wGkWthnm02(o=kE;`}XNVekTAwLb7cMCU?+1!yrKt4Tdm6 z0;Zaph(!aAbZioq3^>-Q$yi1}5tY3fZ3r8yMB-CpnSP8|uA&XF9hu3|2VjL9FQCkS zVt*(}KM_vxPF1R1HmyQpPb#6g<6ZMsRWz_2tAPg28IqcZ~^c0b9e>>p2JNzhtK2-;up9r&Pa|* z+82vU>A4kL_leMUn25^yE4XmWUsw0}YBjj5_x}z$U7hvmQgxNA_kvy!29gp=a_S1i zN9Ba&{*C5ZoGNqXD01xje`2cMbfY?>>|nMytd5^NWa;IJT^6e11H9}o$*{y$*cp1>GFGm&x*e|OuV5$$K3Y&p L;@@$!OVjudAL)$H literal 0 HcmV?d00001 diff --git a/fastapi-react-project/backend/app/db/crud.py b/fastapi-react-project/backend/app/db/crud.py new file mode 100644 index 00000000..06641af3 --- /dev/null +++ b/fastapi-react-project/backend/app/db/crud.py @@ -0,0 +1,119 @@ +from fastapi import HTTPException, status +from sqlalchemy.orm import Session +import typing as t + +from . import models, schemas +from app.core.security import get_password_hash + + +def get_user(db: Session, user_id: int): + user = db.query(models.User).filter(models.User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + + +def get_user_by_email(db: Session, email: str) -> schemas.UserBase: + return db.query(models.User).filter(models.User.email == email).first() + + +def get_users( + db: Session, skip: int = 0, limit: int = 100 +) -> t.List[schemas.UserOut]: + return db.query(models.User).offset(skip).limit(limit).all() + + +def create_user(db: Session, user: schemas.UserCreate): + hashed_password = get_password_hash(user.password) + db_user = models.User( + first_name=user.first_name, + last_name=user.last_name, + email=user.email, + is_active=user.is_active, + is_superuser=user.is_superuser, + hashed_password=hashed_password, + ) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + +def delete_user(db: Session, user_id: int): + user = get_user(db, user_id) + if not user: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail="User not found") + db.delete(user) + db.commit() + return user + + +def edit_user( + db: Session, user_id: int, user: schemas.UserEdit +) -> schemas.User: + db_user = get_user(db, user_id) + if not db_user: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail="User not found") + update_data = user.dict(exclude_unset=True) + + if "password" in update_data: + update_data["hashed_password"] = get_password_hash(user.password) + del update_data["password"] + + for key, value in update_data.items(): + setattr(db_user, key, value) + + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + +def get_note(db: Session, note_id: int): + note = db.query(models.Note).filter(models.Note.id == note_id).first() + if not note: + raise HTTPException(status_code=404, detail="Note not found") + return note + + +def get_notes( + db: Session, skip: int = 0, limit: int = 100 +) -> t.List[schemas.NoteOut]: + return db.query(models.Note).offset(skip).limit(limit).all() + + +def create_note(db: Session, note: schemas.NoteCreate, user_id: int): + db_note = models.Note( + title=note.title, + description=note.description, + user_id=user_id, + ) + db.add(db_note) + db.commit() + db.refresh(db_note) + return db_note + + +def delete_note(db: Session, note_id: int): + note = get_note(db, note_id) + if not note: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Note not found") + db.delete(note) + db.commit() + return note + + +def edit_note( + db: Session, note_id: int, note: schemas.NoteEdit +) -> schemas.Note: + db_note = get_note(db, note_id) + if not db_note: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Note not found") + update_data = note.dict(exclude_unset=True) + + for key, value in update_data.items(): + setattr(db_note, key, value) + + db.add(db_note) + db.commit() + db.refresh(db_note) + return db_note diff --git a/fastapi-react-project/backend/app/db/models.py b/fastapi-react-project/backend/app/db/models.py new file mode 100644 index 00000000..103cff01 --- /dev/null +++ b/fastapi-react-project/backend/app/db/models.py @@ -0,0 +1,28 @@ +from sqlalchemy import Boolean, Column, Integer, String, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from .session import Base +import datetime + +class User(Base): + __tablename__ = "user" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True, nullable=False) + first_name = Column(String) + last_name = Column(String) + hashed_password = Column(String, nullable=False) + is_active = Column(Boolean, default=True) + is_superuser = Column(Boolean, default=False) + notes = relationship("Note", back_populates="user") + + +class Note(Base): + __tablename__ = "notes" # Change this line + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, nullable=False) + description = Column(String, nullable=True) + created_at = Column(DateTime, default=datetime.datetime.utcnow) + user_id = Column(Integer, ForeignKey("user.id")) + + user = relationship("User", back_populates="notes") diff --git a/fastapi-react-project/backend/app/db/schemas.py b/fastapi-react-project/backend/app/db/schemas.py new file mode 100644 index 00000000..ab1a0999 --- /dev/null +++ b/fastapi-react-project/backend/app/db/schemas.py @@ -0,0 +1,72 @@ +from pydantic import BaseModel +import typing as t + + +class UserBase(BaseModel): + email: str + is_active: bool = True + is_superuser: bool = False + first_name: str = None + last_name: str = None + + +class UserOut(UserBase): + pass + + +class UserCreate(UserBase): + password: str + + class Config: + orm_mode = True + + +class UserEdit(UserBase): + password: t.Optional[str] = None + + class Config: + orm_mode = True + + +class User(UserBase): + id: int + + class Config: + orm_mode = True + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + email: str = None + permissions: str = "user" + + +class NoteBase(BaseModel): + title: str + description: t.Optional[str] = None + + +class NoteCreate(NoteBase): + pass + + +class NoteEdit(NoteBase): + pass + + +class NoteOut(NoteBase): + id: int + + class Config: + orm_mode = True + + +class Note(NoteBase): + id: int + + class Config: + orm_mode = True diff --git a/fastapi-react-project/backend/app/main.py b/fastapi-react-project/backend/app/main.py new file mode 100644 index 00000000..1ef77754 --- /dev/null +++ b/fastapi-react-project/backend/app/main.py @@ -0,0 +1,51 @@ +from fastapi import FastAPI, Depends +from starlette.requests import Request +import uvicorn + +from app.api.api_v1.routers.users import users_router +from app.api.api_v1.routers.auth import auth_router +from app.api.api_v1.routers.notes import notes_router # new import +from app.core import config +from app.db.session import SessionLocal +from app.core.auth import get_current_active_user +from app.core.celery_app import celery_app +from app import tasks + + +app = FastAPI( + title=config.PROJECT_NAME, docs_url="/api/docs", openapi_url="/api" +) + + +@app.middleware("http") +async def db_session_middleware(request: Request, call_next): + request.state.db = SessionLocal() + response = await call_next(request) + request.state.db.close() + return response + + +@app.get("/api/v1") +async def root(): + return {"message": "Hello World"} + + +@app.get("/api/v1/task") +async def example_task(): + celery_app.send_task("app.tasks.example_task", args=["Hello World"]) + + return {"message": "success"} + + +# Routers +app.include_router( + users_router, + prefix="/api/v1", + tags=["users"], + dependencies=[Depends(get_current_active_user)], +) +app.include_router(auth_router, prefix="/api", tags=["auth"]) +app.include_router(notes_router, prefix="/api/v1", tags=["notes"]) # new router + +if __name__ == "__main__": + uvicorn.run("main:app", host="0.0.0.0", reload=True, port=8888) diff --git a/fastapi-react-project/frontend/src/admin/Admin.tsx b/fastapi-react-project/frontend/src/admin/Admin.tsx new file mode 100644 index 00000000..4540d4dc --- /dev/null +++ b/fastapi-react-project/frontend/src/admin/Admin.tsx @@ -0,0 +1,41 @@ +import React, { FC } from 'react'; +import { fetchUtils, Admin as ReactAdmin, Resource } from 'react-admin'; +import simpleRestProvider from 'ra-data-simple-rest'; +import authProvider from './authProvider'; + +import { UserList, UserEdit, UserCreate } from './Users'; +import { NoteList, NoteEdit, NoteCreate } from './Notes'; + +const httpClient = (url: any, options: any = {}) => { + if (!options.headers) { + options.headers = new Headers({ Accept: 'application/json' }); + } + const token = localStorage.getItem('token'); + options.headers.set('Authorization', `Bearer ${token}`); + return fetchUtils.fetchJson(url, options); +}; + +const dataProvider = simpleRestProvider('api/v1', httpClient); + +export const Admin: FC = () => { + return ( + + {(permissions: 'admin' | 'user') => [ + permissions === 'admin' && ( + + ), + , + ]} + + ); +}; diff --git a/fastapi-react-project/frontend/src/admin/Notes/NoteCreate.tsx b/fastapi-react-project/frontend/src/admin/Notes/NoteCreate.tsx new file mode 100644 index 00000000..4fe2af26 --- /dev/null +++ b/fastapi-react-project/frontend/src/admin/Notes/NoteCreate.tsx @@ -0,0 +1,15 @@ +import React, { FC } from 'react'; +import { + Create, + SimpleForm, + TextInput, +} from 'react-admin'; + +export const NoteCreate: FC = (props) => ( + + + + + + +); diff --git a/fastapi-react-project/frontend/src/admin/Notes/NoteEdit.tsx b/fastapi-react-project/frontend/src/admin/Notes/NoteEdit.tsx new file mode 100644 index 00000000..9b2b4eab --- /dev/null +++ b/fastapi-react-project/frontend/src/admin/Notes/NoteEdit.tsx @@ -0,0 +1,16 @@ +import React, { FC } from 'react'; +import { + Edit, + SimpleForm, + TextInput, +} from 'react-admin'; + +export const NoteEdit: FC = (props) => ( + + + + + + + +); diff --git a/fastapi-react-project/frontend/src/admin/Notes/NoteList.tsx b/fastapi-react-project/frontend/src/admin/Notes/NoteList.tsx new file mode 100644 index 00000000..db42daa3 --- /dev/null +++ b/fastapi-react-project/frontend/src/admin/Notes/NoteList.tsx @@ -0,0 +1,18 @@ +import React, { FC } from 'react'; +import { + List, + Datagrid, + TextField, + EditButton, +} from 'react-admin'; + +export const NoteList: FC = (props) => ( + + + + + + + + +); diff --git a/fastapi-react-project/frontend/src/admin/Notes/index.ts b/fastapi-react-project/frontend/src/admin/Notes/index.ts new file mode 100644 index 00000000..a03df243 --- /dev/null +++ b/fastapi-react-project/frontend/src/admin/Notes/index.ts @@ -0,0 +1,3 @@ +export * from './NoteEdit'; +export * from './NoteList'; +export * from './NoteCreate';