diff --git a/.dockerignore b/.dockerignore index 0f39bcc7..18710c85 100644 --- a/.dockerignore +++ b/.dockerignore @@ -59,4 +59,5 @@ bower_components # Specific items -docker-compose.yml \ No newline at end of file +docker-compose-dev.yml +docker-compose-prod.yml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6a9f755e..4cedcb20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # SECURITY SENSITIVE FILES -# persistent/* +secrets/ +cert/ # External distributions rsconcept/backend/import/*.whl diff --git a/.vscode/launch.json b/.vscode/launch.json index 99ff9e1f..dcd074d6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -52,6 +52,26 @@ "request": "launch", "script": "${workspaceFolder}/rsconcept/RunServer.ps1", "args": ["-freshStart"] - } + }, + { + "name": "FE-Debug", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceFolder}/rsconcept/frontend/node_modules/.bin/jest", + "args": [ + "${fileBasenameNoExtension}", + "--runInBand", + "--watch", + "--coverage=false", + "--no-cache" + ], + "cwd": "${workspaceFolder}/rsconcept/frontend", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "sourceMaps": true, + "windows": { + "program": "${workspaceFolder}/rsconcept/frontend/node_modules/jest/bin/jest" + } + }, ] } \ No newline at end of file diff --git a/README.md b/README.md index 1f3bf9b7..d6b44c42 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ This readme file is used mostly to document project dependencies - run rsconcept\backend\LocalEnvSetup.ps1 - run 'npm install' in rsconcept\frontend - use VSCode configs in root folder to start developement +- production: create secrets secrets\db_password.txt and django_key.txt +- production: provide TLS certificate nginx\cert\portal-cert.pem and nginx\cert\portal-key.pem # Frontend stack & Tooling [Vite + React + Typescript]
diff --git a/TODO.txt b/TODO.txt index 8c9687f9..640618f5 100644 --- a/TODO.txt +++ b/TODO.txt @@ -2,6 +2,23 @@ This list only contains global tech refactorings and tech debt For more specific TODOs see comments in code +[Functionality] +- home page +- manuals +- текстовый модуль для разрешения отсылок +- компонент для форматирования в редакторе текста (формальное выражения + отсылки в тексте) +- блок нотификаций пользователей +- блок синтеза +- блок организации библиотеки моделей +- проектный модуль? +- обратная связь - система баг репортов + +[Tech] - Use migtation/fixtures to provide initial data for testing - USe migtation/fixtures to load example common data -- Add HTTPS for deployment \ No newline at end of file + +[deployment] +- HTTPS +- database backup daemon +- logs collection +- status dashboard for servers diff --git a/docker-compose.yml b/docker-compose-prod.yml similarity index 65% rename from docker-compose.yml rename to docker-compose-prod.yml index c3ebae27..de83608b 100644 --- a/docker-compose.yml +++ b/docker-compose-prod.yml @@ -1,8 +1,6 @@ -version: "3.9" - volumes: postgres_volume: - name: "postgres-db" + name: "postgresql-db" django_static_volume: name: "static" django_media_volume: @@ -12,6 +10,12 @@ networks: default: name: concept-api-net +secrets: + django_key: + file: ./secrets/django_key.txt + db_password: + file: ./secrets/db_password.txt + services: frontend: restart: always @@ -19,8 +23,8 @@ services: - backend build: context: ./rsconcept/frontend - ports: - - 3000:3000 + expose: + - 3000 command: serve -s /home/node -l 3000 @@ -28,12 +32,17 @@ services: restart: always depends_on: - postgresql-db - - nginx + secrets: + - db_password + - django_key build: context: ./rsconcept/backend - env_file: ./rsconcept/backend/.env.dev - ports: - - 8000:8000 + env_file: ./rsconcept/backend/.env.prod + environment: + SECRET_KEY: /run/secrets/django_key + DB_PASSWORD: /run/secrets/db_password + expose: + - 8000 volumes: - django_static_volume:/home/app/web/static - django_media_volume:/home/app/web/media @@ -44,7 +53,11 @@ services: postgresql-db: restart: always image: postgres:alpine - env_file: ./postgresql/.env.dev + secrets: + - db_password + env_file: ./postgresql/.env.prod + environment: + POSTGRES_PASSWORD: /run/secrets/db_password volumes: - postgres_volume:/var/lib/postgresql/data @@ -54,9 +67,11 @@ services: build: context: ./nginx ports: - - 1337:80 + - 8000:8000 + - 3000:3000 + depends_on: + - backend command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" volumes: - django_static_volume:/var/www/static - django_media_volume:/var/www/media - diff --git a/nginx/Dockerfile b/nginx/Dockerfile index 21caf485..52307f08 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,4 +1,5 @@ FROM nginx:stable-alpine3.17-slim # Сopу nginx configuration to the proxy-server -COPY ./default.conf /etc/nginx/conf.d/default.conf \ No newline at end of file +COPY ./default.conf /etc/nginx/conf.d/default.conf +COPY ./cert/* /etc/ssl/private/ \ No newline at end of file diff --git a/nginx/default.conf b/nginx/default.conf index 0373db16..ad278dca 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -1,12 +1,17 @@ upstream innerdjango { server backend:8000; - # `backend` is the service's name in docker-compose.yml, - # The `innerdjango` is the name of upstream, used by nginx below. +} + +upstream innerreact { + server frontend:3000; } server { - listen 80; - server_name rs.acconcept.ru; + listen 8000 ssl; + ssl_certificate /etc/ssl/private/portal-cert.pem; + ssl_certificate_key /etc/ssl/private/portal-key.pem; + server_name dev.concept.ru www.dev.concept.ru api.portal.acconcept.ru www.api.portal.acconcept.ru; + location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; @@ -19,4 +24,18 @@ server { location /media/ { alias /var/www/media/; } +} + +server { + listen 3000 ssl; + ssl_certificate /etc/ssl/private/portal-cert.pem; + ssl_certificate_key /etc/ssl/private/portal-key.pem; + server_name dev.concept.ru www.dev.concept.ru portal.acconcept.ru www.portal.acconcept.ru; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_pass http://innerreact; + proxy_redirect default; + } } \ No newline at end of file diff --git a/postgresql/.env.dev b/postgresql/.env.dev deleted file mode 100644 index 96838fda..00000000 --- a/postgresql/.env.dev +++ /dev/null @@ -1,3 +0,0 @@ -POSTGRES_USER=dev-test-user -POSTGRES_PASSWORD=02BD82EE0D -POSTGRES_DB=dev-db \ No newline at end of file diff --git a/postgresql/.env.prod b/postgresql/.env.prod new file mode 100644 index 00000000..69f1a2b7 --- /dev/null +++ b/postgresql/.env.prod @@ -0,0 +1,2 @@ +POSTGRES_USER=portal-admin +POSTGRES_DB=portal-db \ No newline at end of file diff --git a/rsconcept/RunServer.ps1 b/rsconcept/RunServer.ps1 index 0b6c3be8..46d7b72d 100644 --- a/rsconcept/RunServer.ps1 +++ b/rsconcept/RunServer.ps1 @@ -46,7 +46,7 @@ function AddAdmin { $env:DJANGO_SUPERUSER_USERNAME = 'admin' $env:DJANGO_SUPERUSER_PASSWORD = '1234' $env:DJANGO_SUPERUSER_EMAIL = 'admin@admin.com' - & $pyExec $djangoSrc createsuperuser --noinput + & $pyExec $djangoSrc createsuperuser --noinput } function DoMigrations { diff --git a/rsconcept/backend/.env.dev b/rsconcept/backend/.env.dev deleted file mode 100644 index a719e182..00000000 --- a/rsconcept/backend/.env.dev +++ /dev/null @@ -1,25 +0,0 @@ -# Application settings -SECRET_KEY=django-insecure-)rq@!&v7l2r%2%q#n!uq+zk@=&yc0^&ql^7%2!%9u)vt1x&j=d -ALLOWED_HOSTS=rs.acconcept.ru;localhost -CSRF_TRUSTED_ORIGINS=http://rs.acconcept.ru:3000;localhost:3000 -CORS_ALLOWED_ORIGINS=http://rs.acconcept.ru:3000;localhost:3000 - - -# File locations -STATIC_ROOT=/home/app/web/static -MEDIA_ROOT=/home/app/web/media - - -# Database settings -DB_ENGINE=django.db.backends.postgresql_psycopg2 -DB_NAME=dev-db -DB_USER=dev-test-user -DB_PASSWORD=02BD82EE0D -DB_HOST=postgresql-db -DB_PORT=5432 - - -# Debug settings -DEBUG=1 -PYTHONDEVMODE=1 -PYTHONTRACEMALLOC=1 \ No newline at end of file diff --git a/rsconcept/backend/.env.prod b/rsconcept/backend/.env.prod new file mode 100644 index 00000000..c8c8bfd2 --- /dev/null +++ b/rsconcept/backend/.env.prod @@ -0,0 +1,24 @@ +# Application settings + +ALLOWED_HOSTS=localhost;portal.acconcept.ru;dev.concept.ru +CSRF_TRUSTED_ORIGINS=https://dev.concept.ru:3000;https://localhost:3000;https://portal.acconcept.ru:3000 +CORS_ALLOWED_ORIGINS=https://dev.concept.ru:3000;https://localhost:3000;https://portal.acconcept.ru:3000 + + +# File locations +STATIC_ROOT=/home/app/web/static +MEDIA_ROOT=/home/app/web/media + + +# Database settings +DB_ENGINE=django.db.backends.postgresql_psycopg2 +DB_NAME=portal-db +DB_USER=portal-admin +DB_HOST=postgresql-db +DB_PORT=5432 + + +# Debug settings +DEBUG=0 +PYTHONDEVMODE=0 +PYTHONTRACEMALLOC=0 \ No newline at end of file diff --git a/rsconcept/backend/Dockerfile b/rsconcept/backend/Dockerfile index dc3d11e5..fbe1973f 100644 --- a/rsconcept/backend/Dockerfile +++ b/rsconcept/backend/Dockerfile @@ -55,8 +55,9 @@ RUN pip install --no-cache /wheels/* && \ rm -rf /wheels # Copy application sources and setup permissions -COPY apps/ ./apps/ +COPY apps/ ./apps COPY project/ ./project +COPY data/ ./data COPY manage.py entrypoint.sh ./ RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.sh && \ chmod +x $APP_HOME/entrypoint.sh && \ @@ -64,6 +65,8 @@ RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.sh && \ chmod -R a+rwx $APP_HOME/static && \ chmod -R a+rwx $APP_HOME/media +RUN + USER app WORKDIR $APP_HOME diff --git a/rsconcept/backend/apps/rsform/serializers.py b/rsconcept/backend/apps/rsform/serializers.py index b71add1b..fd10efd3 100644 --- a/rsconcept/backend/apps/rsform/serializers.py +++ b/rsconcept/backend/apps/rsform/serializers.py @@ -43,9 +43,15 @@ class ConstituentaSerializer(serializers.ModelSerializer): fields = '__all__' read_only_fields = ('id', 'order', 'alias', 'cst_type') - def update(self, instance: Constituenta, validated_data): + def update(self, instance: Constituenta, validated_data) -> Constituenta: + if ('term_raw' in validated_data): + validated_data['term_resolved'] = validated_data['term_raw'] + if ('definition_raw' in validated_data): + validated_data['definition_resolved'] = validated_data['definition_raw'] + + result = super().update(instance, validated_data) instance.schema.save() - return super().update(instance, validated_data) + return result class StandaloneCstSerializer(serializers.ModelSerializer): diff --git a/rsconcept/backend/apps/rsform/tests/t_imports.py b/rsconcept/backend/apps/rsform/tests/t_imports.py index d33d46c2..da54f063 100644 --- a/rsconcept/backend/apps/rsform/tests/t_imports.py +++ b/rsconcept/backend/apps/rsform/tests/t_imports.py @@ -34,6 +34,7 @@ class TestIntegrations(TestCase): schema = self._default_schema() out1 = json.loads(pc.check_expression(schema, 'X1=X1')) self.assertTrue(out1['parseResult']) + self.assertEqual(len(out1['args']), 0) out2 = json.loads(pc.check_expression(schema, 'X1=X2')) self.assertFalse(out2['parseResult']) diff --git a/rsconcept/backend/apps/rsform/tests/t_views.py b/rsconcept/backend/apps/rsform/tests/t_views.py index 26539855..d5e791a0 100644 --- a/rsconcept/backend/apps/rsform/tests/t_views.py +++ b/rsconcept/backend/apps/rsform/tests/t_views.py @@ -31,6 +31,10 @@ class TestConstituentaAPI(APITestCase): alias='X1', schema=self.rsform_owned, order=1, convention='Test') self.cst2 = Constituenta.objects.create( alias='X2', schema=self.rsform_unowned, order=1, convention='Test1') + self.cst3 = Constituenta.objects.create( + alias='X3', schema=self.rsform_owned, order=2, + term_raw='Test1', term_resolved='Test1', + definition_raw='Test1', definition_resolved='Test2') def test_retrieve(self): response = self.client.get(f'/api/constituents/{self.cst1.id}/') @@ -57,6 +61,17 @@ class TestConstituentaAPI(APITestCase): response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json') self.assertEqual(response.status_code, 200) + def test_partial_update_update_resolved(self): + data = json.dumps({ + 'term_raw': 'New term', + 'definition_raw': 'New def' + }) + response = self.client.patch(f'/api/constituents/{self.cst3.id}/', data, content_type='application/json') + self.assertEqual(response.status_code, 200) + self.cst3.refresh_from_db() + self.assertEqual(self.cst3.term_resolved, 'New term') + self.assertEqual(self.cst3.definition_resolved, 'New def') + def test_readonly_cst_fields(self): data = json.dumps({'alias': 'X33', 'order': 10}) response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json') diff --git a/rsconcept/backend/entrypoint.sh b/rsconcept/backend/entrypoint.sh index db802bb4..dff4dec6 100644 --- a/rsconcept/backend/entrypoint.sh +++ b/rsconcept/backend/entrypoint.sh @@ -11,6 +11,7 @@ then echo "Ready!" fi +cd $APP_HOME python $APP_HOME/manage.py collectstatic --noinput --clear python $APP_HOME/manage.py migrate diff --git a/rsconcept/backend/project/settings.py b/rsconcept/backend/project/settings.py index f943ebdc..d9c7b91e 100644 --- a/rsconcept/backend/project/settings.py +++ b/rsconcept/backend/project/settings.py @@ -82,9 +82,9 @@ MIDDLEWARE = [ ] ROOT_URLCONF = 'project.urls' -LOGIN_URL = '/accounts/login/' -LOGIN_REDIRECT_URL = '/home' -LOGOUT_REDIRECT_URL = '/home' +LOGIN_URL = '/admin/login/' +LOGIN_REDIRECT_URL = '/' +LOGOUT_REDIRECT_URL = '/' TEMPLATES = [ { diff --git a/rsconcept/frontend/.gitignore b/rsconcept/frontend/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/rsconcept/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/rsconcept/frontend/Dockerfile b/rsconcept/frontend/Dockerfile index 667e758b..d7a259f6 100644 --- a/rsconcept/frontend/Dockerfile +++ b/rsconcept/frontend/Dockerfile @@ -11,6 +11,7 @@ WORKDIR /result COPY ./ ./ RUN npm install +ENV NODE_ENV production RUN npm run build # ========= Server ======= diff --git a/rsconcept/frontend/package-lock.json b/rsconcept/frontend/package-lock.json index 2ea75e51..62963227 100644 --- a/rsconcept/frontend/package-lock.json +++ b/rsconcept/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "0.1.0", + "version": "1.0.0", "dependencies": { "axios": "^1.4.0", "js-file-download": "^0.4.12", diff --git a/rsconcept/frontend/package.json b/rsconcept/frontend/package.json index 9b2665b2..90b2c691 100644 --- a/rsconcept/frontend/package.json +++ b/rsconcept/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.1.0", + "version": "1.0.0", "type": "module", "scripts": { "test": "jest", diff --git a/rsconcept/frontend/src/App.tsx b/rsconcept/frontend/src/App.tsx index 83bbd004..6769c85d 100644 --- a/rsconcept/frontend/src/App.tsx +++ b/rsconcept/frontend/src/App.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { Route, Routes } from 'react-router-dom'; import Footer from './components/Footer'; @@ -16,9 +17,21 @@ import RSFormPage from './pages/RSFormPage'; import UserProfilePage from './pages/UserProfilePage'; function App () { - const {noNavigation} = useConceptTheme() - const nav_height: string = noNavigation ? "7.5rem" : "7.4rem"; - const main_clsN: string = `min-h-[calc(100vh-${nav_height})] px-2 h-fit`; + const { noNavigation } = useConceptTheme(); + + const scrollWindowSize = useMemo( + () => { + return !noNavigation ? + 'max-h-[calc(100vh-4.5rem)]' + : 'max-h-[100vh]'; + }, [noNavigation]); + const mainSize = useMemo( + () => { + return !noNavigation ? + 'min-h-[calc(100vh-12rem)]' + : 'min-h-[calc(100vh-8rem)] '; + }, [noNavigation]); + return (
@@ -28,7 +41,9 @@ function App () { draggable={false} pauseOnFocusLoss={false} /> -
+ +
+
} /> @@ -46,6 +61,7 @@ function App () {
+
); } diff --git a/rsconcept/frontend/src/components/Common/Button.tsx b/rsconcept/frontend/src/components/Common/Button.tsx index 90d5b03f..0e2f9524 100644 --- a/rsconcept/frontend/src/components/Common/Button.tsx +++ b/rsconcept/frontend/src/components/Common/Button.tsx @@ -13,7 +13,9 @@ extends Omit, 'className' | 'child function Button({ id, text, icon, tooltip, dense, disabled, - borderClass = 'border rounded', colorClass = 'clr-btn-default', widthClass = 'w-fit h-fit', + borderClass = 'border rounded', + colorClass = 'clr-btn-default', + widthClass = 'w-fit h-fit', loading, onClick, ...props }: ButtonProps) { diff --git a/rsconcept/frontend/src/components/Common/Modal.tsx b/rsconcept/frontend/src/components/Common/Modal.tsx index 975dafea..404c0429 100644 --- a/rsconcept/frontend/src/components/Common/Modal.tsx +++ b/rsconcept/frontend/src/components/Common/Modal.tsx @@ -6,14 +6,15 @@ import Button from './Button'; interface ModalProps { title?: string submitText?: string + readonly?: boolean canSubmit?: boolean hideWindow: () => void - onSubmit: () => void + onSubmit?: () => void onCancel?: () => void children: React.ReactNode } -function Modal({ title, hideWindow, onSubmit, onCancel, canSubmit, children, submitText = 'Продолжить' }: ModalProps) { +function Modal({ title, hideWindow, onSubmit, readonly, onCancel, canSubmit, children, submitText = 'Продолжить' }: ModalProps) { const ref = useRef(null); useEscapeKey(hideWindow); @@ -24,29 +25,32 @@ function Modal({ title, hideWindow, onSubmit, onCancel, canSubmit, children, sub const handleSubmit = () => { hideWindow(); - onSubmit(); + if (onSubmit) onSubmit(); }; return ( <>
-
+
{ title &&

{title}

} -
+
{children}
-
-