dap-platform 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. dap_platform-0.1.0/.github/workflows/publish.yml +21 -0
  2. dap_platform-0.1.0/.gitignore +19 -0
  3. dap_platform-0.1.0/LICENSE +21 -0
  4. dap_platform-0.1.0/PKG-INFO +31 -0
  5. dap_platform-0.1.0/README.md +5 -0
  6. dap_platform-0.1.0/pyproject.toml +33 -0
  7. dap_platform-0.1.0/rename.py +23 -0
  8. dap_platform-0.1.0/rename_pkg.py +9 -0
  9. dap_platform-0.1.0/src/dataaudit/__init__.py +2 -0
  10. dap_platform-0.1.0/src/dataaudit/__main__.py +3 -0
  11. dap_platform-0.1.0/src/dataaudit/app.py +473 -0
  12. dap_platform-0.1.0/src/dataaudit/auth/__init__.py +20 -0
  13. dap_platform-0.1.0/src/dataaudit/auth/dependencies.py +80 -0
  14. dap_platform-0.1.0/src/dataaudit/auth/local_auth.py +40 -0
  15. dap_platform-0.1.0/src/dataaudit/auth/models.py +94 -0
  16. dap_platform-0.1.0/src/dataaudit/auth/session.py +95 -0
  17. dap_platform-0.1.0/src/dataaudit/cli.py +80 -0
  18. dap_platform-0.1.0/src/dataaudit/config/useful_links.yaml +58 -0
  19. dap_platform-0.1.0/src/dataaudit/database.py +230 -0
  20. dap_platform-0.1.0/src/dataaudit/routers/__init__.py +1 -0
  21. dap_platform-0.1.0/src/dataaudit/routers/bi_native.py +688 -0
  22. dap_platform-0.1.0/src/dataaudit/routers/grc.py +3068 -0
  23. dap_platform-0.1.0/src/dataaudit/routers/my_reports.py +140 -0
  24. dap_platform-0.1.0/src/dataaudit/routers/workflows.py +37 -0
  25. dap_platform-0.1.0/src/dataaudit/schema.py +1078 -0
  26. dap_platform-0.1.0/src/dataaudit/services/__init__.py +1 -0
  27. dap_platform-0.1.0/src/dataaudit/services/docgen.py +421 -0
  28. dap_platform-0.1.0/src/dataaudit/services/email.py +87 -0
  29. dap_platform-0.1.0/src/dataaudit/static/css/bi.css +967 -0
  30. dap_platform-0.1.0/src/dataaudit/static/css/grc.css +1434 -0
  31. dap_platform-0.1.0/src/dataaudit/static/css/style.css +989 -0
  32. dap_platform-0.1.0/src/dataaudit/static/images/favicon.png +0 -0
  33. dap_platform-0.1.0/src/dataaudit/static/images/favicon_old.png +0 -0
  34. dap_platform-0.1.0/src/dataaudit/static/images/logo.png +0 -0
  35. dap_platform-0.1.0/src/dataaudit/static/images/logo_old.png +0 -0
  36. dap_platform-0.1.0/src/dataaudit/static/js/app.js +11 -0
  37. dap_platform-0.1.0/src/dataaudit/static/js/bi.js +696 -0
  38. dap_platform-0.1.0/src/dataaudit/static/js/grc.js +422 -0
  39. dap_platform-0.1.0/src/dataaudit/templates/base.html +124 -0
  40. dap_platform-0.1.0/src/dataaudit/templates/login.html +68 -0
  41. dap_platform-0.1.0/src/dataaudit/templates/pages/ai_assistant.html +15 -0
  42. dap_platform-0.1.0/src/dataaudit/templates/pages/bi/chart_edit.html +642 -0
  43. dap_platform-0.1.0/src/dataaudit/templates/pages/bi/charts.html +88 -0
  44. dap_platform-0.1.0/src/dataaudit/templates/pages/bi/dashboard_view.html +198 -0
  45. dap_platform-0.1.0/src/dataaudit/templates/pages/bi/dashboards.html +134 -0
  46. dap_platform-0.1.0/src/dataaudit/templates/pages/bi/datasources.html +243 -0
  47. dap_platform-0.1.0/src/dataaudit/templates/pages/bi/sql_lab.html +395 -0
  48. dap_platform-0.1.0/src/dataaudit/templates/pages/bi_analytics.html +167 -0
  49. dap_platform-0.1.0/src/dataaudit/templates/pages/data_governance.html +15 -0
  50. dap_platform-0.1.0/src/dataaudit/templates/pages/grc/engagement_detail.html +525 -0
  51. dap_platform-0.1.0/src/dataaudit/templates/pages/grc/engagements.html +106 -0
  52. dap_platform-0.1.0/src/dataaudit/templates/pages/grc/finding_detail.html +394 -0
  53. dap_platform-0.1.0/src/dataaudit/templates/pages/grc/notifications.html +97 -0
  54. dap_platform-0.1.0/src/dataaudit/templates/pages/grc/plan_detail.html +381 -0
  55. dap_platform-0.1.0/src/dataaudit/templates/pages/grc/plans.html +178 -0
  56. dap_platform-0.1.0/src/dataaudit/templates/pages/grc/remediation_detail.html +381 -0
  57. dap_platform-0.1.0/src/dataaudit/templates/pages/grc/remediations.html +183 -0
  58. dap_platform-0.1.0/src/dataaudit/templates/pages/grc/settings_workflows.html +772 -0
  59. dap_platform-0.1.0/src/dataaudit/templates/pages/grc/strategic_plan_detail.html +400 -0
  60. dap_platform-0.1.0/src/dataaudit/templates/pages/grc/strategic_plans.html +168 -0
  61. dap_platform-0.1.0/src/dataaudit/templates/pages/grc/universe.html +729 -0
  62. dap_platform-0.1.0/src/dataaudit/templates/pages/home.html +59 -0
  63. dap_platform-0.1.0/src/dataaudit/templates/pages/my_reports.html +562 -0
  64. dap_platform-0.1.0/src/dataaudit/templates/pages/reports.html +15 -0
  65. dap_platform-0.1.0/src/dataaudit/templates/pages/settings.html +15 -0
  66. dap_platform-0.1.0/src/dataaudit/templates/pages/useful_links.html +139 -0
  67. dap_platform-0.1.0/src/dataaudit/templates/pages/workflows.html +322 -0
