diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2321c4e..7064c17 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,8 +30,11 @@ jobs: echo "Tagging with $TAG_NAME" VERSION=v$TAG_NAME echo "VERSION=$VERSION" >> $GITHUB_ENV - - make build-prod + echo NEXT_PUBLIC_FRONTEND_URL=\"https://grezy.org/\" >> frontend/.env # TODO: Change + echo NEXT_PUBLIC_BACKEND_URL=\"https://grezy.org/\" >> frontend/.env # TODO: Change + python -m pip install cryptography + python ./setup/env_file_generator.py production + make up-prod docker tag nextjs ghcr.io/${{ github.repository }}/nextjs:$VERSION docker tag nextjs ghcr.io/${{ github.repository }}/nextjs:latest docker tag nango ghcr.io/${{ github.repository }}/nango:$VERSION diff --git a/.github/workflows/prod-build-check.yml b/.github/workflows/prod-build-check.yml index 146b3ea..d9b1f8b 100644 --- a/.github/workflows/prod-build-check.yml +++ b/.github/workflows/prod-build-check.yml @@ -28,10 +28,12 @@ jobs: python-version: "3.12" cache: "pip" # caching pip dependencies - - name: Create ./backend/.envs/.development files + - name: Create ./backend/.envs/.production files run: | python -m pip install cryptography python ./setup/env_file_generator.py production + echo NEXT_PUBLIC_FRONTEND_URL=\"http://localhost:3000/\" >> frontend/.env + echo NEXT_PUBLIC_BACKEND_URL=\"http://localhost:8000/\" >> frontend/.env - name: Start containers - run: make build-prod \ No newline at end of file + run: make up-prod \ No newline at end of file diff --git a/README.md b/README.md index 4d489d2..b39eb2b 100644 --- a/README.md +++ b/README.md @@ -10,110 +10,15 @@ The bridge contains: - drf endpoints - backend routes - front types -- api call methods - -The workflow is simple: - -1. Create a model in your backend -2. Run the `make bridge` command -3. Import type and api call methods in your frontend +- api call methods (WIP) +- Tests (WIP) +- Scripts (for easy manual testing) (WIP) ## Activity ![Alt](https://repobeats.axiom.co/api/embed/3697d98ced5eddd922d97cdc1b47ecbc46b5f23c.svg "Repobeats analytics image") -##  Prerequisites - -###  Env variables - -Database: - -- POSTGRES_USER -- POSTGRES_PASSWORD - -Django: - -- DJANGO_SECRET_KEY - -Celery: - -- CELERY_FLOWER_USER (optional) -- CELERY_FLOWER_PASSWORD (optional) - -Stripe: - -- STRIPE_PUBLISHABLE_KEY -- STRIPE_SECRET_KEY -- STRIPE_ENDPOINT_SECRET (optional) - -Other: - -- FRONTEND_URL (optional) - -### Add node modules - -To add a node module with `npx`, please ensure to add your module to the `./frontend`'s `package.json` file and not to the `/`'s `package.json`. - -## Todo - -### Blue print - -- Django - - [x] Structure - - [x] Settings - - [x] envs files generator - - [x] Ruff - - [x] Requirements - - [x] Celery - - [ ] API - - [x] Structure - - [x] Libraries - - [ ] JWT -- NextJs - - [x] Structure - - [ ] Tailwind - - [ ] Typescript - - [ ] Eslint - - [ ] Shadcn - -- Github - - [x] Dependabot (front & back) - - [ ] CI (Tests, Lint, Build, Coverage) - - [x] Pre-commit hook (ruff) - -- Docker - - Development - - [ ] postgres - - [ ] litestream - - Production - - [ ] postgres - - [ ] litestream - (content: postgres / litestream, redis, traefik, celery (worker, beat), flower, django, mkdocs, nextjs) - -- [x] Semantic release -- [x] Mkdocs - -### Bridge - -- Backend - - [ ] View Generator - - [x] Serializer Generator - - [x] Dynamic fields from models - - [ ] Detail serializer - - Handle nested models for detail serializer - - [ ] ForeignKey - - [ ] ManyToMany - - [ ] OneToOne - - [ ] Chirurgical edit on fields - - [ ] Dynamic routes - - [ ] Retrieve types from models - - [ ] Progress bar - -- Frontend - - [ ] API call methods generator - - [ ] Types generator - -### Setup a new git repo +## Setup a new git repo Once you've created a new git repo from this template, we advise you enter the following command: @@ -132,3 +37,9 @@ Now, you can pull directly from nango to get all our latest updates by running ` If you have problems with pushing to the wrong remote, run: `git push -u origin ` to set the upstream branch for the current checked out branch. Otherwise, you can manually edit the git config with `git config --edit` + +### Generate a token for AWS in order to prove private images + +`echo -n "$USERNAME:$GH_TOKEN" | base64` + +The GH_Token need the read:packages permission. diff --git a/backend/config/settings/__init__.py b/backend/config/settings/__init__.py index 1f5d6ef..e69de29 100644 --- a/backend/config/settings/__init__.py +++ b/backend/config/settings/__init__.py @@ -1,2 +0,0 @@ -from .development import * -from .production import * diff --git a/backend/config/settings/development.py b/backend/config/settings/development.py index aec4fed..77b1ca6 100644 --- a/backend/config/settings/development.py +++ b/backend/config/settings/development.py @@ -3,7 +3,7 @@ from config.settings.base import * # noqa: F403 from config.settings.base import SECRET_KEY, env from config.settings.modules import * # noqa: F403 -from config.settings.modules import SIMPLE_JWT +from config.settings.modules import REST_FRAMEWORK, SIMPLE_JWT # Add signing key to JWT settings SIMPLE_JWT["SIGNING_KEY"] = SECRET_KEY @@ -22,6 +22,7 @@ # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration INSTALLED_APPS += ["django_extensions"] # noqa: F405 +REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema" # DATABASES # ------------------------------------------------------------------------------ @@ -42,6 +43,9 @@ # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_AUTO_FIELD DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] # noqa: S104 +ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1", "django"] # noqa: S104 CORS_ALLOWED_ORIGINS = [f"http://{host}:3000" for host in ALLOWED_HOSTS] CSRF_TRUSTED_ORIGINS = [f"http://{host}:3000" for host in ALLOWED_HOSTS] + +# stop the reloader from going crazy +RUNSERVERPLUS_POLLER_RELOADER_TYPE = "stat" diff --git a/backend/config/settings/modules/drf.py b/backend/config/settings/modules/drf.py index ceeb277..fca32d2 100644 --- a/backend/config/settings/modules/drf.py +++ b/backend/config/settings/modules/drf.py @@ -2,7 +2,6 @@ REST_FRAMEWORK = { # To configure APISettings of DRF (rest_framework.settings.APISettings) - "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 50, "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework_simplejwt.authentication.JWTAuthentication",), diff --git a/backend/config/urls.py b/backend/config/urls.py index 2d8533f..db7111c 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -18,10 +18,7 @@ from api.urls import main_api_router from django.http import HttpResponse from django.urls import include, path -from rest_framework_simplejwt.views import ( - TokenObtainPairView, - TokenRefreshView, -) +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView def health_check(request) -> HttpResponse: # noqa: ANN001, ARG001 @@ -34,4 +31,5 @@ def health_check(request) -> HttpResponse: # noqa: ANN001, ARG001 path("api/", include(main_api_router.urls)), path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("api/token/verify/", TokenVerifyView.as_view(), name="token_verify"), ] diff --git a/backend/nango/bridge/float_serializer_method_field.py b/backend/nango/bridge/float_serializer_method_field.py new file mode 100644 index 0000000..3477fff --- /dev/null +++ b/backend/nango/bridge/float_serializer_method_field.py @@ -0,0 +1,5 @@ +from rest_framework import serializers + + +class FloatSerializerMethodField(serializers.SerializerMethodField): + """Used to type method serializers.""" diff --git a/backend/nango/bridge/int_serializer_method_field.py b/backend/nango/bridge/int_serializer_method_field.py new file mode 100644 index 0000000..544c240 --- /dev/null +++ b/backend/nango/bridge/int_serializer_method_field.py @@ -0,0 +1,5 @@ +from rest_framework import serializers + + +class IntSerializerMethodField(serializers.SerializerMethodField): + """Used to type method serializers.""" diff --git a/backend/nango/bridge/string_serializer_method_field.py b/backend/nango/bridge/string_serializer_method_field.py new file mode 100644 index 0000000..a96c053 --- /dev/null +++ b/backend/nango/bridge/string_serializer_method_field.py @@ -0,0 +1,5 @@ +from rest_framework import serializers + + +class StringSerializerMethodField(serializers.SerializerMethodField): + """Used to type method serializers.""" diff --git a/backend/nango/bridge/type_factory.py b/backend/nango/bridge/type_factory.py index 89c61f0..c4d07a1 100644 --- a/backend/nango/bridge/type_factory.py +++ b/backend/nango/bridge/type_factory.py @@ -9,6 +9,10 @@ from rest_framework.relations import ManyRelatedField, PrimaryKeyRelatedField from rest_framework.serializers import ListSerializer, Serializer +from nango.bridge.float_serializer_method_field import FloatSerializerMethodField +from nango.bridge.int_serializer_method_field import IntSerializerMethodField +from nango.bridge.string_serializer_method_field import StringSerializerMethodField + if TYPE_CHECKING: from pathlib import Path @@ -37,7 +41,7 @@ def __init__(self, **kwargs: dict[str, any]) -> None: def get_type_name(self, serializer: Serializer | ListSerializer) -> str: """Return the type name, deduced from the serializer's name.""" - if isinstance(serializer, ListSerializer): + if isinstance(serializer, ListSerializer | ListField): return self.get_type_name(serializer.child) match serializer.__class__.__name__: @@ -46,7 +50,7 @@ def get_type_name(self, serializer: Serializer | ListSerializer) -> str: case "CharField" | "EmailField" | "ChoiceField": return "string" case "DateField" | "DateTimeField": - return "Date" + return "string" case "IntegerField" | "FloatField": return "number" return serializer.__class__.__name__.split("Serializer")[0] @@ -149,7 +153,7 @@ def map_serializer_field_to_type(self, field: Field) -> str: # noqa: PLR0911 case "CharField" | "EmailField" | "ChoiceField": return "string" case "DateField" | "DateTimeField": - return "Date" + return "string" case "IntegerField" | "FloatField": return "number" @@ -161,7 +165,10 @@ def map_serializer_field_to_type(self, field: Field) -> str: # noqa: PLR0911 return "number[]" if isinstance(field, ListSerializer | ListField): return f"{self.get_type_name(field)}[]" - + if isinstance(field, StringSerializerMethodField): + return "string" + if isinstance(field, FloatSerializerMethodField | IntSerializerMethodField): + return "number" print(f"Impossible to get a TypeScript match for {field} ({type(field)})") # noqa: T201 return "" diff --git a/backend/requirements/production.txt b/backend/requirements/production.txt index 39bda30..bc54497 100644 --- a/backend/requirements/production.txt +++ b/backend/requirements/production.txt @@ -1 +1,3 @@ -r ./base.txt + +gunicorn==21.2.0 # https://github.com/benoitc/gunicorn \ No newline at end of file diff --git a/frontend/src/components/pages/forms/signIn.tsx b/frontend/src/components/pages/forms/signIn.tsx index d172a8e..d6e331e 100644 --- a/frontend/src/components/pages/forms/signIn.tsx +++ b/frontend/src/components/pages/forms/signIn.tsx @@ -52,10 +52,10 @@ export default function SignInForm() { loggedIn.setState(true) user.setState({ name: userInfo.username, email: userInfo.email }) toast.success("Login successful!") + router.push(BASE_LOGGED_IN_URL + "/") } else { toast.error("An error has occurred, please try again...") } - router.push(BASE_LOGGED_IN_URL + "/") })} className="w-full space-y-4 py-8" > diff --git a/frontend/src/lib/useMounted.ts b/frontend/src/lib/useMounted.ts new file mode 100644 index 0000000..cc1ede8 --- /dev/null +++ b/frontend/src/lib/useMounted.ts @@ -0,0 +1,12 @@ +import { useEffect, useState } from "react" + +export const useMounted = () => { + const [mounted, setMounted] = useState() + // effects run only client-side + // so we can detect when the component is hydrated/mounted + // @see https://react.dev/reference/react/useEffect + useEffect(() => { + setMounted(true) + }, []) + return mounted +} diff --git a/setup/env_file_generator.py b/setup/env_file_generator.py index a3d57cb..0cdc8d4 100644 --- a/setup/env_file_generator.py +++ b/setup/env_file_generator.py @@ -43,6 +43,7 @@ def build_postgres_env(env_name: str) -> None: folder: Path = make_folder(env_name) path: Path = folder.joinpath(".postgres") if path.exists(): + print(f".{env_name}/.postgres already exists. Stop.") # noqa: T201 return content: str = f"""# PostgreSQL\n @@ -64,6 +65,7 @@ def build_django_secrets(env_name: str) -> None: path: Path = folder.joinpath(".django") if path.exists(): + print(f".{env_name}/.django already exists. Stop.") # noqa: T201 return content: str = f"""# General\n @@ -79,10 +81,11 @@ def build_django_secrets(env_name: str) -> None: STRIPE_ENDPOINT_SECRET = "" # Django # ------------------------------------------------------------------------------ -DJANGO_SECRET_KEY="{get_or_generate_key(name="DJANGO_SECRET_KEY", multiplier=2)}" # noqa: S105 -DJANGO_DEBUG=True -IS_LOCAL=True +DJANGO_SECRET_KEY="{'brBDrH4Gb-65!' if env_name == 'development' else get_or_generate_key(name="DJANGO_SECRET_KEY", multiplier=2)}" # noqa: S105 +DJANGO_DEBUG={env_name!='production'} +IS_LOCAL={env_name!='production'} DJANGO_SETTINGS_MODULE="config.settings.{env_name}" +DB_SETUP='postgres' # (postgres or litestream) # Celery CELERY_FLOWER_USER="{get_or_generate_key(name="CELERY_FLOWER_USER")}" CELERY_FLOWER_PASSWORD="{get_or_generate_key(name="CELERY_FLOWER_PASSWORD", multiplier=2)}" # noqa: S105"""