@@ -0,0 +1,21 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ id-token: write
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: '3.12'
19
+ - run: pip install build
20
+ - run: python -m build
21
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,19 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .venv/
10
+ venv/
11
+ *.db
12
+ *.sqlite
13
+ .idea/
14
+ .vscode/
15
+ *.swp
16
+ *.swo
17
+ check_*.py
18
+ fix_*.py
19
+ diagnose*.py
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pavel Maximov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: dap-platform
3
+ Version: 0.1.0
4
+ Summary: Audit management platform with GRC workflows and BI analytics
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: fastapi>=0.104
9
+ Requires-Dist: httpx>=0.25
10
+ Requires-Dist: itsdangerous>=2.0
11
+ Requires-Dist: jinja2>=3.1
12
+ Requires-Dist: python-docx>=1.0
13
+ Requires-Dist: python-multipart>=0.0.6
14
+ Requires-Dist: pyyaml>=6.0
15
+ Requires-Dist: uvicorn[standard]>=0.24
16
+ Provides-Extra: duckdb
17
+ Requires-Dist: duckdb>=0.9; extra == 'duckdb'
18
+ Provides-Extra: ldap
19
+ Requires-Dist: ldap3>=2.9; extra == 'ldap'
20
+ Provides-Extra: oracle
21
+ Requires-Dist: oracledb>=2.0; extra == 'oracle'
22
+ Provides-Extra: postgres
23
+ Requires-Dist: asyncpg>=0.29; extra == 'postgres'
24
+ Requires-Dist: psycopg2-binary>=2.9; extra == 'postgres'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # DataAudit Platform
28
+ Audit management platform with GRC workflows and BI analytics.
29
+
30
+ Install: pip install dap-platform
31
+ Run: dataaudit serve
@@ -0,0 +1,5 @@
1
+ # DataAudit Platform
2
+ Audit management platform with GRC workflows and BI analytics.
3
+
4
+ Install: pip install dap-platform
5
+ Run: dataaudit serve
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dap-platform"
7
+ version = "0.1.0"
8
+ description = "Audit management platform with GRC workflows and BI analytics"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ dependencies = [
13
+ "fastapi>=0.104",
14
+ "uvicorn[standard]>=0.24",
15
+ "jinja2>=3.1",
16
+ "python-multipart>=0.0.6",
17
+ "itsdangerous>=2.0",
18
+ "httpx>=0.25",
19
+ "pyyaml>=6.0",
20
+ "python-docx>=1.0",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ postgres = ["asyncpg>=0.29", "psycopg2-binary>=2.9"]
25
+ ldap = ["ldap3>=2.9"]
26
+ oracle = ["oracledb>=2.0"]
27
+ duckdb = ["duckdb>=0.9"]
28
+
29
+ [project.scripts]
30
+ dataaudit = "dataaudit.cli:main"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/dataaudit"]
@@ -0,0 +1,23 @@
1
+ import pathlib
2
+
3
+ # 1. pyproject.toml
4
+ pp = pathlib.Path("pyproject.toml")
5
+ txt = pp.read_text(encoding="utf-8")
6
+ txt = txt.replace('name = "dataaudit"', 'name = "dap-platform"', 1)
7
+ pp.write_text(txt, encoding="utf-8")
8
+ print("pyproject.toml: name -> dap-platform")
9
+
10
+ # 2. README.md - update install commands
11
+ rm = pathlib.Path("README.md")
12
+ if rm.exists():
13
+ rtxt = rm.read_text(encoding="utf-8")
14
+ rtxt = rtxt.replace("pip install dataaudit", "pip install dap-platform")
15
+ rtxt = rtxt.replace("pypi.org/project/dataaudit", "pypi.org/project/dap-platform")
16
+ rtxt = rtxt.replace("badge.fury.io/py/dataaudit", "badge.fury.io/py/dap-platform")
17
+ rm.write_text(rtxt, encoding="utf-8")
18
+ print("README.md: updated")
19
+
20
+ # 3. Rebuild
21
+ print("\nNow run:")
22
+ print(" Remove-Item dist/* -Force")
23
+ print(" python -m build")
@@ -0,0 +1,9 @@
1
+ options = [
2
+ "dap-platform",
3
+ "dataaudit-platform",
4
+ "audit-platform",
5
+ "dap-audit",
6
+ "dataaudit-grc",
7
+ ]
8
+ for o in options:
9
+ print(f" pip install {o}")
@@ -0,0 +1,2 @@
1
+ """DataAudit Platform — Audit Management & Analytics"""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ """Allow `python -m dataaudit`."""
2
+ from .cli import main
3
+ main()
@@ -0,0 +1,473 @@
1
+ from fastapi import FastAPI, Request, Form
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.templating import Jinja2Templates
4
+ from fastapi.responses import HTMLResponse, RedirectResponse
5
+ from pathlib import Path
6
+ import asyncio
7
+ import os
8
+ import logging
9
+ from datetime import datetime, time as dtime, timedelta
10
+ import yaml
11
+
12
+ from dataaudit.routers.bi_native import router as bi_router
13
+ from dataaudit.routers.workflows import router as workflows_router
14
+ from dataaudit.routers.grc import router as grc_router
15
+ from dataaudit.routers.my_reports import router as my_reports_router
16
+ from dataaudit.auth import (
17
+ get_current_user,
18
+ auth_middleware,
19
+ add_user_to_context,
20
+ )
21
+ from dataaudit.auth.local_auth import get_auth_client
22
+ from dataaudit.auth.session import get_session_manager
23
+ logging.getLogger("dap.scheduler").setLevel(logging.INFO)
24
+ logging.getLogger("dap.scheduler").addHandler(logging.StreamHandler())
25
+ app = FastAPI(title="DataAudit Platform")
26
+ app.include_router(bi_router)
27
+ app.include_router(workflows_router)
28
+ app.include_router(grc_router)
29
+ app.include_router(my_reports_router)
30
+
31
+
32
+ # Auth middleware - protects all routes except /login, /health, /static
33
+ app.middleware("http")(auth_middleware)
34
+
35
+ # Mount static files
36
+ _PKG_DIR = Path(__file__).parent
37
+ app.mount("/static", StaticFiles(directory=str(_PKG_DIR / "static")), name="static")
38
+
39
+ # Setup Jinja2 templates
40
+ templates = Jinja2Templates(directory=str(_PKG_DIR / "templates"))
41
+
42
+
43
+ def load_useful_links():
44
+ """Load useful links from YAML config"""
45
+ config_path = _PKG_DIR / "config" / "useful_links.yaml"
46
+ try:
47
+ with open(config_path, 'r', encoding='utf-8') as f:
48
+ return yaml.safe_load(f)
49
+ except Exception as e:
50
+ print(f"Error loading useful links: {e}")
51
+ return {"categories": []}
52
+
53
+
54
+ @app.get("/", response_class=HTMLResponse)
55
+ async def home(request: Request):
56
+ return templates.TemplateResponse(
57
+ "pages/home.html",
58
+ {"request": request, "current_page": "home", **add_user_to_context(request)}
59
+ )
60
+
61
+
62
+ @app.get("/reports", response_class=HTMLResponse)
63
+ async def reports(request: Request):
64
+ """Legacy route - redirect to my-reports"""
65
+ return RedirectResponse(url="/my-reports", status_code=302)
66
+
67
+
68
+ @app.get("/my-reports", response_class=HTMLResponse)
69
+ async def my_reports_page(request: Request):
70
+ return templates.TemplateResponse(
71
+ "pages/my_reports.html",
72
+ {"request": request, "current_page": "my-reports", **add_user_to_context(request)}
73
+ )
74
+
75
+
76
+ # ── BI Analytics (Native) ─────────────────────────────────────────────
77
+
78
+ @app.get("/analytics", response_class=HTMLResponse)
79
+ async def analytics(request: Request):
80
+ return templates.TemplateResponse(
81
+ "pages/bi_analytics.html",
82
+ {"request": request, "current_page": "analytics", **add_user_to_context(request)}
83
+ )
84
+
85
+
86
+ @app.get("/analytics/sql", response_class=HTMLResponse)
87
+ async def analytics_sql(request: Request):
88
+ return templates.TemplateResponse(
89
+ "pages/bi/sql_lab.html",
90
+ {"request": request, "current_page": "analytics", **add_user_to_context(request)}
91
+ )
92
+
93
+
94
+ @app.get("/analytics/charts", response_class=HTMLResponse)
95
+ async def analytics_charts(request: Request):
96
+ return templates.TemplateResponse(
97
+ "pages/bi/charts.html",
98
+ {"request": request, "current_page": "analytics", **add_user_to_context(request)}
99
+ )
100
+
101
+
102
+ @app.get("/analytics/charts/{chart_id}/edit", response_class=HTMLResponse)
103
+ async def analytics_chart_edit(request: Request, chart_id: int):
104
+ return templates.TemplateResponse(
105
+ "pages/bi/chart_edit.html",
106
+ {"request": request, "current_page": "analytics", "chart_id": chart_id, **add_user_to_context(request)}
107
+ )
108
+
109
+
110
+ @app.get("/analytics/dashboards", response_class=HTMLResponse)
111
+ async def analytics_dashboards(request: Request):
112
+ return templates.TemplateResponse(
113
+ "pages/bi/dashboards.html",
114
+ {"request": request, "current_page": "analytics", **add_user_to_context(request)}
115
+ )
116
+
117
+
118
+ @app.get("/analytics/dashboards/{dashboard_id}", response_class=HTMLResponse)
119
+ async def analytics_dashboard_view(request: Request, dashboard_id: int):
120
+ return templates.TemplateResponse(
121
+ "pages/bi/dashboard_view.html",
122
+ {"request": request, "current_page": "analytics", "dashboard_id": dashboard_id, **add_user_to_context(request)}
123
+ )
124
+
125
+
126
+ @app.get("/analytics/datasources", response_class=HTMLResponse)
127
+ async def analytics_datasources(request: Request):
128
+ return templates.TemplateResponse(
129
+ "pages/bi/datasources.html",
130
+ {"request": request, "current_page": "analytics", **add_user_to_context(request)}
131
+ )
132
+
133
+
134
+ # ── GRC Workflows (Native) ────────────────────────────────────────────
135
+
136
+ @app.get("/workflows", response_class=HTMLResponse)
137
+ async def workflows(request: Request):
138
+ return templates.TemplateResponse(
139
+ "pages/workflows.html",
140
+ {"request": request, "current_page": "workflows", **add_user_to_context(request)}
141
+ )
142
+
143
+
144
+ @app.get("/workflows/plans", response_class=HTMLResponse)
145
+ async def grc_plans(request: Request):
146
+ return templates.TemplateResponse(
147
+ "pages/grc/plans.html",
148
+ {"request": request, "current_page": "workflows", **add_user_to_context(request)}
149
+ )
150
+
151
+
152
+ @app.get("/workflows/plans/{plan_id}", response_class=HTMLResponse)
153
+ async def grc_plan_detail(request: Request, plan_id: int):
154
+ return templates.TemplateResponse(
155
+ "pages/grc/plan_detail.html",
156
+ {"request": request, "current_page": "workflows", "plan_id": plan_id, **add_user_to_context(request)}
157
+ )
158
+
159
+
160
+ @app.get("/workflows/engagements", response_class=HTMLResponse)
161
+ async def grc_engagements(request: Request):
162
+ return templates.TemplateResponse(
163
+ "pages/grc/engagements.html",
164
+ {"request": request, "current_page": "workflows", **add_user_to_context(request)}
165
+ )
166
+
167
+
168
+ @app.get("/workflows/engagements/{engagement_id}", response_class=HTMLResponse)
169
+ async def grc_engagement_detail(request: Request, engagement_id: int):
170
+ return templates.TemplateResponse(
171
+ "pages/grc/engagement_detail.html",
172
+ {"request": request, "current_page": "workflows", "engagement_id": engagement_id, **add_user_to_context(request)}
173
+ )
174
+
175
+
176
+ @app.get("/workflows/findings/{finding_id}", response_class=HTMLResponse)
177
+ async def grc_finding_detail(request: Request, finding_id: int):
178
+ return templates.TemplateResponse(
179
+ "pages/grc/finding_detail.html",
180
+ {"request": request, "current_page": "workflows", "finding_id": finding_id, **add_user_to_context(request)}
181
+ )
182
+
183
+
184
+ @app.get("/workflows/remediations", response_class=HTMLResponse)
185
+ async def grc_remediations(request: Request):
186
+ return templates.TemplateResponse(
187
+ "pages/grc/remediations.html",
188
+ {"request": request, "current_page": "workflows", **add_user_to_context(request)}
189
+ )
190
+
191
+
192
+ @app.get("/workflows/remediations/{remediation_id}", response_class=HTMLResponse)
193
+ async def grc_remediation_detail(request: Request, remediation_id: int):
194
+ return templates.TemplateResponse(
195
+ "pages/grc/remediation_detail.html",
196
+ {"request": request, "current_page": "workflows", "remediation_id": remediation_id, **add_user_to_context(request)}
197
+ )
198
+
199
+
200
+ @app.get("/workflows/notifications", response_class=HTMLResponse)
201
+ async def grc_notifications(request: Request):
202
+ return templates.TemplateResponse(
203
+ "pages/grc/notifications.html",
204
+ {"request": request, "current_page": "workflows", **add_user_to_context(request)}
205
+ )
206
+
207
+
208
+ @app.get("/workflows/settings", response_class=HTMLResponse)
209
+ async def grc_settings(request: Request):
210
+ return templates.TemplateResponse(
211
+ "pages/grc/settings_workflows.html",
212
+ {"request": request, "current_page": "workflows", **add_user_to_context(request)}
213
+ )
214
+
215
+ @app.get("/workflows/universe", response_class=HTMLResponse)
216
+ async def page_universe(request: Request):
217
+ return templates.TemplateResponse(
218
+ "pages/grc/universe.html",
219
+ {"request": request, "current_page": "workflows", **add_user_to_context(request)}
220
+ )
221
+
222
+ @app.get("/workflows/strategic-plans", response_class=HTMLResponse)
223
+ async def page_strategic_plans(request: Request):
224
+ return templates.TemplateResponse(
225
+ "pages/grc/strategic_plans.html",
226
+ {"request": request, "current_page": "workflows", **add_user_to_context(request)}
227
+ )
228
+
229
+ @app.get("/workflows/strategic-plans/{plan_id}", response_class=HTMLResponse)
230
+ async def page_strategic_plan_detail(request: Request, plan_id: int):
231
+ return templates.TemplateResponse(
232
+ "pages/grc/strategic_plan_detail.html",
233
+ {"request": request, "current_page": "workflows", "plan_id": plan_id, **add_user_to_context(request)}
234
+ )
235
+ # ── Other Pages ────────────────────────────────────────────────────────
236
+
237
+ @app.get("/ai", response_class=HTMLResponse)
238
+ async def ai_assistant(request: Request):
239
+ return templates.TemplateResponse(
240
+ "pages/ai_assistant.html",
241
+ {"request": request, "current_page": "ai", **add_user_to_context(request)}
242
+ )
243
+
244
+
245
+ @app.get("/governance", response_class=HTMLResponse)
246
+ async def data_governance(request: Request):
247
+ return templates.TemplateResponse(
248
+ "pages/data_governance.html",
249
+ {"request": request, "current_page": "governance", **add_user_to_context(request)}
250
+ )
251
+
252
+
253
+ @app.get("/useful-links", response_class=HTMLResponse)
254
+ async def useful_links(request: Request):
255
+ links_config = load_useful_links()
256
+ return templates.TemplateResponse(
257
+ "pages/useful_links.html",
258
+ {
259
+ "request": request,
260
+ "current_page": "useful-links",
261
+ "categories": links_config.get("categories", []),
262
+ **add_user_to_context(request)
263
+ }
264
+ )
265
+
266
+
267
+ @app.get("/settings", response_class=HTMLResponse)
268
+ async def settings(request: Request):
269
+ return templates.TemplateResponse(
270
+ "pages/settings.html",
271
+ {"request": request, "current_page": "settings", **add_user_to_context(request)}
272
+ )
273
+
274
+
275
+ @app.get("/login", response_class=HTMLResponse)
276
+ async def login(request: Request, next: str = "/", error: str = None):
277
+ # If already logged in, redirect
278
+ user = await get_current_user(request)
279
+ if user:
280
+ return RedirectResponse(url=next, status_code=302)
281
+
282
+ error_messages = {
283
+ "invalid": "Неверный логин или пароль",
284
+ "expired": "Сессия истекла. Войдите снова.",
285
+ "unavailable": "Сервис авторизации недоступен. Обратитесь в службу поддержки.",
286
+ }
287
+
288
+ return templates.TemplateResponse(
289
+ "login.html",
290
+ {"request": request, "next": next, "error_message": error_messages.get(error)}
291
+ )
292
+
293
+
294
+ @app.post("/login")
295
+ async def login_post(
296
+ request: Request,
297
+ username: str = Form(...),
298
+ password: str = Form(...),
299
+ next: str = Form("/"),
300
+ ):
301
+ auth = get_auth_client()
302
+ session_mgr = get_session_manager()
303
+
304
+ result = await auth.authenticate(username.strip(), password)
305
+
306
+ if not result.success:
307
+ error = "unavailable" if result.error_code == "LDAP_UNREACHABLE" else "invalid"
308
+ return RedirectResponse(url=f"/login?error={error}&next={next}", status_code=302)
309
+
310
+ session = session_mgr.create_session(result.user)
311
+ response = RedirectResponse(url=next, status_code=302)
312
+ session_mgr.set_session_cookie(response, session)
313
+ return response
314
+
315
+
316
+ @app.get("/logout")
317
+ async def logout(request: Request):
318
+ session_mgr = get_session_manager()
319
+ html = """
320
+ <!DOCTYPE html>
321
+ <html>
322
+ <head><title>Выход...</title></head>
323
+ <body>
324
+ <script>
325
+ window.location.replace('/login');
326
+ </script>
327
+ </body>
328
+ </html>
329
+ """
330
+ response = HTMLResponse(html)
331
+ session_mgr.logout(request, response)
332
+ response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
333
+ return response
334
+
335
+
336
+ @app.get("/api/me")
337
+ async def me(request: Request):
338
+ user = await get_current_user(request)
339
+ if not user:
340
+ return {"error": "Не авторизован"}
341
+ return user.to_dict()
342
+
343
+
344
+ # ── Escalation Scheduler ─────────────────────────────────────────────
345
+ _sched_logger = logging.getLogger("dap.scheduler")
346
+
347
+
348
+ async def _run_escalation():
349
+ """Direct escalation check — no HTTP, no auth needed."""
350
+ from dataaudit.database import get_pool
351
+ from dataaudit.routers.grc import (
352
+ _get_config, _notify, _resolve_user_email,
353
+ _escalation_email_html, get_email_service, _today,
354
+ )
355
+
356
+ pool = await get_pool()
357
+ today = _today()
358
+
359
+ escalation_config = await _get_config(pool, "escalation", "deadlines")
360
+ thresholds = escalation_config.get("thresholds", [])
361
+
362
+ tasks = await pool.fetch(
363
+ """SELECT r.*, f.title as finding_title, f.ref_number as finding_ref,
364
+ u.head_user_id as unit_head
365
+ FROM grc.remediation_tasks r
366
+ LEFT JOIN grc.findings f ON r.finding_id = f.id
367
+ LEFT JOIN grc.org_units u ON r.assigned_to_unit_id = u.id
368
+ WHERE r.status NOT IN ('closed', 'cancelled', 'verified')
369
+ AND r.escalation_enabled = TRUE
370
+ AND (r.escalation_muted_until IS NULL OR r.escalation_muted_until < $1)""",
371
+ today,
372
+ )
373
+
374
+ created = 0
375
+ for task in tasks:
376
+ deadline = task["deadline"]
377
+ days_until = (deadline - today).days
378
+ assignee = task["assigned_to_user_id"]
379
+ unit_head = task["unit_head"]
380
+ ref = task["ref_number"] or f"#{task['id']}"
381
+ title = task["title"]
382
+
383
+ if days_until < 0 and not task["is_overdue"]:
384
+ await pool.execute(
385
+ "UPDATE grc.remediation_tasks SET is_overdue=TRUE, updated_at=NOW() WHERE id=$1",
386
+ task["id"],
387
+ )
388
+
389
+ for threshold in thresholds:
390
+ matched = False
391
+ ntype = threshold.get("type", "")
392
+ if "days_before" in threshold:
393
+ db = threshold["days_before"]
394
+ if (db > 0 and days_until == db) or (db == 0 and days_until == 0):
395
+ matched = True
396
+ if "days_after" in threshold and days_until < 0:
397
+ if threshold.get("repeat") == "daily":
398
+ matched = True
399
+ elif abs(days_until) == threshold["days_after"]:
400
+ matched = True
401
+ if not matched:
402
+ continue
403
+
404
+ notify_targets = threshold.get("notify", [])
405
+ overdue_days = abs(days_until) if days_until < 0 else 0
406
+ msg_prefix = (
407
+ f"Срок через {days_until} дн." if days_until > 0
408
+ else "Срок сегодня" if days_until == 0
409
+ else f"Просрочено {overdue_days} дн."
410
+ )
411
+
412
+ people = []
413
+ if "assignee" in notify_targets and assignee:
414
+ people.append((
415
+ assignee,
416
+ f"{msg_prefix}: {ref}",
417
+ f"Рекомендация «{title}» — срок до {deadline}",
418
+ ))
419
+ if "unit_head" in notify_targets and unit_head:
420
+ people.append((
421
+ unit_head,
422
+ f"{msg_prefix}: {ref} (подчинённый)",
423
+ f"Рекомендация «{title}» назначена {assignee} — срок до {deadline}",
424
+ ))
425
+
426
+ for (uid, ntitle, nmessage) in people:
427
+ existing = await pool.fetchval(
428
+ """SELECT COUNT(*) FROM grc.notifications
429
+ WHERE user_id=$1 AND notif_type=$2 AND entity_type='remediation'
430
+ AND entity_id=$3 AND created_at::date=$4""",
431
+ uid, ntype, task["id"], today,
432
+ )
433
+ if existing == 0:
434
+ await _notify(pool, uid, ntype, ntitle, nmessage, "remediation", task["id"])
435
+ created += 1
436
+ email_svc = get_email_service()
437
+ user_email = await _resolve_user_email(pool, uid)
438
+ if user_email:
439
+ email_svc.send(
440
+ to=user_email,
441
+ subject=f"DAP: {ntitle}",
442
+ body_text=f"{ntitle}\n\n{nmessage}\n\n— DataAudit Platform",
443
+ body_html=_escalation_email_html(ntitle, nmessage, ref, deadline),
444
+ )
445
+
446
+ _sched_logger.info(f"Escalation check: {len(tasks)} tasks, {created} notifications")
447
+
448
+
449
+ async def _escalation_scheduler():
450
+ """Run escalation daily at 09:00."""
451
+ while True:
452
+ now = datetime.now()
453
+ target = datetime.combine(now.date(), dtime(9, 0))
454
+ if now >= target:
455
+ target += timedelta(days=1)
456
+ wait = (target - now).total_seconds()
457
+ _sched_logger.info(f"Next escalation: {target} (in {wait:.0f}s)")
458
+ await asyncio.sleep(wait)
459
+ try:
460
+ await _run_escalation()
461
+ except Exception as e:
462
+ _sched_logger.error(f"Escalation failed: {e}")
463
+
464
+
465
+ @app.on_event("startup")
466
+ async def start_scheduler():
467
+ if os.getenv("DAP_SCHEDULER", "false").lower() == "true":
468
+ asyncio.create_task(_escalation_scheduler())
469
+
470
+
471
+ @app.get("/health")
472
+ async def health():
473
+ return {"status": "ok"}
@@ -0,0 +1,20 @@
1
+ """DAP Authentication Module"""
2
+
3
+ from .models import User, Role, Session, AuthResult
4
+ from .local_auth import LocalAuth, get_auth_client
5
+ from .session import SessionManager, get_session_manager
6
+ from .dependencies import (
7
+ get_current_user,
8
+ require_auth,
9
+ require_role,
10
+ auth_middleware,
11
+ add_user_to_context,
12
+ )
13
+
14
+ __all__ = [
15
+ "User", "Role", "Session", "AuthResult",
16
+ "LocalAuth", "get_auth_client",
17
+ "SessionManager", "get_session_manager",
18
+ "get_current_user", "require_auth", "require_role",
19
+ "auth_middleware", "add_user_to_context",
20
+ ]