Compare commits
51
Commits
80cebeaf9c
..
main
@@ -0,0 +1,7 @@
|
|||||||
|
build/
|
||||||
|
.git/
|
||||||
|
.vscode/
|
||||||
|
.cache/
|
||||||
|
uploads/
|
||||||
|
.env
|
||||||
|
.env.example
|
||||||
+15
-2
@@ -3,5 +3,18 @@ MYSQL_DATABASE=visione
|
|||||||
MYSQL_USER=visione
|
MYSQL_USER=visione
|
||||||
MYSQL_PASSWORD=visione
|
MYSQL_PASSWORD=visione
|
||||||
MYSQL_ROOT_PASSWORD=root
|
MYSQL_ROOT_PASSWORD=root
|
||||||
MAILHOG_SMTP_PORT=1025
|
MAILPIT_SMTP_PORT=1025
|
||||||
MAILHOG_WEB_PORT=8025
|
MAILPIT_WEB_PORT=8025
|
||||||
|
|
||||||
|
MAIL_PROVIDER=smtp
|
||||||
|
SMTP_HOST=127.0.0.1
|
||||||
|
SMTP_PORT=1025
|
||||||
|
SMTP_USERNAME=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SMTP_USE_TLS=false
|
||||||
|
SMTP_USE_STARTTLS=false
|
||||||
|
SMTP_VERIFY_PEER=false
|
||||||
|
SMTP_VERIFY_HOST=false
|
||||||
|
|
||||||
|
MAIL_FROM=[email protected]
|
||||||
|
CORS_ALLOWED_ORIGIN=http://localhost:3000
|
||||||
|
|||||||
@@ -566,4 +566,5 @@ FodyWeavers.xsd
|
|||||||
# Additional files built by Visual Studio
|
# Additional files built by Visual Studio
|
||||||
*.tlog
|
*.tlog
|
||||||
|
|
||||||
|
build-codex/
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/intellij+all,visualstudio,visualstudiocode,cmake,c,c++
|
# End of https://www.toptal.com/developers/gitignore/api/intellij+all,visualstudio,visualstudiocode,cmake,c,c++
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
# Visione Backend Agent Guide
|
|
||||||
|
|
||||||
## Project Context
|
|
||||||
- This repository is the backend for **Visione**, an urban issue reporting webapp.
|
|
||||||
- The backend is C++20 with Drogon. Keep code, symbols, API fields, database names and comments in English.
|
|
||||||
- User-facing product labels may be Italian when they represent UI/business vocabulary.
|
|
||||||
- Docker is for local external services only: MySQL, MailHog and future Redis-like dependencies.
|
|
||||||
|
|
||||||
## Authoritative References
|
|
||||||
- Local Drogon source: `/home/gabdevele/Projects/snitchitaly/backend/drogon`
|
|
||||||
- Official Drogon docs: `https://drogonframework.github.io/drogon-docs/`
|
|
||||||
- Project docs start from `docs/LLM_CONTEXT.md`.
|
|
||||||
|
|
||||||
## Architecture Rules
|
|
||||||
- Controllers parse HTTP input and shape HTTP responses only.
|
|
||||||
- Services own business rules.
|
|
||||||
- Repositories/database helpers own SQL access.
|
|
||||||
- Prefer small, readable C++ over clever abstractions.
|
|
||||||
- Use parameterized SQL; do not concatenate user input into SQL.
|
|
||||||
- Keep local development simple. Do not introduce production infrastructure unless requested.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
- Configure: `cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug`
|
|
||||||
- Build: `cmake --build build`
|
|
||||||
- Test: `ctest --test-dir build --output-on-failure`
|
|
||||||
- OpenAPI: `cmake --build build --target openapi`
|
|
||||||
- Lab frontend: `cd scripts/lab && uv run streamlit run app.py`
|
|
||||||
- Local services: `docker compose up -d`
|
|
||||||
- App: `./build/visione-backend`
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
- Update docs when behavior, schema, setup or API contracts change.
|
|
||||||
- After each completed and verified feature, create a short commit.
|
|
||||||
- Keep commit messages concise, for example `Add MailHog config` or `Add report API`.
|
|
||||||
- Do not spend excessive time on exhaustive tests; cover high-risk behavior and regressions.
|
|
||||||
@@ -8,6 +8,8 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
|||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||||
|
find_package(OpenSSL REQUIRED)
|
||||||
|
find_package(CURL REQUIRED)
|
||||||
|
|
||||||
option(USE_SYSTEM_DROGON "Use an installed Drogon package instead of CPM" OFF)
|
option(USE_SYSTEM_DROGON "Use an installed Drogon package instead of CPM" OFF)
|
||||||
|
|
||||||
@@ -71,6 +73,8 @@ target_include_directories(${PROJECT_NAME}
|
|||||||
target_link_libraries(${PROJECT_NAME}
|
target_link_libraries(${PROJECT_NAME}
|
||||||
PRIVATE
|
PRIVATE
|
||||||
Drogon::Drogon
|
Drogon::Drogon
|
||||||
|
CURL::libcurl
|
||||||
|
OpenSSL::Crypto
|
||||||
)
|
)
|
||||||
|
|
||||||
file(GLOB CONTROLLER_HEADERS CONFIGURE_DEPENDS
|
file(GLOB CONTROLLER_HEADERS CONFIGURE_DEPENDS
|
||||||
|
|||||||
+11
-8
@@ -1,10 +1,12 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
# --- Builder Stage ---
|
# --- Builder Stage ---
|
||||||
FROM ubuntu:22.04 AS builder
|
FROM ubuntu:22.04 AS builder
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
build-essential cmake ninja-build git pkg-config \
|
build-essential ccache cmake ninja-build git pkg-config \
|
||||||
libssl-dev zlib1g-dev \
|
libcurl4-openssl-dev libssl-dev zlib1g-dev \
|
||||||
libmariadb-dev libmariadb-dev-compat \
|
libmariadb-dev libmariadb-dev-compat \
|
||||||
libsqlite3-dev python3 libjsoncpp-dev uuid-dev ca-certificates \
|
libsqlite3-dev python3 libjsoncpp-dev uuid-dev ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -14,26 +16,27 @@ RUN rm -rf /usr/include/mysql && ln -s /usr/include/mariadb /usr/include/mysql
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
RUN cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
RUN --mount=type=cache,target=/app/.cache/cpm \
|
||||||
&& cmake --build build -- -j$(nproc)
|
--mount=type=cache,target=/root/.cache/ccache \
|
||||||
|
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||||
|
&& cmake --build build
|
||||||
|
|
||||||
# --- Runtime Stage ---
|
# --- Runtime Stage ---
|
||||||
FROM ubuntu:22.04 AS runtime
|
FROM ubuntu:22.04 AS runtime
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
libssl3 ca-certificates libstdc++6 libmariadb3 libjsoncpp25 libuuid1 \
|
libcurl4 libssl3 ca-certificates libstdc++6 libmariadb3 libjsoncpp25 libuuid1 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/build/visione-backend /app/visione-backend
|
COPY --from=builder /app/build/visione-backend /app/visione-backend
|
||||||
COPY --from=builder /app/build/openapi /app/build/openapi
|
COPY --from=builder /app/build/openapi /app/build/openapi
|
||||||
|
COPY --from=builder /app/config.json /app/config.json
|
||||||
COPY --from=builder /app/migrations /app/migrations
|
COPY --from=builder /app/migrations /app/migrations
|
||||||
|
|
||||||
EXPOSE 5555
|
EXPOSE 5555
|
||||||
|
|
||||||
RUN chown -R 1000:1000 /app
|
|
||||||
USER 1000:1000
|
|
||||||
|
|
||||||
ENTRYPOINT ["/app/visione-backend"]
|
ENTRYPOINT ["/app/visione-backend"]
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Visione Backend
|
# Visione Backend
|
||||||
|
|
||||||
C++20 API backend built with Drogon for the Visione urban reporting project.
|
C++20 API backend built with Drogon for the Visione urban reporting project.
|
||||||
Docker is used only for local external services: MySQL and MailHog.
|
Docker is used only for local external services: MySQL and Mailpit.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ Install native build dependencies first. On Ubuntu:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install build-essential cmake ninja-build git pkg-config \
|
sudo apt install build-essential cmake ninja-build git pkg-config \
|
||||||
libjsoncpp-dev uuid-dev zlib1g-dev libssl-dev \
|
libcurl4-openssl-dev libjsoncpp-dev uuid-dev zlib1g-dev libssl-dev \
|
||||||
libmariadb-dev libmariadb-dev-compat
|
libmariadb-dev libmariadb-dev-compat
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ cmake --build build
|
|||||||
```
|
```
|
||||||
|
|
||||||
The API listens on `http://localhost:5555`.
|
The API listens on `http://localhost:5555`.
|
||||||
MailHog listens on `http://localhost:8025`.
|
Mailpit listens on `http://localhost:8025`.
|
||||||
Local CORS allows `http://localhost:5173` by default for the future Vue frontend.
|
Local CORS allows `http://localhost:5173` by default for the future Vue frontend.
|
||||||
Multipart uploads currently accept report images and videos up to the app body
|
Multipart uploads currently accept report images and videos up to the app body
|
||||||
limit configured in `config.json`.
|
limit configured in `config.json`.
|
||||||
@@ -36,9 +36,8 @@ has been built.
|
|||||||
|
|
||||||
Local seeded admin:
|
Local seeded admin:
|
||||||
|
|
||||||
- email: `admin@visione.local`
|
- email: `admin@visionebari.it`
|
||||||
- password: `ChangeMe123!`
|
- access uses OTP email delivery and needs mail provider config.
|
||||||
- the account has `must_change_password = true` and is for local development only.
|
|
||||||
|
|
||||||
If you already had a MySQL volume from the old scaffold, create the local
|
If you already had a MySQL volume from the old scaffold, create the local
|
||||||
database/user once:
|
database/user once:
|
||||||
@@ -55,26 +54,15 @@ docker compose exec -T mysql mysql -uroot -proot -e \
|
|||||||
```bash
|
```bash
|
||||||
curl http://localhost:5555/api/v1/health
|
curl http://localhost:5555/api/v1/health
|
||||||
curl http://localhost:5555/api/v1/ready
|
curl http://localhost:5555/api/v1/ready
|
||||||
curl -X POST http://localhost:5555/api/v1/auth/login/password \
|
curl -X POST http://localhost:5555/api/v1/auth/otp/request \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"email":"admin@visione.local","password":"ChangeMe123!"}'
|
-d '{"email":"admin@visionebari.it"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `-DUSE_SYSTEM_DROGON=ON` only if Drogon is already installed and
|
Use `-DUSE_SYSTEM_DROGON=ON` only if Drogon is already installed and
|
||||||
discoverable by CMake. The default build uses CPM.cmake and downloads Drogon
|
discoverable by CMake. The default build uses CPM.cmake and downloads Drogon
|
||||||
`v1.9.13`.
|
`v1.9.13`.
|
||||||
|
|
||||||
## Project Docs
|
|
||||||
|
|
||||||
- `AGENTS.md`: durable instructions for coding agents.
|
|
||||||
- `docs/LLM_CONTEXT.md`: short project context for future LLM sessions.
|
|
||||||
- `docs/MILESTONE_01.md`: what this milestone implemented and what comes next.
|
|
||||||
- `docs/MILESTONE_02.md`: workflow and authorization milestone.
|
|
||||||
- `docs/MILESTONE_03.md`: media upload, OpenAPI generator and API lab milestone.
|
|
||||||
- `docs/MILESTONE_04.md`: backend completion milestone.
|
|
||||||
- `docs/API_SPEC.md`: API surface.
|
|
||||||
- `docs/DATABASE_SCHEMA.md`: schema overview.
|
|
||||||
- `docs/ARCHITECTURE.md`: backend structure.
|
|
||||||
|
|
||||||
## OpenAPI
|
## OpenAPI
|
||||||
|
|
||||||
@@ -94,6 +82,32 @@ python3 -m unittest scripts/test_generate_openapi.py
|
|||||||
The app serves the generated JSON at `/api/v1/openapi.json` and Swagger UI at
|
The app serves the generated JSON at `/api/v1/openapi.json` and Swagger UI at
|
||||||
`/api/v1/docs`.
|
`/api/v1/docs`.
|
||||||
|
|
||||||
|
## Email Delivery
|
||||||
|
|
||||||
|
OTP delivery uses SMTP through `libcurl`. Local dev defaults to Mailpit.
|
||||||
|
Set these environment variables before starting backend for local Mailpit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MAIL_PROVIDER=smtp
|
||||||
|
export SMTP_HOST=127.0.0.1
|
||||||
|
export SMTP_PORT=1025
|
||||||
|
export MAIL_FROM=[email protected]
|
||||||
|
```
|
||||||
|
|
||||||
|
For Resend in production, use SMTP credentials instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MAIL_PROVIDER=resend
|
||||||
|
export SMTP_HOST=smtp.resend.com
|
||||||
|
export SMTP_PORT=465
|
||||||
|
export SMTP_USERNAME=resend
|
||||||
|
export SMTP_PASSWORD=re_xxx
|
||||||
|
export SMTP_USE_TLS=true
|
||||||
|
export SMTP_VERIFY_PEER=true
|
||||||
|
export SMTP_VERIFY_HOST=true
|
||||||
|
export MAIL_FROM=[email protected]
|
||||||
|
```
|
||||||
|
|
||||||
## Visione Lab
|
## Visione Lab
|
||||||
|
|
||||||
`scripts/lab/` is a small Streamlit frontend used only to test the backend
|
`scripts/lab/` is a small Streamlit frontend used only to test the backend
|
||||||
@@ -131,6 +145,33 @@ ctest --test-dir build --output-on-failure
|
|||||||
The `RequestLogger` plugin logs every response as method, path, status and
|
The `RequestLogger` plugin logs every response as method, path, status and
|
||||||
client address.
|
client address.
|
||||||
|
|
||||||
|
## Dokploy
|
||||||
|
|
||||||
|
Use `docker-compose.prod.yml` as the Dokploy compose path for a self-contained
|
||||||
|
backend deployment with MySQL and a local SMTP catcher. Configure the public
|
||||||
|
domain in Dokploy against service `backend` and container port `5555`.
|
||||||
|
|
||||||
|
The runtime image includes `config.json`; override environment-specific values
|
||||||
|
with Dokploy environment variables. The Mailpit service uses the
|
||||||
|
multi-architecture Mailpit image to avoid `exec format error` failures on ARM
|
||||||
|
servers.
|
||||||
|
|
||||||
|
Set `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `MYSQL_ROOT_PASSWORD`,
|
||||||
|
`SMTP_PASSWORD`, `MAIL_FROM` and
|
||||||
|
`CORS_ALLOWED_ORIGIN` in Dokploy environment variables. Use the exact browser
|
||||||
|
origin without quotes or a trailing slash, for example
|
||||||
|
`https://visione.studentindipendenti.it`.
|
||||||
|
|
||||||
|
For CIE auth deploys, also override values that vary by environment:
|
||||||
|
- `CIE_AUTH_SERVICE_BASE_URL`
|
||||||
|
- `CIE_AUTH_START_PATH`
|
||||||
|
- `CIE_AUTH_SHARED_SECRET`
|
||||||
|
- `CIE_AUTH_STATE_TTL_MINUTES`
|
||||||
|
- `CIE_AUTH_DEFAULT_FRONTEND_REDIRECT_URI`
|
||||||
|
|
||||||
|
Docker builds use BuildKit cache mounts for CPM dependencies and `ccache`, so
|
||||||
|
repeat Dokploy builds can reuse Drogon and C++ compilation cache.
|
||||||
|
|
||||||
## Current Endpoints
|
## Current Endpoints
|
||||||
|
|
||||||
- `GET /api/v1/health`: process health check, no external dependency.
|
- `GET /api/v1/health`: process health check, no external dependency.
|
||||||
|
|||||||
+6
-7
@@ -45,7 +45,7 @@ std::optional<UserContext> authenticate(const drogon::HttpRequestPtr &request)
|
|||||||
|
|
||||||
auto db = drogon::app().getDbClient();
|
auto db = drogon::app().getDbClient();
|
||||||
const auto userResult = db->execSqlSync(
|
const auto userResult = db->execSqlSync(
|
||||||
"SELECT u.id, u.email, u.must_change_password "
|
"SELECT u.id, u.email "
|
||||||
"FROM api_tokens t "
|
"FROM api_tokens t "
|
||||||
"JOIN users u ON u.id = t.user_id "
|
"JOIN users u ON u.id = t.user_id "
|
||||||
"WHERE t.token_hash = ? AND t.revoked_at IS NULL "
|
"WHERE t.token_hash = ? AND t.revoked_at IS NULL "
|
||||||
@@ -93,7 +93,6 @@ std::optional<UserContext> authenticate(const drogon::HttpRequestPtr &request)
|
|||||||
context.roles = std::move(roles);
|
context.roles = std::move(roles);
|
||||||
context.permissions = std::move(permissions);
|
context.permissions = std::move(permissions);
|
||||||
context.role = primaryRole(context.roles);
|
context.role = primaryRole(context.roles);
|
||||||
context.mustChangePassword = userResult[0]["must_change_password"].as<bool>();
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,13 +112,13 @@ bool canManageReports(const UserContext &user)
|
|||||||
return isAdmin(user) || hasPermission(user, "reports.manage");
|
return isAdmin(user) || hasPermission(user, "reports.manage");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool canOperateReports(const UserContext &user)
|
||||||
|
{
|
||||||
|
return !isAdmin(user) && canManageReports(user);
|
||||||
|
}
|
||||||
|
|
||||||
bool isAdmin(const UserContext &user)
|
bool isAdmin(const UserContext &user)
|
||||||
{
|
{
|
||||||
return hasRole(user, "admin") || hasPermission(user, "admin.manage");
|
return hasRole(user, "admin") || hasPermission(user, "admin.manage");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool mustChangePassword(const UserContext &user)
|
|
||||||
{
|
|
||||||
return user.mustChangePassword;
|
|
||||||
}
|
|
||||||
} // namespace visione::auth
|
} // namespace visione::auth
|
||||||
|
|||||||
+1
-2
@@ -15,7 +15,6 @@ struct UserContext
|
|||||||
std::string role;
|
std::string role;
|
||||||
std::vector<std::string> roles;
|
std::vector<std::string> roles;
|
||||||
std::vector<std::string> permissions;
|
std::vector<std::string> permissions;
|
||||||
bool mustChangePassword{false};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
std::optional<std::string> bearerToken(const drogon::HttpRequestPtr &request);
|
std::optional<std::string> bearerToken(const drogon::HttpRequestPtr &request);
|
||||||
@@ -23,6 +22,6 @@ std::optional<UserContext> authenticate(const drogon::HttpRequestPtr &request);
|
|||||||
bool hasRole(const UserContext &user, const std::string &role);
|
bool hasRole(const UserContext &user, const std::string &role);
|
||||||
bool hasPermission(const UserContext &user, const std::string &permission);
|
bool hasPermission(const UserContext &user, const std::string &permission);
|
||||||
bool canManageReports(const UserContext &user);
|
bool canManageReports(const UserContext &user);
|
||||||
|
bool canOperateReports(const UserContext &user);
|
||||||
bool isAdmin(const UserContext &user);
|
bool isAdmin(const UserContext &user);
|
||||||
bool mustChangePassword(const UserContext &user);
|
|
||||||
} // namespace visione::auth
|
} // namespace visione::auth
|
||||||
|
|||||||
+9
-67
@@ -6,91 +6,33 @@ namespace visione::cors
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
std::vector<std::string> allowedOrigins()
|
std::string allowedOrigin()
|
||||||
{
|
{
|
||||||
auto cfg = drogon::app().getCustomConfig();
|
return drogon::app()
|
||||||
std::vector<std::string> origins;
|
.getCustomConfig()["cors"]
|
||||||
|
.get("allowed_origin", "http://localhost:5173")
|
||||||
if (cfg.isMember("cors") && cfg["cors"].isObject())
|
.asString();
|
||||||
{
|
|
||||||
auto &c = cfg["cors"];
|
|
||||||
if (c.isMember("allowed_origin"))
|
|
||||||
{
|
|
||||||
auto &v = c["allowed_origin"];
|
|
||||||
if (v.isArray())
|
|
||||||
{
|
|
||||||
for (const auto &el : v)
|
|
||||||
{
|
|
||||||
if (el.isString())
|
|
||||||
origins.emplace_back(el.asString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (v.isString())
|
|
||||||
{
|
|
||||||
origins.emplace_back(v.asString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (origins.empty())
|
|
||||||
origins.emplace_back("http://localhost:5173");
|
|
||||||
|
|
||||||
return origins;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isAllowed(const drogon::HttpRequestPtr &request)
|
bool isAllowed(const drogon::HttpRequestPtr &request)
|
||||||
{
|
{
|
||||||
auto origin = request->getHeader("Origin");
|
return request->getHeader("Origin") == allowedOrigin();
|
||||||
if (origin.empty())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
auto origins = allowedOrigins();
|
|
||||||
for (const auto &o : origins)
|
|
||||||
{
|
|
||||||
if (o == "*" || o == origin)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void addHeaders(const drogon::HttpRequestPtr &request,
|
void addHeaders(const drogon::HttpRequestPtr &request,
|
||||||
const drogon::HttpResponsePtr &response)
|
const drogon::HttpResponsePtr &response)
|
||||||
{
|
{
|
||||||
auto origin = request->getHeader("Origin");
|
if (!isAllowed(request))
|
||||||
if (origin.empty())
|
{
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto origins = allowedOrigins();
|
|
||||||
bool allowAll = false;
|
|
||||||
bool matched = false;
|
|
||||||
for (const auto &o : origins)
|
|
||||||
{
|
|
||||||
if (o == "*")
|
|
||||||
{
|
|
||||||
allowAll = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (o == origin)
|
|
||||||
{
|
|
||||||
matched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allowAll && !matched)
|
response->addHeader("Access-Control-Allow-Origin", allowedOrigin());
|
||||||
return;
|
|
||||||
|
|
||||||
if (allowAll)
|
|
||||||
response->addHeader("Access-Control-Allow-Origin", "*");
|
|
||||||
else
|
|
||||||
response->addHeader("Access-Control-Allow-Origin", origin);
|
|
||||||
|
|
||||||
response->addHeader("Access-Control-Allow-Credentials", "true");
|
response->addHeader("Access-Control-Allow-Credentials", "true");
|
||||||
response->addHeader("Access-Control-Allow-Methods",
|
response->addHeader("Access-Control-Allow-Methods",
|
||||||
"GET,POST,PATCH,DELETE,OPTIONS");
|
"GET,POST,PATCH,DELETE,OPTIONS");
|
||||||
response->addHeader("Access-Control-Allow-Headers",
|
response->addHeader("Access-Control-Allow-Headers",
|
||||||
"Authorization,Content-Type");
|
"Authorization,Content-Type");
|
||||||
response->addHeader("Vary", "Origin");
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
+25
-58
@@ -1,10 +1,9 @@
|
|||||||
#include "common/Crypto.h"
|
#include "common/Crypto.h"
|
||||||
|
|
||||||
#include <drogon/utils/Utilities.h>
|
#include <drogon/utils/Utilities.h>
|
||||||
|
#include <openssl/crypto.h>
|
||||||
|
#include <openssl/hmac.h>
|
||||||
|
|
||||||
#include <openssl/evp.h>
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -14,9 +13,6 @@ namespace visione::crypto
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr int passwordIterations = 210000;
|
|
||||||
constexpr size_t passwordHashBytes = 32;
|
|
||||||
|
|
||||||
std::string toHex(const unsigned char *data, size_t size)
|
std::string toHex(const unsigned char *data, size_t size)
|
||||||
{
|
{
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
@@ -27,22 +23,6 @@ std::string toHex(const unsigned char *data, size_t size)
|
|||||||
}
|
}
|
||||||
return stream.str();
|
return stream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string pbkdf2(const std::string &password,
|
|
||||||
const std::string &salt,
|
|
||||||
int iterations)
|
|
||||||
{
|
|
||||||
std::array<unsigned char, passwordHashBytes> output{};
|
|
||||||
PKCS5_PBKDF2_HMAC(password.c_str(),
|
|
||||||
static_cast<int>(password.size()),
|
|
||||||
reinterpret_cast<const unsigned char *>(salt.data()),
|
|
||||||
static_cast<int>(salt.size()),
|
|
||||||
iterations,
|
|
||||||
EVP_sha256(),
|
|
||||||
static_cast<int>(output.size()),
|
|
||||||
output.data());
|
|
||||||
return toHex(output.data(), output.size());
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::string sha256(const std::string &value)
|
std::string sha256(const std::string &value)
|
||||||
@@ -50,6 +30,29 @@ std::string sha256(const std::string &value)
|
|||||||
return drogon::utils::getSha256(value);
|
return drogon::utils::getSha256(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string hmacSha256Hex(const std::string &key, const std::string &value)
|
||||||
|
{
|
||||||
|
unsigned char digest[EVP_MAX_MD_SIZE];
|
||||||
|
unsigned int digestSize = 0;
|
||||||
|
HMAC(EVP_sha256(),
|
||||||
|
key.data(),
|
||||||
|
static_cast<int>(key.size()),
|
||||||
|
reinterpret_cast<const unsigned char *>(value.data()),
|
||||||
|
value.size(),
|
||||||
|
digest,
|
||||||
|
&digestSize);
|
||||||
|
return toHex(digest, digestSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool constantTimeEquals(const std::string &left, const std::string &right)
|
||||||
|
{
|
||||||
|
if (left.size() != right.size())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return CRYPTO_memcmp(left.data(), right.data(), left.size()) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
std::string randomNumericCode(int digits)
|
std::string randomNumericCode(int digits)
|
||||||
{
|
{
|
||||||
std::random_device device;
|
std::random_device device;
|
||||||
@@ -70,42 +73,6 @@ std::string randomToken()
|
|||||||
return drogon::utils::getUuid() + drogon::utils::getUuid();
|
return drogon::utils::getUuid() + drogon::utils::getUuid();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string hashPassword(const std::string &password)
|
|
||||||
{
|
|
||||||
const auto salt = drogon::utils::getUuid() + drogon::utils::getUuid();
|
|
||||||
return "pbkdf2_sha256$" + std::to_string(passwordIterations) + "$" + salt +
|
|
||||||
"$" + pbkdf2(password, salt, passwordIterations);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool verifyPassword(const std::string &password, const std::string &hash)
|
|
||||||
{
|
|
||||||
constexpr std::string_view pbkdf2Prefix = "pbkdf2_sha256$";
|
|
||||||
|
|
||||||
if (!hash.starts_with(pbkdf2Prefix))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto iterationsStart = pbkdf2Prefix.size();
|
|
||||||
const auto saltStart = hash.find('$', iterationsStart);
|
|
||||||
if (saltStart == std::string::npos)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const auto digestStart = hash.find('$', saltStart + 1);
|
|
||||||
if (digestStart == std::string::npos)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto iterations =
|
|
||||||
std::stoi(hash.substr(iterationsStart, saltStart - iterationsStart));
|
|
||||||
const auto salt = hash.substr(saltStart + 1, digestStart - saltStart - 1);
|
|
||||||
const auto expected = "pbkdf2_sha256$" + std::to_string(iterations) + "$" +
|
|
||||||
salt + "$" + pbkdf2(password, salt, iterations);
|
|
||||||
return expected == hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string hashSecret(const std::string &secret)
|
std::string hashSecret(const std::string &secret)
|
||||||
{
|
{
|
||||||
return sha256("visione:" + secret);
|
return sha256("visione:" + secret);
|
||||||
|
|||||||
+2
-3
@@ -5,10 +5,9 @@
|
|||||||
namespace visione::crypto
|
namespace visione::crypto
|
||||||
{
|
{
|
||||||
std::string sha256(const std::string &value);
|
std::string sha256(const std::string &value);
|
||||||
|
std::string hmacSha256Hex(const std::string &key, const std::string &value);
|
||||||
|
bool constantTimeEquals(const std::string &left, const std::string &right);
|
||||||
std::string randomNumericCode(int digits);
|
std::string randomNumericCode(int digits);
|
||||||
std::string randomToken();
|
std::string randomToken();
|
||||||
std::string hashPassword(const std::string &password);
|
|
||||||
bool verifyPassword(const std::string &password, const std::string &hash);
|
|
||||||
std::string hashSecret(const std::string &secret);
|
std::string hashSecret(const std::string &secret);
|
||||||
} // namespace visione::crypto
|
} // namespace visione::crypto
|
||||||
|
|
||||||
|
|||||||
+189
-69
@@ -1,119 +1,239 @@
|
|||||||
#include "common/Email.h"
|
#include "common/Email.h"
|
||||||
|
|
||||||
|
#include <curl/curl.h>
|
||||||
#include <drogon/drogon.h>
|
#include <drogon/drogon.h>
|
||||||
|
|
||||||
#include <array>
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <memory>
|
||||||
|
#include <sstream>
|
||||||
#include <netdb.h>
|
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
namespace visione::email
|
namespace visione::email
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
const Json::Value &mailConfig()
|
||||||
|
{
|
||||||
|
return drogon::app().getCustomConfig()["mail"];
|
||||||
|
}
|
||||||
|
|
||||||
std::string configString(const std::string &key, const std::string &fallback)
|
std::string configString(const std::string &key, const std::string &fallback)
|
||||||
{
|
{
|
||||||
const auto &mail = drogon::app().getCustomConfig()["mail"];
|
const auto &mail = mailConfig();
|
||||||
return mail.get(key, fallback).asString();
|
return mail.get(key, fallback).asString();
|
||||||
}
|
}
|
||||||
|
|
||||||
int configInt(const std::string &key, int fallback)
|
int configInt(const std::string &key, int fallback)
|
||||||
{
|
{
|
||||||
const auto &mail = drogon::app().getCustomConfig()["mail"];
|
const auto &mail = mailConfig();
|
||||||
return mail.get(key, fallback).asInt();
|
return mail.get(key, fallback).asInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool readResponse(int socket)
|
bool configBool(const std::string &key, bool fallback)
|
||||||
{
|
{
|
||||||
std::array<char, 512> buffer{};
|
const auto &mail = mailConfig();
|
||||||
const auto count = recv(socket, buffer.data(), buffer.size() - 1, 0);
|
return mail.get(key, fallback).asBool();
|
||||||
return count > 0 && buffer[0] >= '2' && buffer[0] <= '3';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sendLine(int socket, const std::string &line)
|
struct SmtpDefaults
|
||||||
{
|
{
|
||||||
const auto payload = line + "\r\n";
|
std::string host;
|
||||||
return send(socket, payload.data(), payload.size(), 0) ==
|
int port;
|
||||||
static_cast<ssize_t>(payload.size());
|
std::string username;
|
||||||
|
bool useTls;
|
||||||
|
bool useStartTls;
|
||||||
|
bool verifyPeer;
|
||||||
|
bool verifyHost;
|
||||||
|
};
|
||||||
|
|
||||||
|
SmtpDefaults smtpDefaults(const std::string &provider)
|
||||||
|
{
|
||||||
|
if (provider == "resend")
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
.host = "smtp.resend.com",
|
||||||
|
.port = 465,
|
||||||
|
.username = "resend",
|
||||||
|
.useTls = true,
|
||||||
|
.useStartTls = false,
|
||||||
|
.verifyPeer = true,
|
||||||
|
.verifyHost = true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
.host = "127.0.0.1",
|
||||||
|
.port = 1025,
|
||||||
|
.username = "",
|
||||||
|
.useTls = false,
|
||||||
|
.useStartTls = false,
|
||||||
|
.verifyPeer = false,
|
||||||
|
.verifyHost = false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sendCommand(int socket, const std::string &line)
|
std::string smtpUrl(const std::string &host, int port, bool useTls)
|
||||||
{
|
{
|
||||||
return sendLine(socket, line) && readResponse(socket);
|
return (useTls ? "smtps://" : "smtp://") + host + ":" + std::to_string(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UploadContext
|
||||||
|
{
|
||||||
|
const std::string *payload;
|
||||||
|
size_t offset{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
size_t readPayload(char *buffer, size_t size, size_t count, void *userData)
|
||||||
|
{
|
||||||
|
auto *context = static_cast<UploadContext *>(userData);
|
||||||
|
const auto capacity = size * count;
|
||||||
|
const auto remaining = context->payload->size() - context->offset;
|
||||||
|
const auto bytesToCopy = std::min(capacity, remaining);
|
||||||
|
|
||||||
|
if (bytesToCopy == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(buffer,
|
||||||
|
context->payload->data() + static_cast<std::ptrdiff_t>(context->offset),
|
||||||
|
bytesToCopy);
|
||||||
|
context->offset += bytesToCopy;
|
||||||
|
return bytesToCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ensureCurlInitialized()
|
||||||
|
{
|
||||||
|
static const bool initialized = []() {
|
||||||
|
const auto result = curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||||
|
if (result != CURLE_OK)
|
||||||
|
{
|
||||||
|
LOG_ERROR << "curl_global_init failed: " << curl_easy_strerror(result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}();
|
||||||
|
return initialized;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
std::string EmailService::deliveryName() const
|
||||||
|
{
|
||||||
|
return configString("provider", "smtp");
|
||||||
|
}
|
||||||
|
|
||||||
bool EmailService::sendOtp(const std::string &to, const std::string &code) const
|
bool EmailService::sendOtp(const std::string &to, const std::string &code) const
|
||||||
{
|
{
|
||||||
const auto host = configString("smtp_host", "127.0.0.1");
|
const auto provider = deliveryName();
|
||||||
const auto port = std::to_string(configInt("smtp_port", 1025));
|
const auto defaults = smtpDefaults(provider);
|
||||||
const auto from = configString("from", "[email protected]");
|
const auto host = configString("host", defaults.host);
|
||||||
|
const auto port = configInt("port", defaults.port);
|
||||||
|
const auto username = configString("username", defaults.username);
|
||||||
|
const auto password = configString("password", "");
|
||||||
|
const auto from = configString("from", "");
|
||||||
|
const auto useTls = configBool("use_tls", defaults.useTls);
|
||||||
|
const auto useStartTls = configBool("use_starttls", defaults.useStartTls);
|
||||||
|
const auto verifyPeer = configBool("verify_peer", defaults.verifyPeer);
|
||||||
|
const auto verifyHost = configBool("verify_host", defaults.verifyHost);
|
||||||
|
|
||||||
addrinfo hints{};
|
if (host.empty() || from.empty())
|
||||||
hints.ai_family = AF_UNSPEC;
|
{
|
||||||
hints.ai_socktype = SOCK_STREAM;
|
LOG_ERROR << "Email delivery configuration missing: host or from";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (provider == "resend" && password.empty())
|
||||||
|
{
|
||||||
|
LOG_ERROR << "Email delivery configuration missing: SMTP password for Resend";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
addrinfo *addresses = nullptr;
|
if (!ensureCurlInitialized())
|
||||||
if (getaddrinfo(host.c_str(), port.c_str(), &hints, &addresses) != 0)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int smtp = -1;
|
std::ostringstream payload;
|
||||||
for (auto *address = addresses; address != nullptr; address = address->ai_next)
|
payload << "To: <" << to << ">\r\n";
|
||||||
{
|
payload << "From: Visione <" << from << ">\r\n";
|
||||||
smtp = socket(address->ai_family, address->ai_socktype, address->ai_protocol);
|
payload << "Subject: Codice OTP Visione\r\n";
|
||||||
if (smtp < 0)
|
payload << "MIME-Version: 1.0\r\n";
|
||||||
{
|
payload << "Content-Type: text/plain; charset=utf-8\r\n";
|
||||||
continue;
|
payload << "\r\n";
|
||||||
}
|
payload << "Il tuo codice OTP per Visione e': " << code << "\r\n";
|
||||||
if (connect(smtp, address->ai_addr, address->ai_addrlen) == 0)
|
payload << "Scade tra 10 minuti.\r\n";
|
||||||
{
|
const auto payloadString = payload.str();
|
||||||
break;
|
const auto url = smtpUrl(host, port, useTls);
|
||||||
}
|
const auto mailFrom = "<" + from + ">";
|
||||||
close(smtp);
|
const auto recipientAddress = "<" + to + ">";
|
||||||
smtp = -1;
|
|
||||||
}
|
|
||||||
freeaddrinfo(addresses);
|
|
||||||
|
|
||||||
if (smtp < 0)
|
using CurlHandle = std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>;
|
||||||
|
using CurlList = std::unique_ptr<curl_slist, decltype(&curl_slist_free_all)>;
|
||||||
|
|
||||||
|
CurlHandle curl(curl_easy_init(), &curl_easy_cleanup);
|
||||||
|
if (!curl)
|
||||||
{
|
{
|
||||||
|
LOG_ERROR << "Could not initialize libcurl SMTP handle";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto closeSocket = [&smtp]() {
|
CurlList recipients(nullptr, &curl_slist_free_all);
|
||||||
if (smtp >= 0)
|
recipients.reset(curl_slist_append(nullptr, recipientAddress.c_str()));
|
||||||
|
if (!recipients)
|
||||||
{
|
{
|
||||||
close(smtp);
|
LOG_ERROR << "Could not build SMTP recipient list for " << to;
|
||||||
smtp = -1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!readResponse(smtp) || !sendCommand(smtp, "HELO visione.local") ||
|
|
||||||
!sendCommand(smtp, "MAIL FROM:<" + from + ">") ||
|
|
||||||
!sendCommand(smtp, "RCPT TO:<" + to + ">") ||
|
|
||||||
!sendCommand(smtp, "DATA"))
|
|
||||||
{
|
|
||||||
closeSocket();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string body =
|
UploadContext uploadContext{.payload = &payloadString};
|
||||||
"From: Visione <" + from + ">\r\n"
|
char errorBuffer[CURL_ERROR_SIZE] = {0};
|
||||||
"To: " + to + "\r\n"
|
|
||||||
"Subject: Codice OTP Visione\r\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\r\n"
|
|
||||||
"\r\n"
|
|
||||||
"Il tuo codice OTP per Visione e': " + code + "\r\n"
|
|
||||||
"Scade tra 10 minuti.\r\n"
|
|
||||||
".";
|
|
||||||
|
|
||||||
const auto ok = sendCommand(smtp, body) && sendCommand(smtp, "QUIT");
|
curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str());
|
||||||
closeSocket();
|
curl_easy_setopt(curl.get(), CURLOPT_MAIL_FROM, mailFrom.c_str());
|
||||||
return ok;
|
curl_easy_setopt(curl.get(), CURLOPT_MAIL_RCPT, recipients.get());
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_READFUNCTION, readPayload);
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_READDATA, &uploadContext);
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_UPLOAD, 1L);
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, "visione-backend/1.0");
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_CONNECTTIMEOUT, 10L);
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 20L);
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_ERRORBUFFER, errorBuffer);
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, verifyPeer ? 1L : 0L);
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, verifyHost ? 2L : 0L);
|
||||||
|
|
||||||
|
if (!username.empty())
|
||||||
|
{
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_USERNAME, username.c_str());
|
||||||
|
}
|
||||||
|
if (!password.empty())
|
||||||
|
{
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_PASSWORD, password.c_str());
|
||||||
|
}
|
||||||
|
if (useStartTls)
|
||||||
|
{
|
||||||
|
curl_easy_setopt(curl.get(), CURLOPT_USE_SSL, CURLUSESSL_ALL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto result = curl_easy_perform(curl.get());
|
||||||
|
if (result != CURLE_OK)
|
||||||
|
{
|
||||||
|
LOG_ERROR << "OTP email delivery failed via SMTP for "
|
||||||
|
<< to
|
||||||
|
<< ", provider="
|
||||||
|
<< provider
|
||||||
|
<< ", host="
|
||||||
|
<< host
|
||||||
|
<< ", port="
|
||||||
|
<< port
|
||||||
|
<< ", error="
|
||||||
|
<< (errorBuffer[0] != '\0' ? errorBuffer : curl_easy_strerror(result));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO << "OTP email sent via SMTP for " << to
|
||||||
|
<< ", provider=" << provider
|
||||||
|
<< ", host=" << host
|
||||||
|
<< ", port=" << port;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
} // namespace visione::email
|
} // namespace visione::email
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,6 +8,6 @@ class EmailService
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
bool sendOtp(const std::string &to, const std::string &code) const;
|
bool sendOtp(const std::string &to, const std::string &code) const;
|
||||||
|
std::string deliveryName() const;
|
||||||
};
|
};
|
||||||
} // namespace visione::email
|
} // namespace visione::email
|
||||||
|
|
||||||
|
|||||||
+114
-10
@@ -1,5 +1,6 @@
|
|||||||
#include "common/GeoJson.h"
|
#include "common/GeoJson.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace visione::geojson
|
namespace visione::geojson
|
||||||
@@ -11,17 +12,12 @@ struct Point
|
|||||||
double x;
|
double x;
|
||||||
double y;
|
double y;
|
||||||
};
|
};
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool pointInPolygon(double latitude, double longitude, const Json::Value &geojson)
|
bool pointInRing(double latitude,
|
||||||
|
double longitude,
|
||||||
|
const Json::Value &ring,
|
||||||
|
bool *onBoundary = nullptr)
|
||||||
{
|
{
|
||||||
if (!geojson.isObject() || geojson["type"].asString() != "Polygon" ||
|
|
||||||
!geojson["coordinates"].isArray() || geojson["coordinates"].empty())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto &ring = geojson["coordinates"][0];
|
|
||||||
if (!ring.isArray() || ring.size() < 4)
|
if (!ring.isArray() || ring.size() < 4)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -41,10 +37,27 @@ bool pointInPolygon(double latitude, double longitude, const Json::Value &geojso
|
|||||||
const auto x = longitude;
|
const auto x = longitude;
|
||||||
const auto y = latitude;
|
const auto y = latitude;
|
||||||
bool inside = false;
|
bool inside = false;
|
||||||
|
constexpr double epsilon = 1e-9;
|
||||||
for (size_t i = 0, j = points.size() - 1; i < points.size(); j = i++)
|
for (size_t i = 0, j = points.size() - 1; i < points.size(); j = i++)
|
||||||
{
|
{
|
||||||
const auto &pi = points[i];
|
const auto &pi = points[i];
|
||||||
const auto &pj = points[j];
|
const auto &pj = points[j];
|
||||||
|
const auto cross =
|
||||||
|
(x - pi.x) * (pj.y - pi.y) - (y - pi.y) * (pj.x - pi.x);
|
||||||
|
const auto minX = std::min(pi.x, pj.x) - epsilon;
|
||||||
|
const auto maxX = std::max(pi.x, pj.x) + epsilon;
|
||||||
|
const auto minY = std::min(pi.y, pj.y) - epsilon;
|
||||||
|
const auto maxY = std::max(pi.y, pj.y) + epsilon;
|
||||||
|
if (std::abs(cross) <= epsilon && x >= minX && x <= maxX && y >= minY &&
|
||||||
|
y <= maxY)
|
||||||
|
{
|
||||||
|
if (onBoundary)
|
||||||
|
{
|
||||||
|
*onBoundary = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const bool intersects =
|
const bool intersects =
|
||||||
((pi.y > y) != (pj.y > y)) &&
|
((pi.y > y) != (pj.y > y)) &&
|
||||||
(x < (pj.x - pi.x) * (y - pi.y) / (pj.y - pi.y) + pi.x);
|
(x < (pj.x - pi.x) * (y - pi.y) / (pj.y - pi.y) + pi.x);
|
||||||
@@ -55,5 +68,96 @@ bool pointInPolygon(double latitude, double longitude, const Json::Value &geojso
|
|||||||
}
|
}
|
||||||
return inside;
|
return inside;
|
||||||
}
|
}
|
||||||
} // namespace visione::geojson
|
|
||||||
|
|
||||||
|
bool polygonContainsPoint(double latitude,
|
||||||
|
double longitude,
|
||||||
|
const Json::Value &polygon)
|
||||||
|
{
|
||||||
|
if (!polygon.isObject() || polygon["type"].asString() != "Polygon" ||
|
||||||
|
!polygon["coordinates"].isArray() || polygon["coordinates"].empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool onBoundary = false;
|
||||||
|
if (!pointInRing(latitude, longitude, polygon["coordinates"][0], &onBoundary))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (onBoundary)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (Json::ArrayIndex i = 1; i < polygon["coordinates"].size(); ++i)
|
||||||
|
{
|
||||||
|
if (pointInRing(latitude, longitude, polygon["coordinates"][i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool pointInPolygon(double latitude, double longitude, const Json::Value &geojson)
|
||||||
|
{
|
||||||
|
return polygonContainsPoint(latitude, longitude, geojson);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pointInGeometry(double latitude, double longitude, const Json::Value &geojson)
|
||||||
|
{
|
||||||
|
if (!geojson.isObject() || !geojson.isMember("type"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto type = geojson["type"].asString();
|
||||||
|
if (type == "Polygon")
|
||||||
|
{
|
||||||
|
return polygonContainsPoint(latitude, longitude, geojson);
|
||||||
|
}
|
||||||
|
if (type == "MultiPolygon" && geojson["coordinates"].isArray())
|
||||||
|
{
|
||||||
|
for (const auto &polygonCoords : geojson["coordinates"])
|
||||||
|
{
|
||||||
|
Json::Value polygon(Json::objectValue);
|
||||||
|
polygon["type"] = "Polygon";
|
||||||
|
polygon["coordinates"] = polygonCoords;
|
||||||
|
if (polygonContainsPoint(latitude, longitude, polygon))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool polygonInsideGeometry(const Json::Value &candidate, const Json::Value &container)
|
||||||
|
{
|
||||||
|
if (!candidate.isObject() || candidate["type"].asString() != "Polygon" ||
|
||||||
|
!candidate["coordinates"].isArray() || candidate["coordinates"].empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &ring = candidate["coordinates"][0];
|
||||||
|
if (!ring.isArray() || ring.size() < 4)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const auto &coordinate : ring)
|
||||||
|
{
|
||||||
|
if (!coordinate.isArray() || coordinate.size() < 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!pointInGeometry(coordinate[1].asDouble(),
|
||||||
|
coordinate[0].asDouble(),
|
||||||
|
container))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace visione::geojson
|
||||||
|
|||||||
+2
-1
@@ -5,5 +5,6 @@
|
|||||||
namespace visione::geojson
|
namespace visione::geojson
|
||||||
{
|
{
|
||||||
bool pointInPolygon(double latitude, double longitude, const Json::Value &geojson);
|
bool pointInPolygon(double latitude, double longitude, const Json::Value &geojson);
|
||||||
|
bool pointInGeometry(double latitude, double longitude, const Json::Value &geojson);
|
||||||
|
bool polygonInsideGeometry(const Json::Value &candidate, const Json::Value &container);
|
||||||
} // namespace visione::geojson
|
} // namespace visione::geojson
|
||||||
|
|
||||||
|
|||||||
+31
-1
@@ -1,5 +1,9 @@
|
|||||||
#include "common/Http.h"
|
#include "common/Http.h"
|
||||||
|
|
||||||
|
#include <drogon/HttpRequest.h>
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
namespace visione::http
|
namespace visione::http
|
||||||
{
|
{
|
||||||
drogon::HttpResponsePtr json(Json::Value body, drogon::HttpStatusCode status)
|
drogon::HttpResponsePtr json(Json::Value body, drogon::HttpStatusCode status)
|
||||||
@@ -22,6 +26,14 @@ drogon::HttpResponsePtr error(const std::string &code,
|
|||||||
return json(std::move(body), status);
|
return json(std::move(body), status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drogon::HttpResponsePtr redirect(const std::string &url,
|
||||||
|
drogon::HttpStatusCode status)
|
||||||
|
{
|
||||||
|
auto response = drogon::HttpResponse::newRedirectionResponse(url);
|
||||||
|
response->setStatusCode(status);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value requireJson(const drogon::HttpRequestPtr &request)
|
Json::Value requireJson(const drogon::HttpRequestPtr &request)
|
||||||
{
|
{
|
||||||
const auto &body = request->getJsonObject();
|
const auto &body = request->getJsonObject();
|
||||||
@@ -31,5 +43,23 @@ Json::Value requireJson(const drogon::HttpRequestPtr &request)
|
|||||||
}
|
}
|
||||||
return *body;
|
return *body;
|
||||||
}
|
}
|
||||||
} // namespace visione::http
|
|
||||||
|
|
||||||
|
Json::Value parseJsonBody(const std::string &body)
|
||||||
|
{
|
||||||
|
if (body.empty())
|
||||||
|
{
|
||||||
|
return Json::Value(Json::nullValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::CharReaderBuilder builder;
|
||||||
|
builder["collectComments"] = false;
|
||||||
|
std::string errors;
|
||||||
|
Json::Value parsed;
|
||||||
|
std::istringstream stream(body);
|
||||||
|
if (!Json::parseFromStream(builder, stream, &parsed, &errors))
|
||||||
|
{
|
||||||
|
return Json::Value(Json::nullValue);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
} // namespace visione::http
|
||||||
|
|||||||
+3
-1
@@ -12,6 +12,8 @@ drogon::HttpResponsePtr json(Json::Value body,
|
|||||||
drogon::HttpResponsePtr error(const std::string &code,
|
drogon::HttpResponsePtr error(const std::string &code,
|
||||||
drogon::HttpStatusCode status,
|
drogon::HttpStatusCode status,
|
||||||
const std::string &message = {});
|
const std::string &message = {});
|
||||||
|
drogon::HttpResponsePtr redirect(const std::string &url,
|
||||||
|
drogon::HttpStatusCode status = drogon::k302Found);
|
||||||
Json::Value requireJson(const drogon::HttpRequestPtr &request);
|
Json::Value requireJson(const drogon::HttpRequestPtr &request);
|
||||||
|
Json::Value parseJsonBody(const std::string &body);
|
||||||
} // namespace visione::http
|
} // namespace visione::http
|
||||||
|
|
||||||
|
|||||||
+60
-3
@@ -1,9 +1,44 @@
|
|||||||
#include "common/Validation.h"
|
#include "common/Validation.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cctype>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
namespace visione::validation
|
namespace visione::validation
|
||||||
{
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
char normalizeFiscalCodeChar(char ch)
|
||||||
|
{
|
||||||
|
return static_cast<char>(
|
||||||
|
std::toupper(static_cast<unsigned char>(ch)));
|
||||||
|
}
|
||||||
|
|
||||||
|
int fiscalCodeControlValueForOdd(char ch)
|
||||||
|
{
|
||||||
|
static constexpr std::array<int, 26> letterValues{
|
||||||
|
1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 2, 4, 18,
|
||||||
|
20, 11, 3, 6, 8, 12, 14, 16, 10, 22, 25, 24, 23};
|
||||||
|
static constexpr std::array<int, 10> digitValues{1, 0, 5, 7, 9,
|
||||||
|
13, 15, 17, 19, 21};
|
||||||
|
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(ch)))
|
||||||
|
{
|
||||||
|
return digitValues[static_cast<size_t>(ch - '0')];
|
||||||
|
}
|
||||||
|
return letterValues[static_cast<size_t>(ch - 'A')];
|
||||||
|
}
|
||||||
|
|
||||||
|
int fiscalCodeControlValueForEven(char ch)
|
||||||
|
{
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(ch)))
|
||||||
|
{
|
||||||
|
return ch - '0';
|
||||||
|
}
|
||||||
|
return ch - 'A';
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
bool hasString(const Json::Value &body, const std::string &key)
|
bool hasString(const Json::Value &body, const std::string &key)
|
||||||
{
|
{
|
||||||
return body.isMember(key) && body[key].isString() &&
|
return body.isMember(key) && body[key].isString() &&
|
||||||
@@ -38,8 +73,30 @@ bool isValidEmail(const std::string &email)
|
|||||||
|
|
||||||
bool isValidFiscalCode(const std::string &fiscalCode)
|
bool isValidFiscalCode(const std::string &fiscalCode)
|
||||||
{
|
{
|
||||||
static const std::regex pattern(R"(^[A-Za-z0-9]{16}$)");
|
static const std::regex pattern(
|
||||||
return std::regex_match(fiscalCode, pattern);
|
R"(^[A-Za-z]{6}[0-9]{2}[ABCDEHLMPRST]{1}[0-9]{2}[A-Za-z][0-9]{3}[A-Za-z]$)");
|
||||||
|
|
||||||
|
if (!std::regex_match(fiscalCode, pattern))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string normalized;
|
||||||
|
normalized.reserve(fiscalCode.size());
|
||||||
|
for (char ch : fiscalCode)
|
||||||
|
{
|
||||||
|
normalized.push_back(normalizeFiscalCodeChar(ch));
|
||||||
|
}
|
||||||
|
|
||||||
|
int sum = 0;
|
||||||
|
for (size_t i = 0; i < normalized.size() - 1; ++i)
|
||||||
|
{
|
||||||
|
sum += ((i + 1) % 2 == 0) ? fiscalCodeControlValueForEven(normalized[i])
|
||||||
|
: fiscalCodeControlValueForOdd(normalized[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char expectedControl =
|
||||||
|
static_cast<char>('A' + (sum % 26));
|
||||||
|
return normalized.back() == expectedControl;
|
||||||
}
|
}
|
||||||
} // namespace visione::validation
|
} // namespace visione::validation
|
||||||
|
|
||||||
|
|||||||
+26
-6
@@ -40,7 +40,13 @@
|
|||||||
"use_gzip": true,
|
"use_gzip": true,
|
||||||
"use_brotli": false,
|
"use_brotli": false,
|
||||||
"client_max_body_size": "50M",
|
"client_max_body_size": "50M",
|
||||||
"client_max_memory_body_size": "256K"
|
"client_max_memory_body_size": "256K",
|
||||||
|
"static_files": [
|
||||||
|
{
|
||||||
|
"document_root": "./uploads",
|
||||||
|
"alias": "/uploads"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"custom_config": {
|
"custom_config": {
|
||||||
@@ -50,20 +56,34 @@
|
|||||||
"token_ttl_hours": 24,
|
"token_ttl_hours": 24,
|
||||||
"otp_ttl_minutes": 10
|
"otp_ttl_minutes": 10
|
||||||
},
|
},
|
||||||
|
"cie_auth": {
|
||||||
|
"service_base_url": "http://127.0.0.1:8001",
|
||||||
|
"start_path": "/visione/start",
|
||||||
|
"shared_secret": "change-me-for-local-dev",
|
||||||
|
"state_ttl_minutes": 10,
|
||||||
|
"default_frontend_redirect_uri": "http://localhost:3000/auth/cie/callback"
|
||||||
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"smtp_host": "127.0.0.1",
|
"provider": "smtp",
|
||||||
"smtp_port": 1025,
|
"host": "127.0.0.1",
|
||||||
"from": "[email protected]"
|
"port": 1025,
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"from": "[email protected]",
|
||||||
|
"use_tls": false,
|
||||||
|
"use_starttls": false,
|
||||||
|
"verify_peer": false,
|
||||||
|
"verify_host": false
|
||||||
},
|
},
|
||||||
"uploads": {
|
"uploads": {
|
||||||
"public_base_path": "/uploads",
|
"public_base_path": "/uploads",
|
||||||
"reports_path": "uploads/reports"
|
"reports_path": "reports"
|
||||||
},
|
},
|
||||||
"openapi": {
|
"openapi": {
|
||||||
"spec_path": "build/openapi/visione.openapi.json"
|
"spec_path": "build/openapi/visione.openapi.json"
|
||||||
},
|
},
|
||||||
"cors": {
|
"cors": {
|
||||||
"allowed_origin": ["http://localhost:3000", "http://localhost:5137"]
|
"allowed_origin": "http://127.0.0.1:3000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+117
-48
@@ -33,19 +33,6 @@ std::optional<visione::auth::UserContext> requireUser(
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool blockPasswordChangeRequired(
|
|
||||||
const visione::auth::UserContext &user,
|
|
||||||
std::function<void(const HttpResponsePtr &)> &callback)
|
|
||||||
{
|
|
||||||
if (!visione::auth::mustChangePassword(user))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
callback(visione::http::error("password_change_required",
|
|
||||||
drogon::k403Forbidden));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendAdmin(Json::Value body,
|
void sendAdmin(Json::Value body,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
@@ -56,26 +43,37 @@ void sendAdmin(Json::Value body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto error = body.get("error", "").asString();
|
const auto error = body.get("error", "").asString();
|
||||||
const auto status = error == "forbidden" ? drogon::k403Forbidden
|
drogon::HttpStatusCode status = drogon::k400BadRequest;
|
||||||
: drogon::k400BadRequest;
|
if (error == "forbidden" || error.rfind("cannot_", 0) == 0)
|
||||||
|
{
|
||||||
|
status = drogon::k403Forbidden;
|
||||||
|
}
|
||||||
|
else if (error.ends_with("_not_found"))
|
||||||
|
{
|
||||||
|
status = drogon::k404NotFound;
|
||||||
|
}
|
||||||
|
else if (error == "invalid_status_transition")
|
||||||
|
{
|
||||||
|
status = drogon::k409Conflict;
|
||||||
|
}
|
||||||
|
else if (error == "invalid_reference" || error.starts_with("invalid_"))
|
||||||
|
{
|
||||||
|
status = drogon::k422UnprocessableEntity;
|
||||||
|
}
|
||||||
|
else if (error == "area_outside_allowed_boundary")
|
||||||
|
{
|
||||||
|
status = drogon::k422UnprocessableEntity;
|
||||||
|
}
|
||||||
callback(visione::http::json(std::move(body), status));
|
callback(visione::http::json(std::move(body), status));
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void AdminController::listCategories(
|
|
||||||
const drogon::HttpRequestPtr &,
|
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
|
||||||
{
|
|
||||||
visione::services::AdminService service;
|
|
||||||
callback(visione::http::json(service.listCategories()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AdminController::createCategory(
|
void AdminController::createCategory(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -84,20 +82,12 @@ void AdminController::createCategory(
|
|||||||
std::move(callback));
|
std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
void AdminController::listAreas(
|
|
||||||
const drogon::HttpRequestPtr &,
|
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
|
||||||
{
|
|
||||||
visione::services::AdminService service;
|
|
||||||
callback(visione::http::json(service.listAreas()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AdminController::createArea(
|
void AdminController::createArea(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -106,20 +96,12 @@ void AdminController::createArea(
|
|||||||
std::move(callback));
|
std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
void AdminController::listTeams(
|
|
||||||
const drogon::HttpRequestPtr &,
|
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
|
||||||
{
|
|
||||||
visione::services::AdminService service;
|
|
||||||
callback(visione::http::json(service.listTeams()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AdminController::createTeam(
|
void AdminController::createTeam(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -128,12 +110,99 @@ void AdminController::createTeam(
|
|||||||
std::move(callback));
|
std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AdminController::updateCategory(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long categoryId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::AdminService service;
|
||||||
|
sendAdmin(service.updateCategory(*user, categoryId, visione::http::requireJson(req)),
|
||||||
|
std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AdminController::deleteCategory(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long categoryId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::AdminService service;
|
||||||
|
sendAdmin(service.deleteCategory(*user, categoryId), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AdminController::updateArea(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long areaId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::AdminService service;
|
||||||
|
sendAdmin(service.updateArea(*user, areaId, visione::http::requireJson(req)),
|
||||||
|
std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AdminController::deleteArea(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long areaId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::AdminService service;
|
||||||
|
sendAdmin(service.deleteArea(*user, areaId), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AdminController::updateTeam(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long teamId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::AdminService service;
|
||||||
|
sendAdmin(service.updateTeam(*user, teamId, visione::http::requireJson(req)),
|
||||||
|
std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AdminController::deleteTeam(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long teamId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::AdminService service;
|
||||||
|
sendAdmin(service.deleteTeam(*user, teamId), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
void AdminController::overviewStats(
|
void AdminController::overviewStats(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -146,7 +215,7 @@ void AdminController::listModeration(
|
|||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -164,7 +233,7 @@ void AdminController::updateModeration(
|
|||||||
long long flagId)
|
long long flagId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -178,7 +247,7 @@ void AdminController::listUsers(
|
|||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -192,7 +261,7 @@ void AdminController::updateUserRoles(
|
|||||||
long long userId)
|
long long userId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -207,7 +276,7 @@ void AdminController::updateUserBan(
|
|||||||
long long userId)
|
long long userId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,29 @@ class AdminController : public drogon::HttpController<AdminController>
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
METHOD_LIST_BEGIN
|
METHOD_LIST_BEGIN
|
||||||
ADD_METHOD_TO(AdminController::listCategories,
|
|
||||||
"/api/v1/admin/categories",
|
|
||||||
drogon::Get);
|
|
||||||
ADD_METHOD_TO(AdminController::createCategory,
|
ADD_METHOD_TO(AdminController::createCategory,
|
||||||
"/api/v1/admin/categories",
|
"/api/v1/admin/categories",
|
||||||
drogon::Post);
|
drogon::Post);
|
||||||
ADD_METHOD_TO(AdminController::listAreas, "/api/v1/admin/areas", drogon::Get);
|
|
||||||
ADD_METHOD_TO(AdminController::createArea, "/api/v1/admin/areas", drogon::Post);
|
ADD_METHOD_TO(AdminController::createArea, "/api/v1/admin/areas", drogon::Post);
|
||||||
ADD_METHOD_TO(AdminController::listTeams, "/api/v1/admin/teams", drogon::Get);
|
|
||||||
ADD_METHOD_TO(AdminController::createTeam, "/api/v1/admin/teams", drogon::Post);
|
ADD_METHOD_TO(AdminController::createTeam, "/api/v1/admin/teams", drogon::Post);
|
||||||
|
ADD_METHOD_TO(AdminController::updateCategory,
|
||||||
|
"/api/v1/admin/categories/{1}",
|
||||||
|
drogon::Patch);
|
||||||
|
ADD_METHOD_TO(AdminController::deleteCategory,
|
||||||
|
"/api/v1/admin/categories/{1}",
|
||||||
|
drogon::Delete);
|
||||||
|
ADD_METHOD_TO(AdminController::updateArea,
|
||||||
|
"/api/v1/admin/areas/{1}",
|
||||||
|
drogon::Patch);
|
||||||
|
ADD_METHOD_TO(AdminController::deleteArea,
|
||||||
|
"/api/v1/admin/areas/{1}",
|
||||||
|
drogon::Delete);
|
||||||
|
ADD_METHOD_TO(AdminController::updateTeam,
|
||||||
|
"/api/v1/admin/teams/{1}",
|
||||||
|
drogon::Patch);
|
||||||
|
ADD_METHOD_TO(AdminController::deleteTeam,
|
||||||
|
"/api/v1/admin/teams/{1}",
|
||||||
|
drogon::Delete);
|
||||||
ADD_METHOD_TO(AdminController::overviewStats,
|
ADD_METHOD_TO(AdminController::overviewStats,
|
||||||
"/api/v1/admin/stats/overview",
|
"/api/v1/admin/stats/overview",
|
||||||
drogon::Get);
|
drogon::Get);
|
||||||
@@ -34,24 +47,39 @@ class AdminController : public drogon::HttpController<AdminController>
|
|||||||
drogon::Patch);
|
drogon::Patch);
|
||||||
METHOD_LIST_END
|
METHOD_LIST_END
|
||||||
|
|
||||||
void listCategories(
|
|
||||||
const drogon::HttpRequestPtr &req,
|
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
|
||||||
void createCategory(
|
void createCategory(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void listAreas(
|
|
||||||
const drogon::HttpRequestPtr &req,
|
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
|
||||||
void createArea(
|
void createArea(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void listTeams(
|
|
||||||
const drogon::HttpRequestPtr &req,
|
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
|
||||||
void createTeam(
|
void createTeam(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
|
void updateCategory(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long categoryId);
|
||||||
|
void deleteCategory(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long categoryId);
|
||||||
|
void updateArea(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long areaId);
|
||||||
|
void deleteArea(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long areaId);
|
||||||
|
void updateTeam(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long teamId);
|
||||||
|
void deleteTeam(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long teamId);
|
||||||
void overviewStats(
|
void overviewStats(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ void AuthController::verifyOtp(
|
|||||||
serviceResponse(service.verifyOtp(body), std::move(callback));
|
serviceResponse(service.verifyOtp(body), std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
void AuthController::loginWithPassword(
|
void AuthController::startCieLogin(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
@@ -78,21 +78,15 @@ void AuthController::loginWithPassword(
|
|||||||
}
|
}
|
||||||
|
|
||||||
visione::services::AuthService service;
|
visione::services::AuthService service;
|
||||||
serviceResponse(service.loginWithPassword(body), std::move(callback));
|
serviceResponse(service.startCieLogin(body), std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
void AuthController::changePassword(
|
void AuthController::completeCieLogin(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
const auto user = visione::auth::authenticate(req);
|
const std::string rawBody(req->body());
|
||||||
if (!user)
|
const auto body = visione::http::parseJsonBody(rawBody);
|
||||||
{
|
|
||||||
callback(visione::http::error("unauthorized", drogon::k401Unauthorized));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto body = visione::http::requireJson(req);
|
|
||||||
if (body.isNull())
|
if (body.isNull())
|
||||||
{
|
{
|
||||||
callback(visione::http::error("invalid_json", drogon::k400BadRequest));
|
callback(visione::http::error("invalid_json", drogon::k400BadRequest));
|
||||||
@@ -100,7 +94,30 @@ void AuthController::changePassword(
|
|||||||
}
|
}
|
||||||
|
|
||||||
visione::services::AuthService service;
|
visione::services::AuthService service;
|
||||||
serviceResponse(service.changePassword(*user, body), std::move(callback));
|
auto responseBody =
|
||||||
|
service.completeCieLogin(rawBody, body, req->getHeader("X-Visione-Signature"));
|
||||||
|
if (responseBody.get("ok", false).asBool() &&
|
||||||
|
responseBody.isMember("redirect_url"))
|
||||||
|
{
|
||||||
|
callback(visione::http::redirect(responseBody["redirect_url"].asString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
serviceResponse(std::move(responseBody), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AuthController::cieSessionStatus(
|
||||||
|
const drogon::HttpRequestPtr & /*req*/,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
std::string state)
|
||||||
|
{
|
||||||
|
visione::services::AuthService service;
|
||||||
|
auto body = service.cieSessionStatus(state);
|
||||||
|
if (!body.get("ok", false).asBool())
|
||||||
|
{
|
||||||
|
callback(visione::http::json(std::move(body), drogon::k404NotFound));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(visione::http::json(std::move(body)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void AuthController::me(
|
void AuthController::me(
|
||||||
@@ -117,3 +134,24 @@ void AuthController::me(
|
|||||||
visione::services::AuthService service;
|
visione::services::AuthService service;
|
||||||
callback(visione::http::json(service.currentUser(*user)));
|
callback(visione::http::json(service.currentUser(*user)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AuthController::editUser(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback) {
|
||||||
|
const auto user = visione::auth::authenticate(req);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
callback(visione::http::error("unauthorized", drogon::k401Unauthorized));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto body = visione::http::requireJson(req);
|
||||||
|
if (body.isNull())
|
||||||
|
{
|
||||||
|
callback(visione::http::error("invalid_json", drogon::k400BadRequest));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visione::services::AuthService service;
|
||||||
|
serviceResponse(service.editUser(*user, body), std::move(callback));
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,13 +15,17 @@ class AuthController : public drogon::HttpController<AuthController>
|
|||||||
ADD_METHOD_TO(AuthController::verifyOtp,
|
ADD_METHOD_TO(AuthController::verifyOtp,
|
||||||
"/api/v1/auth/otp/verify",
|
"/api/v1/auth/otp/verify",
|
||||||
drogon::Post);
|
drogon::Post);
|
||||||
ADD_METHOD_TO(AuthController::loginWithPassword,
|
ADD_METHOD_TO(AuthController::startCieLogin,
|
||||||
"/api/v1/auth/login/password",
|
"/api/v1/auth/cie/start",
|
||||||
drogon::Post);
|
drogon::Post);
|
||||||
ADD_METHOD_TO(AuthController::changePassword,
|
ADD_METHOD_TO(AuthController::completeCieLogin,
|
||||||
"/api/v1/auth/change-password",
|
"/api/v1/auth/cie/exchange",
|
||||||
drogon::Post);
|
drogon::Post);
|
||||||
|
ADD_METHOD_TO(AuthController::cieSessionStatus,
|
||||||
|
"/api/v1/auth/cie/session/{1}",
|
||||||
|
drogon::Get);
|
||||||
ADD_METHOD_TO(AuthController::me, "/api/v1/me", drogon::Get);
|
ADD_METHOD_TO(AuthController::me, "/api/v1/me", drogon::Get);
|
||||||
|
ADD_METHOD_TO(AuthController::editUser, "/api/v1/me", drogon::Patch);
|
||||||
METHOD_LIST_END
|
METHOD_LIST_END
|
||||||
|
|
||||||
void registerCitizen(
|
void registerCitizen(
|
||||||
@@ -33,12 +37,18 @@ class AuthController : public drogon::HttpController<AuthController>
|
|||||||
void verifyOtp(
|
void verifyOtp(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void loginWithPassword(
|
void startCieLogin(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void changePassword(
|
void completeCieLogin(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
|
void cieSessionStatus(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
std::string state);
|
||||||
void me(const drogon::HttpRequestPtr &req,
|
void me(const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
|
void editUser(const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ std::optional<std::string> queryValue(const drogon::HttpRequestPtr &req,
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
void PublicController::listAreas(
|
||||||
|
const drogon::HttpRequestPtr &,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
|
{
|
||||||
|
visione::services::ReportService service;
|
||||||
|
callback(visione::http::json(service.listAreas()));
|
||||||
|
}
|
||||||
|
|
||||||
void PublicController::openDataReports(
|
void PublicController::openDataReports(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
@@ -29,3 +37,15 @@ void PublicController::openDataReports(
|
|||||||
callback(visione::http::json(service.openDataReports(queryValue(req, "page"),
|
callback(visione::http::json(service.openDataReports(queryValue(req, "page"),
|
||||||
queryValue(req, "per_page"))));
|
queryValue(req, "per_page"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PublicController::interventionSummary(
|
||||||
|
const drogon::HttpRequestPtr &,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
const std::string &token)
|
||||||
|
{
|
||||||
|
visione::services::ReportService service;
|
||||||
|
auto body = service.publicInterventionSummary(token);
|
||||||
|
const auto status = body.get("ok", false).asBool() ? drogon::k200OK
|
||||||
|
: drogon::k404NotFound;
|
||||||
|
callback(visione::http::json(std::move(body), status));
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,12 +6,22 @@ class PublicController : public drogon::HttpController<PublicController>
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
METHOD_LIST_BEGIN
|
METHOD_LIST_BEGIN
|
||||||
|
ADD_METHOD_TO(PublicController::listAreas, "/api/v1/areas", drogon::Get);
|
||||||
ADD_METHOD_TO(PublicController::openDataReports,
|
ADD_METHOD_TO(PublicController::openDataReports,
|
||||||
"/api/v1/open-data/reports",
|
"/api/v1/open-data/reports",
|
||||||
drogon::Get);
|
drogon::Get);
|
||||||
|
ADD_METHOD_TO(PublicController::interventionSummary,
|
||||||
|
"/api/v1/interventions/{1}",
|
||||||
|
drogon::Get);
|
||||||
METHOD_LIST_END
|
METHOD_LIST_END
|
||||||
|
|
||||||
|
void listAreas(const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void openDataReports(
|
void openDataReports(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
|
void interventionSummary(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
const std::string &token);
|
||||||
};
|
};
|
||||||
|
|||||||
+111
-32
@@ -36,19 +36,6 @@ std::optional<visione::auth::UserContext> requireUser(
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool blockPasswordChangeRequired(
|
|
||||||
const visione::auth::UserContext &user,
|
|
||||||
std::function<void(const HttpResponsePtr &)> &callback)
|
|
||||||
{
|
|
||||||
if (!visione::auth::mustChangePassword(user))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
callback(visione::http::error("password_change_required",
|
|
||||||
drogon::k403Forbidden));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendService(Json::Value body,
|
void sendService(Json::Value body,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
@@ -58,8 +45,32 @@ void sendService(Json::Value body,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto error = body.get("error", "").asString();
|
const auto error = body.get("error", "").asString();
|
||||||
const auto status = error == "forbidden" ? drogon::k403Forbidden
|
drogon::HttpStatusCode status = drogon::k400BadRequest;
|
||||||
: drogon::k400BadRequest;
|
if (error == "forbidden" || error.rfind("cannot_", 0) == 0)
|
||||||
|
{
|
||||||
|
status = drogon::k403Forbidden;
|
||||||
|
}
|
||||||
|
else if (error.ends_with("_not_found"))
|
||||||
|
{
|
||||||
|
status = drogon::k404NotFound;
|
||||||
|
}
|
||||||
|
else if (error == "invalid_status_transition" ||
|
||||||
|
error == "media_limit_reached" ||
|
||||||
|
error == "report_not_open" ||
|
||||||
|
error == "report_owned_by_other_operator" ||
|
||||||
|
error == "feedback_already_exists")
|
||||||
|
{
|
||||||
|
status = drogon::k409Conflict;
|
||||||
|
}
|
||||||
|
else if (error == "invalid_reference" || error.starts_with("invalid_") ||
|
||||||
|
error == "missing_rating")
|
||||||
|
{
|
||||||
|
status = drogon::k422UnprocessableEntity;
|
||||||
|
}
|
||||||
|
else if (error == "coordinates_outside_allowed_boundary")
|
||||||
|
{
|
||||||
|
status = drogon::k422UnprocessableEntity;
|
||||||
|
}
|
||||||
callback(visione::http::json(std::move(body), status));
|
callback(visione::http::json(std::move(body), status));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +107,7 @@ void ReportController::create(
|
|||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -116,7 +127,7 @@ void ReportController::list(
|
|||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -129,12 +140,38 @@ void ReportController::list(
|
|||||||
std::move(callback));
|
std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ReportController::listCategories(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::ReportService service;
|
||||||
|
sendService(service.listCategories(*user), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReportController::listTeams(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::ReportService service;
|
||||||
|
sendService(service.listOperatorTeams(*user), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
void ReportController::assignedReports(
|
void ReportController::assignedReports(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -150,7 +187,7 @@ void ReportController::openReports(
|
|||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -166,7 +203,7 @@ void ReportController::otherOperatorReports(
|
|||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -183,7 +220,7 @@ void ReportController::detail(
|
|||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -197,7 +234,7 @@ void ReportController::interventionSummary(
|
|||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -205,13 +242,55 @@ void ReportController::interventionSummary(
|
|||||||
sendService(service.interventionSummary(*user, reportId), std::move(callback));
|
sendService(service.interventionSummary(*user, reportId), std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ReportController::teamSuggestions(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long reportId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::ReportService service;
|
||||||
|
sendService(service.teamSuggestions(*user, reportId), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReportController::createInterventionLink(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long reportId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::ReportService service;
|
||||||
|
sendService(service.createInterventionLink(*user, reportId), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReportController::revokeInterventionLink(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
|
long long reportId)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::ReportService service;
|
||||||
|
sendService(service.revokeInterventionLink(*user, reportId), std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
void ReportController::updateStatus(
|
void ReportController::updateStatus(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -226,7 +305,7 @@ void ReportController::takeReport(
|
|||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -240,7 +319,7 @@ void ReportController::addMessage(
|
|||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -255,7 +334,7 @@ void ReportController::listMessages(
|
|||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -270,7 +349,7 @@ void ReportController::flagMessage(
|
|||||||
long long messageId)
|
long long messageId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -286,7 +365,7 @@ void ReportController::assignTeam(
|
|||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -301,7 +380,7 @@ void ReportController::addFeedback(
|
|||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -316,7 +395,7 @@ void ReportController::flagReport(
|
|||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -331,7 +410,7 @@ void ReportController::uploadMedia(
|
|||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -356,7 +435,7 @@ void ReportController::uploadMedia(
|
|||||||
auto allowed = service.canAddMedia(*user, reportId);
|
auto allowed = service.canAddMedia(*user, reportId);
|
||||||
if (!allowed.get("ok", false).asBool())
|
if (!allowed.get("ok", false).asBool())
|
||||||
{
|
{
|
||||||
callback(visione::http::json(std::move(allowed), drogon::k403Forbidden));
|
sendService(std::move(allowed), std::move(callback));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ class ReportController : public drogon::HttpController<ReportController>
|
|||||||
METHOD_LIST_BEGIN
|
METHOD_LIST_BEGIN
|
||||||
ADD_METHOD_TO(ReportController::create, "/api/v1/reports", drogon::Post);
|
ADD_METHOD_TO(ReportController::create, "/api/v1/reports", drogon::Post);
|
||||||
ADD_METHOD_TO(ReportController::list, "/api/v1/reports", drogon::Get);
|
ADD_METHOD_TO(ReportController::list, "/api/v1/reports", drogon::Get);
|
||||||
|
ADD_METHOD_TO(ReportController::listCategories,
|
||||||
|
"/api/v1/report-categories",
|
||||||
|
drogon::Get);
|
||||||
|
ADD_METHOD_TO(ReportController::listTeams,
|
||||||
|
"/api/v1/operator/teams",
|
||||||
|
drogon::Get);
|
||||||
ADD_METHOD_TO(ReportController::assignedReports,
|
ADD_METHOD_TO(ReportController::assignedReports,
|
||||||
"/api/v1/operator/reports/assigned",
|
"/api/v1/operator/reports/assigned",
|
||||||
drogon::Get);
|
drogon::Get);
|
||||||
@@ -21,6 +27,15 @@ class ReportController : public drogon::HttpController<ReportController>
|
|||||||
ADD_METHOD_TO(ReportController::interventionSummary,
|
ADD_METHOD_TO(ReportController::interventionSummary,
|
||||||
"/api/v1/reports/{1}/intervention-summary",
|
"/api/v1/reports/{1}/intervention-summary",
|
||||||
drogon::Get);
|
drogon::Get);
|
||||||
|
ADD_METHOD_TO(ReportController::teamSuggestions,
|
||||||
|
"/api/v1/reports/{1}/team-suggestions",
|
||||||
|
drogon::Get);
|
||||||
|
ADD_METHOD_TO(ReportController::createInterventionLink,
|
||||||
|
"/api/v1/reports/{1}/intervention-link",
|
||||||
|
drogon::Post);
|
||||||
|
ADD_METHOD_TO(ReportController::revokeInterventionLink,
|
||||||
|
"/api/v1/reports/{1}/intervention-link",
|
||||||
|
drogon::Delete);
|
||||||
ADD_METHOD_TO(ReportController::updateStatus,
|
ADD_METHOD_TO(ReportController::updateStatus,
|
||||||
"/api/v1/reports/{1}/status",
|
"/api/v1/reports/{1}/status",
|
||||||
drogon::Patch);
|
drogon::Patch);
|
||||||
@@ -54,6 +69,11 @@ class ReportController : public drogon::HttpController<ReportController>
|
|||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void list(const drogon::HttpRequestPtr &req,
|
void list(const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
|
void listCategories(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
|
void listTeams(const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void assignedReports(
|
void assignedReports(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
@@ -70,6 +90,18 @@ class ReportController : public drogon::HttpController<ReportController>
|
|||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
long long reportId);
|
long long reportId);
|
||||||
|
void teamSuggestions(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long reportId);
|
||||||
|
void createInterventionLink(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long reportId);
|
||||||
|
void revokeInterventionLink(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
long long reportId);
|
||||||
void updateStatus(
|
void updateStatus(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
|
|||||||
@@ -32,19 +32,6 @@ std::optional<visione::auth::UserContext> requireUser(
|
|||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool blockPasswordChangeRequired(
|
|
||||||
const visione::auth::UserContext &user,
|
|
||||||
std::function<void(const HttpResponsePtr &)> &callback)
|
|
||||||
{
|
|
||||||
if (!visione::auth::mustChangePassword(user))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
callback(visione::http::error("password_change_required",
|
|
||||||
drogon::k403Forbidden));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void UserController::listNotifications(
|
void UserController::listNotifications(
|
||||||
@@ -52,7 +39,7 @@ void UserController::listNotifications(
|
|||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -68,7 +55,7 @@ void UserController::markNotificationRead(
|
|||||||
long long notificationId)
|
long long notificationId)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -79,12 +66,25 @@ void UserController::markNotificationRead(
|
|||||||
callback(visione::http::json(std::move(body), status));
|
callback(visione::http::json(std::move(body), status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UserController::markAllNotificationsRead(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
|
{
|
||||||
|
auto user = requireUser(req, callback);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visione::services::UserService service;
|
||||||
|
callback(visione::http::json(service.markAllNotificationsRead(*user)));
|
||||||
|
}
|
||||||
|
|
||||||
void UserController::rewards(
|
void UserController::rewards(
|
||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||||
{
|
{
|
||||||
auto user = requireUser(req, callback);
|
auto user = requireUser(req, callback);
|
||||||
if (!user || blockPasswordChangeRequired(*user, callback))
|
if (!user)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ class UserController : public drogon::HttpController<UserController>
|
|||||||
ADD_METHOD_TO(UserController::markNotificationRead,
|
ADD_METHOD_TO(UserController::markNotificationRead,
|
||||||
"/api/v1/notifications/{1}/read",
|
"/api/v1/notifications/{1}/read",
|
||||||
drogon::Patch);
|
drogon::Patch);
|
||||||
|
ADD_METHOD_TO(UserController::markAllNotificationsRead,
|
||||||
|
"/api/v1/notifications/read-all",
|
||||||
|
drogon::Patch);
|
||||||
ADD_METHOD_TO(UserController::rewards, "/api/v1/me/rewards", drogon::Get);
|
ADD_METHOD_TO(UserController::rewards, "/api/v1/me/rewards", drogon::Get);
|
||||||
METHOD_LIST_END
|
METHOD_LIST_END
|
||||||
|
|
||||||
@@ -22,6 +25,9 @@ class UserController : public drogon::HttpController<UserController>
|
|||||||
const drogon::HttpRequestPtr &req,
|
const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
|
||||||
long long notificationId);
|
long long notificationId);
|
||||||
|
void markAllNotificationsRead(
|
||||||
|
const drogon::HttpRequestPtr &req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
void rewards(const drogon::HttpRequestPtr &req,
|
void rewards(const drogon::HttpRequestPtr &req,
|
||||||
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
std::function<void(const drogon::HttpResponsePtr &)> &&callback);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
services:
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
ports:
|
|
||||||
- "${BACKEND_PORT:-5555}:5555"
|
|
||||||
volumes:
|
|
||||||
- ./config.json:/app/config.json
|
|
||||||
environment:
|
|
||||||
- DB_HOST=mysql
|
|
||||||
- DB_PORT=3306
|
|
||||||
- DB_USER=${MYSQL_USER:-visione}
|
|
||||||
- DB_PASSWORD=${MYSQL_PASSWORD:-visione}
|
|
||||||
- DB_NAME=${MYSQL_DATABASE:-visione}
|
|
||||||
- SMTP_HOST=mailhog
|
|
||||||
- SMTP_PORT=1025
|
|
||||||
depends_on:
|
|
||||||
mysql:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
mysql:
|
|
||||||
image: mysql:8.4
|
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:-root}"
|
|
||||||
MYSQL_DATABASE: "${MYSQL_DATABASE:-visione}"
|
|
||||||
MYSQL_USER: "${MYSQL_USER:-visione}"
|
|
||||||
MYSQL_PASSWORD: "${MYSQL_PASSWORD:-visione}"
|
|
||||||
ports:
|
|
||||||
- "${MYSQL_PORT:-3306}:3306"
|
|
||||||
volumes:
|
|
||||||
- mysql-data:/var/lib/mysql
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u$${MYSQL_USER} -p$${MYSQL_PASSWORD} --silent"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 20
|
|
||||||
start_period: 20s
|
|
||||||
|
|
||||||
mailhog:
|
|
||||||
image: mailhog/mailhog:v1.0.1
|
|
||||||
ports:
|
|
||||||
- "${MAILHOG_SMTP_PORT:-1025}:1025"
|
|
||||||
- "${MAILHOG_WEB_PORT:-8025}:8025"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mysql-data:
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- uploads-data:/app/uploads
|
||||||
|
environment:
|
||||||
|
DB_HOST: ${DB_HOST:-mysql}
|
||||||
|
DB_PORT: ${DB_PORT:-3306}
|
||||||
|
DB_USER: ${DB_USER:-visione}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-visione}
|
||||||
|
DB_NAME: ${DB_NAME:-visione}
|
||||||
|
DB_TIMEOUT: ${DB_TIMEOUT:-30}
|
||||||
|
MAIL_PROVIDER: ${MAIL_PROVIDER:-resend}
|
||||||
|
SMTP_HOST: ${SMTP_HOST:-smtp.resend.com}
|
||||||
|
SMTP_PORT: ${SMTP_PORT:-465}
|
||||||
|
SMTP_USERNAME: ${SMTP_USERNAME:-resend}
|
||||||
|
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||||
|
SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
|
||||||
|
SMTP_USE_STARTTLS: ${SMTP_USE_STARTTLS:-false}
|
||||||
|
SMTP_VERIFY_PEER: ${SMTP_VERIFY_PEER:-true}
|
||||||
|
SMTP_VERIFY_HOST: ${SMTP_VERIFY_HOST:-true}
|
||||||
|
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
||||||
|
MAIL_FROM: ${MAIL_FROM:[email protected]}
|
||||||
|
CORS_ALLOWED_ORIGIN: ${CORS_ALLOWED_ORIGIN:-http://localhost:3000}
|
||||||
|
CIE_AUTH_SERVICE_BASE_URL: ${CIE_AUTH_SERVICE_BASE_URL:-http://127.0.0.1:8001}
|
||||||
|
CIE_AUTH_START_PATH: ${CIE_AUTH_START_PATH:-/visione/start}
|
||||||
|
CIE_AUTH_SHARED_SECRET: ${CIE_AUTH_SHARED_SECRET:-change-me-for-local-dev}
|
||||||
|
CIE_AUTH_STATE_TTL_MINUTES: ${CIE_AUTH_STATE_TTL_MINUTES:-10}
|
||||||
|
CIE_AUTH_DEFAULT_FRONTEND_REDIRECT_URI: ${CIE_AUTH_DEFAULT_FRONTEND_REDIRECT_URI:-http://localhost:3000/auth/cie/callback}
|
||||||
|
expose:
|
||||||
|
- "5555"
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.4
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:-root}"
|
||||||
|
MYSQL_DATABASE: "${DB_NAME:-visione}"
|
||||||
|
MYSQL_USER: "${DB_USER:-visione}"
|
||||||
|
MYSQL_PASSWORD: "${DB_PASSWORD:-visione}"
|
||||||
|
expose:
|
||||||
|
- "3306"
|
||||||
|
volumes:
|
||||||
|
- mysql-data:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u$${MYSQL_USER} -p$${MYSQL_PASSWORD} --silent"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
mailpit:
|
||||||
|
image: axllent/mailpit:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
expose:
|
||||||
|
- "1025"
|
||||||
|
- "8025"
|
||||||
|
|
||||||
|
phpmyadmin:
|
||||||
|
image: phpmyadmin:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
expose:
|
||||||
|
- "80"
|
||||||
|
environment:
|
||||||
|
PMA_HOST: mysql
|
||||||
|
PMA_USER: ${DB_USER:-visione}
|
||||||
|
PMA_PASSWORD: ${DB_PASSWORD:-visione}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql-data:
|
||||||
|
uploads-data:
|
||||||
+4
-4
@@ -17,11 +17,11 @@ services:
|
|||||||
retries: 20
|
retries: 20
|
||||||
start_period: 20s
|
start_period: 20s
|
||||||
|
|
||||||
mailhog:
|
mailpit:
|
||||||
image: mailhog/mailhog:v1.0.1
|
image: axllent/mailpit:latest
|
||||||
ports:
|
ports:
|
||||||
- "${MAILHOG_SMTP_PORT:-1025}:1025"
|
- "${MAILPIT_SMTP_PORT:-1025}:1025"
|
||||||
- "${MAILHOG_WEB_PORT:-8025}:8025"
|
- "${MAILPIT_WEB_PORT:-8025}:8025"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql-data:
|
mysql-data:
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
# Visione API Spec
|
|
||||||
|
|
||||||
Base path: `/api/v1`.
|
|
||||||
|
|
||||||
## Technical
|
|
||||||
- `GET /health`: process health.
|
|
||||||
- `GET /ready`: MySQL readiness.
|
|
||||||
- `GET /openapi.json`: generated OpenAPI 3.0 JSON.
|
|
||||||
- `GET /docs`: Swagger UI page backed by the generated spec.
|
|
||||||
|
|
||||||
## Auth
|
|
||||||
- `POST /auth/register`: create a citizen account.
|
|
||||||
- `POST /auth/otp/request`: send an OTP email through MailHog.
|
|
||||||
- `POST /auth/otp/verify`: verify OTP and create an API token.
|
|
||||||
- `POST /auth/login/password`: password login fallback.
|
|
||||||
- `POST /auth/change-password`: change the authenticated user's password.
|
|
||||||
- `GET /me`: return the authenticated user.
|
|
||||||
|
|
||||||
Protected endpoints use:
|
|
||||||
|
|
||||||
```http
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reports
|
|
||||||
- `POST /reports`: create a report.
|
|
||||||
- `GET /reports`: list reports with optional `status`, `category_id`, `area_id`.
|
|
||||||
- `GET /operator/reports/assigned`: list reports assigned to the current operator.
|
|
||||||
- `GET /operator/reports/open`: list open unassigned reports.
|
|
||||||
- `GET /operator/reports/others`: list reports assigned to other operators.
|
|
||||||
- `GET /reports/{id}`: report detail.
|
|
||||||
- `GET /reports/{id}/intervention-summary`: operator-facing report summary with
|
|
||||||
citizen contact data, media and latest messages.
|
|
||||||
- `PATCH /reports/{id}/status`: update status.
|
|
||||||
- `POST /reports/{id}/take`: operator/admin takes an open report.
|
|
||||||
- `POST /reports/{id}/messages`: write a report message.
|
|
||||||
- `GET /reports/{id}/messages`: list report messages.
|
|
||||||
- `POST /reports/{id}/messages/{message_id}/flags`: flag a message for moderation.
|
|
||||||
- `POST /reports/{id}/assignments`: assign a team.
|
|
||||||
- `POST /reports/{id}/feedback`: add citizen feedback.
|
|
||||||
- `POST /reports/{id}/flags`: flag a report for moderation.
|
|
||||||
- `POST /reports/{id}/media`: upload up to five multipart media files. Supported
|
|
||||||
types are JPEG, PNG, WebP, MP4, WebM and QuickTime.
|
|
||||||
|
|
||||||
## User
|
|
||||||
- `GET /notifications`: list authenticated user notifications.
|
|
||||||
- `PATCH /notifications/{id}/read`: mark one notification as read.
|
|
||||||
- `GET /me/rewards`: return current points and reward event history.
|
|
||||||
|
|
||||||
## Admin Reference Data
|
|
||||||
- `GET|POST /admin/categories`
|
|
||||||
- `GET|POST /admin/areas`
|
|
||||||
- `GET|POST /admin/teams`
|
|
||||||
- `GET /admin/stats/overview`
|
|
||||||
- `GET /admin/moderation`: list moderation flags with optional `status`.
|
|
||||||
- `PATCH /admin/moderation/{id}`: resolve or dismiss a moderation flag, optionally
|
|
||||||
rejecting the related report.
|
|
||||||
- `GET /admin/users`
|
|
||||||
- `PATCH /admin/users/{id}/roles`
|
|
||||||
- `PATCH /admin/users/{id}/ban`
|
|
||||||
|
|
||||||
## Public Data
|
|
||||||
- `GET /open-data/reports`: anonymized resolved/closed report export.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# Visione Backend Architecture
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
- `controllers/`: Drogon `HttpController` classes.
|
|
||||||
- `services/`: application/business logic.
|
|
||||||
- `repositories/`: database helpers and migrations.
|
|
||||||
- `common/`: reusable HTTP, auth, crypto, email and validation utilities.
|
|
||||||
- `migrations/`: versioned SQL files.
|
|
||||||
- `scripts/generate_openapi.py`: minimal OpenAPI route extractor.
|
|
||||||
- `docs/openapi/`: manual OpenAPI contract documents.
|
|
||||||
- `scripts/lab/`: local Streamlit frontend for visual endpoint testing.
|
|
||||||
- `docs/`: product, API, schema and milestone documentation.
|
|
||||||
|
|
||||||
## Drogon Usage
|
|
||||||
Controllers use Drogon route macros and return JSON with
|
|
||||||
`HttpResponse::newHttpJsonResponse`. Database access uses
|
|
||||||
`drogon::orm::DbClient` with raw parameterized SQL placeholders.
|
|
||||||
|
|
||||||
The codebase does not use Drogon generated models or `Mapper<T>`. Raw SQL stays
|
|
||||||
in repository/database helpers because the schema is still evolving.
|
|
||||||
|
|
||||||
## Local Email
|
|
||||||
OTP emails are sent to MailHog over SMTP. MailHog keeps messages local and
|
|
||||||
inspectable through its web UI.
|
|
||||||
|
|
||||||
## Password Hashing
|
|
||||||
User passwords use PBKDF2-HMAC-SHA256 with per-password random salts and 210000
|
|
||||||
iterations. This is intentionally slower than a plain SHA-256 digest and is
|
|
||||||
available through OpenSSL, which is already a Drogon dependency in this project.
|
|
||||||
Legacy `sha256$` password hashes are rejected instead of silently accepted.
|
|
||||||
|
|
||||||
## OpenAPI Generation
|
|
||||||
`scripts/generate_openapi.py` scans Drogon `ADD_METHOD_TO` route macros in
|
|
||||||
`controllers/*.h` and extracts only paths, HTTP methods, operation ids, tags and
|
|
||||||
path parameters. It merges those route skeletons with
|
|
||||||
`docs/openapi/visione.openapi.yaml`, which owns summaries, request bodies,
|
|
||||||
responses, schemas and examples. The manual file uses JSON-compatible YAML so
|
|
||||||
the CMake `openapi` target can run without extra Python packages and writes
|
|
||||||
`build/openapi/visione.openapi.json`.
|
|
||||||
|
|
||||||
If the manual YAML contains an operation that no longer exists in the Drogon
|
|
||||||
route list, generation fails unless that operation is explicitly marked
|
|
||||||
`x-manual-only: true`.
|
|
||||||
|
|
||||||
## Visione Lab
|
|
||||||
`scripts/lab/` is a Streamlit dev frontend for visual endpoint testing. It is
|
|
||||||
intentionally separate from the real product frontend and should stay small:
|
|
||||||
forms, uploads and response rendering only. Run it with uv from `scripts/lab/`.
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# Visione Database Schema
|
|
||||||
|
|
||||||
The schema is stored in ordered SQL migrations under `migrations/`.
|
|
||||||
|
|
||||||
Persistence currently uses `drogon::orm::DbClient` with raw parameterized SQL.
|
|
||||||
The project does not use Drogon generated models or `Mapper<T>` while the schema
|
|
||||||
is still evolving.
|
|
||||||
|
|
||||||
Important tables:
|
|
||||||
|
|
||||||
- `users`: identity, personal data, password hash, points and moderation flags.
|
|
||||||
- `roles`, `permissions`, `role_permissions`, `user_roles`: authorization.
|
|
||||||
- `otp_challenges`: local OTP login flow.
|
|
||||||
- `api_tokens`: bearer tokens for the local API.
|
|
||||||
- `report_categories`: report classification.
|
|
||||||
- `areas`: named city areas with GeoJSON polygon boundaries.
|
|
||||||
- `team_categories`, `teams`: municipal intervention teams.
|
|
||||||
- `reports`: citizen reports with coordinates, address, status, area and assigned operator.
|
|
||||||
- `report_status_history`: immutable status transition log.
|
|
||||||
- `assignments`: report-to-team assignment history.
|
|
||||||
- `report_photos`: uploaded report media URL, original filename, content type,
|
|
||||||
media kind and size. The table name is historical; rows can be images or videos.
|
|
||||||
- `messages`: citizen/operator report conversation.
|
|
||||||
- `feedback`: citizen rating and comment after resolution.
|
|
||||||
- `reward_events`: indirect gamification points ledger.
|
|
||||||
- `notifications`: local in-app notifications for status changes, messages and
|
|
||||||
reward events.
|
|
||||||
- `moderation_flags`: report/message review queue for abusive or invalid content.
|
|
||||||
|
|
||||||
GeoJSON is intentionally used instead of MySQL Spatial in milestone 1 to keep
|
|
||||||
the local setup simple and portable.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Visione Backend LLM Context
|
|
||||||
|
|
||||||
Visione is a C++20/Drogon backend for a municipal urban issue reporting system.
|
|
||||||
Citizens create reports with category, position, address, description and media
|
|
||||||
attachments.
|
|
||||||
Operators manage report queues, assign teams, update status, chat with citizens
|
|
||||||
and use intervention summaries.
|
|
||||||
Admins manage roles, users, areas, teams, reference data, moderation and stats.
|
|
||||||
|
|
||||||
The project runs locally on the host machine. Docker is used only for external
|
|
||||||
services such as MySQL and MailHog. Real production deployment and Dockerizing
|
|
||||||
the backend are later delivery tasks.
|
|
||||||
|
|
||||||
Important conventions:
|
|
||||||
|
|
||||||
- Code and database identifiers are English.
|
|
||||||
- The webapp is Italian-facing, so labels or seeded category names may be Italian.
|
|
||||||
- Use Drogon references from `/home/gabdevele/Projects/snitchitaly/backend/drogon`
|
|
||||||
and `https://drogonframework.github.io/drogon-docs/`.
|
|
||||||
- `AGENTS.md` is the durable agent instruction entrypoint.
|
|
||||||
- `docs/MILESTONE_01.md`, `docs/MILESTONE_02.md`, `docs/MILESTONE_03.md` and
|
|
||||||
`docs/MILESTONE_04.md` describe what has been implemented and what comes next.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Milestone 01: Local Backend Foundation
|
|
||||||
|
|
||||||
## Implemented in this milestone
|
|
||||||
- Project identity and documentation for Visione.
|
|
||||||
- Root `AGENTS.md` for future coding agents.
|
|
||||||
- Local Docker services for MySQL and MailHog.
|
|
||||||
- Initial MySQL schema and seed data.
|
|
||||||
- Drogon backend structure for auth, reports, admin data and stats.
|
|
||||||
- OTP email delivery through MailHog.
|
|
||||||
- Focused regression tests for technical endpoints and core validation helpers.
|
|
||||||
- Local admin seed: `[email protected]` / `ChangeMe123!`, with forced password change flag.
|
|
||||||
- Multipart report media upload endpoint storing files under `uploads/reports/`.
|
|
||||||
- PBKDF2-HMAC-SHA256 password hashes for credentials. Legacy SHA-256 hashes are intentionally rejected.
|
|
||||||
|
|
||||||
## To do next
|
|
||||||
- Consider Argon2id/bcrypt and password reset flows before production hardening.
|
|
||||||
- Add real email provider configuration for non-local environments.
|
|
||||||
- Add CIE/SPID integration design and provider adapters.
|
|
||||||
- Expand frontend-driven API coverage as Vue screens are implemented.
|
|
||||||
- Add stronger frontend-driven endpoint coverage as Vue screens are implemented.
|
|
||||||
- Add notification channels, route optimization and open-data exports.
|
|
||||||
- Dockerize the backend only for final delivery/deployment.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Milestone 02: Workflow Core and Hardening
|
|
||||||
|
|
||||||
## Implemented in this milestone
|
|
||||||
- Documentation/config fixes from Milestone 01 review.
|
|
||||||
- Authorization context with roles, permissions and forced password-change state.
|
|
||||||
- Authenticated password change endpoint.
|
|
||||||
- Admin user listing, role assignment and ban/unban endpoints.
|
|
||||||
- Report ownership workflow with assigned operator and take endpoint.
|
|
||||||
- Valid report status transitions.
|
|
||||||
- Citizen report filters scoped to own reports.
|
|
||||||
- Report media upload validation and metadata storage.
|
|
||||||
|
|
||||||
## To do next
|
|
||||||
- Add broader API tests once the frontend contracts stabilize.
|
|
||||||
- Add notification delivery for status changes.
|
|
||||||
- Add moderation flow for abusive reports/messages.
|
|
||||||
- Add open-data exports and statistics refinements.
|
|
||||||
- Design CIE/SPID integration.
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Milestone 03: Media, OpenAPI and API Lab
|
|
||||||
|
|
||||||
## Implemented in this milestone
|
|
||||||
- Removed legacy `sha256$` password verification and updated local admin seeds to
|
|
||||||
PBKDF2-HMAC-SHA256.
|
|
||||||
- Documented why PBKDF2-HMAC-SHA256 is used now: salted, intentionally slower
|
|
||||||
than plain SHA-256 and available through the existing OpenSSL dependency.
|
|
||||||
- Added report media uploads at `POST /api/v1/reports/{id}/media`.
|
|
||||||
- Supported JPEG, PNG, WebP, MP4, WebM and QuickTime uploads.
|
|
||||||
- Report attachments now use the `/media` upload path.
|
|
||||||
- Added generated OpenAPI JSON from Drogon route macros plus the manual
|
|
||||||
`docs/openapi/visione.openapi.yaml` contract.
|
|
||||||
- Exposed `/api/v1/openapi.json` and `/api/v1/docs`.
|
|
||||||
- Added a small Streamlit API lab for visual endpoint testing.
|
|
||||||
|
|
||||||
## To do next
|
|
||||||
- Expand the manual OpenAPI contract as endpoints evolve.
|
|
||||||
- Add minimal media upload integration tests after endpoint contracts settle.
|
|
||||||
- Keep API and docs on `media` terminology only.
|
|
||||||
- Keep the Streamlit lab small and replace it with the real frontend later.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Milestone 04: Backend Completion
|
|
||||||
|
|
||||||
## Implemented in this milestone
|
|
||||||
- Added operator queues for assigned reports, open unassigned reports and reports
|
|
||||||
assigned to other operators.
|
|
||||||
- Added an operator intervention summary with report data, citizen contact data,
|
|
||||||
media and latest messages.
|
|
||||||
- Added local in-app notifications for status changes, team assignment, report
|
|
||||||
messages and reward events.
|
|
||||||
- Added citizen reward ledger APIs and automatic points for resolved reports.
|
|
||||||
- Added report/message moderation flags with admin/operator review and optional
|
|
||||||
report rejection.
|
|
||||||
- Expanded admin statistics with category, team, monthly and segmented
|
|
||||||
resolution-time aggregates.
|
|
||||||
- Added anonymized public open-data report export.
|
|
||||||
- Updated the manual OpenAPI contract for the new backend routes.
|
|
||||||
|
|
||||||
## Intentionally not implemented
|
|
||||||
- Real CIE/SPID integration.
|
|
||||||
- Real SMS, Telegram, WhatsApp or production SMTP notification delivery.
|
|
||||||
- Route optimization for intervention teams.
|
|
||||||
- Frontend-specific pages outside the Streamlit API lab.
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
# Visione Project Spec
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
Visione lets citizens report urban problems and lets municipal staff manage,
|
|
||||||
assign and resolve interventions while preserving history and basic analytics.
|
|
||||||
|
|
||||||
## Roles
|
|
||||||
- `citizen`: registers, requests OTP login, creates reports, follows own reports,
|
|
||||||
sends messages and leaves feedback after resolution.
|
|
||||||
- `operator`: views reports, takes open reports, assigns teams, updates managed
|
|
||||||
reports and messages citizens.
|
|
||||||
- `admin`: manages users, roles, reference data, teams, areas and all reports.
|
|
||||||
|
|
||||||
The system admin seed must change password before using protected operational
|
|
||||||
or administrative APIs.
|
|
||||||
|
|
||||||
## Report Statuses
|
|
||||||
- `OPEN`: created and waiting for review.
|
|
||||||
- `UNDER_REVIEW`: an operator is checking the report.
|
|
||||||
- `ASSIGNED`: a team has been assigned.
|
|
||||||
- `IN_PROGRESS`: intervention is active.
|
|
||||||
- `RESOLVED`: intervention was completed.
|
|
||||||
- `REJECTED`: report was invalid or abusive.
|
|
||||||
- `CLOSED`: administrative closure after feedback or final review.
|
|
||||||
|
|
||||||
Valid milestone 2 transitions:
|
|
||||||
|
|
||||||
- `OPEN -> UNDER_REVIEW` when an operator/admin takes the report.
|
|
||||||
- `UNDER_REVIEW -> ASSIGNED` when a team is assigned.
|
|
||||||
- `ASSIGNED -> IN_PROGRESS`.
|
|
||||||
- `IN_PROGRESS -> RESOLVED`.
|
|
||||||
- `RESOLVED -> CLOSED`.
|
|
||||||
- `OPEN/UNDER_REVIEW/ASSIGNED/IN_PROGRESS -> REJECTED` only for invalid reports.
|
|
||||||
- `REJECTED -> CLOSED`.
|
|
||||||
|
|
||||||
## MVP Scope
|
|
||||||
- Local auth with email OTP delivered through MailHog.
|
|
||||||
- MySQL persistence with migrations and seed data.
|
|
||||||
- Report creation/list/detail/status/message/assignment/feedback/moderation flows.
|
|
||||||
- Area polygons stored as GeoJSON and matched by backend logic.
|
|
||||||
- Operator queues for assigned, open and other-operator reports.
|
|
||||||
- Local in-app notifications and reward points ledger.
|
|
||||||
- Public anonymized open-data report export.
|
|
||||||
- Basic and segmented stats for administration.
|
|
||||||
|
|
||||||
## Out of Scope for the Local Backend MVP
|
|
||||||
- Real CIE integration.
|
|
||||||
- Real SMTP provider, SMS, Telegram or WhatsApp notifications.
|
|
||||||
- Route optimization.
|
|
||||||
- Full Docker packaging of the backend.
|
|
||||||
-168
@@ -1,168 +0,0 @@
|
|||||||
Traccia principale del progetto:
|
|
||||||
Caso di studio
|
|
||||||
Segnalazioni urbane
|
|
||||||
Un’amministrazione comunale desidera sviluppare un sistema digitale per permettere ai cittadini di segnalare problemi presenti sul territorio, come buche stradali, illuminazione guasta, rifiuti abbandonati o altri disservizi urbani, con l’obiettivo di migliorare la gestione degli interventi e la qualità della vita in città.
|
|
||||||
|
|
||||||
Il sistema deve prevedere due principali tipologie di utenti: da un lato i cittadini, che inviano segnalazioni, dall’altro gli operatori comunali, responsabili della gestione e risoluzione degli interventi.
|
|
||||||
|
|
||||||
I cittadini devono potersi registrare e accedere tramite credenziali personali. Una volta autenticati, devono poter inserire una nuova segnalazione specificando il tipo di problema, la posizione e una breve descrizione. Il sistema deve memorizzare le segnalazioni e permettere ai cittadini di visualizzarne lo stato nel tempo, ad esempio in attesa, in lavorazione o risolta.
|
|
||||||
|
|
||||||
Gli utenti devono inoltre poter consultare le segnalazioni già presenti, eventualmente filtrandole per categoria o area, e fornire un feedback sull’efficacia degli interventi effettuati.
|
|
||||||
|
|
||||||
Dal lato degli operatori comunali, il sistema deve consentire di visualizzare tutte le segnalazioni ricevute, assegnarle a specifiche squadre di intervento e aggiornarne lo stato man mano che vengono gestite. Deve essere possibile monitorare i tempi di risposta e individuare le aree della città con maggior concentrazione di problemi.
|
|
||||||
|
|
||||||
Il sistema deve mantenere uno storico delle segnalazioni e degli interventi effettuati, permettendo all’amministrazione di analizzare i dati raccolti per pianificare interventi futuri e migliorare l’efficienza dei servizi.
|
|
||||||
|
|
||||||
Dopodichè ho elaborato una serie di feature che il sistema deve implementare anche rielaborazione e idee. ovvero:
|
|
||||||
Feature Segnalazioni urbane
|
|
||||||
– Lo sviluppo del della webapp avverrà su una istanza self hosted di Gitea come gesto di ribellione nei confronti di github che userà i dati sulle repo per trainare i modelli di IA (lo faceva già prima ma ora lo dice)
|
|
||||||
– Il progetto sarà diviso tra frontend e backend. Frontend in probabilmente Vue.js e backend in C++, nello specifico Dragon è stato scelto come framework. Abbiamo scelto di sviluppare una webapp perchè il punto importante di un progetto del genere è la versatilità, la compatibilità su tutti i sistemi e la velocità.
|
|
||||||
– Deve esistere un account amministratore generato dal sistema, appena ci si logga la prima volta ci chiederà di cambiare password, e questo profilo lo dovrà teoricamente avere solo chi il tecnico che gestisce la piattaforma di quel comune, dato che da questo profilo si può fare tutto. E’ diverso da un account operatore perchè l’operatore ha meno permessi rispetto ad un admin, l’admin può decidere chi sono gli operatori. (NOTA: eventualmente ci possono essere i permessi customizzabili, quindi l’admin può creare anche altri utenti che hanno permessi in più)
|
|
||||||
– Deve essere possibile registrarsi alla piattaforma con tutti i quanti i dati anagrafici possibili e immaginabili, sarebbe figo avere un sistema che dato il codice fiscale ti auto completa i dati deducibili da quest’ultimo. Integrazione frontend con google maps per l’indirizzo. Deve essere possibile anche collegare CIE. Nella registrazione si richiede verifica a due fattori
|
|
||||||
– Il login deve essere effettuato con email o CIE. Nel caso di email la modalità di default non richiede una password ma un OTP, in quanto dimostrato essere più sicuro. (La password è comunque obbligatoria da settare)
|
|
||||||
– Una volta registrati, se siamo un operatore (comunale, poliziotto, pompiere o chiunque) sarà un admin o un’altro operatore con abbastanza permessi ad assegnarci a quel ruolo e sbloccare i permessi per accedere ad altre parti della piattaforma.
|
|
||||||
– Admin/operatori vari devono poter creare le varie zone in base al perimetro, se questo è fattibile tramite una mappa sarebbe l’ideale, assegnare i vari nomi ecc.. Questo serve sia per le segnalazioni e per l’assegnazione dell’agente/squadra che deve intervenire sia per fini statistici
|
|
||||||
– Al sito non si registrano solo gli agenti che intervengono o gli operatori ma anche i cittadini, che possono effettuare delle segnalazioni. Le segnalazioni devono avere una foto, la posizione con coordinate geografiche e via con numero. Il sistema automaticamente capisce a che zona del comune fa parte
|
|
||||||
– Quando viene creata una segnalazione c’è un indicatore dello stato, tipo: APERTA, IN REVISIONE, AGENTI IN ARRIVO, RISOLTA (sono esempi, vanno migliorati). Di default viene assegnato APERTA.
|
|
||||||
– Nella segnalazione c’è una chat che viene fatta tra l’operatore e il cittadino per fare eventuali domande. L’operatore quindi può aggiornare lo stato, scrivere messaggi, segnalare il cittadino perchè magari ha inviato un pene come segnalazione.
|
|
||||||
– L’agente riceverà un summary della segnalazione + numero di telefono di chi ha segnalato, dato che intervengono sul posto non serve che loro chattano, chiamano direttamente.
|
|
||||||
– La webapp deve avere una pagina con una serie di statistiche
|
|
||||||
– Si può implementare anche una specie di gamification (indiretta, non diciamolo in maniera ovvia nel sito) del sistema per renderlo più interattivo, tipo ogni segnalazione valida vale dei punti che se accumulati portano a buoni / sconti.
|
|
||||||
– L’utente e gli operatori devono poter aggiungere anche dei filtri quando si cercano le varie segnalazioni. Inoltre il cittadino deve anche poter lasciare un feedback sull’intervento che hanno fatto, questo sarà collegato quindi anche all’operatore e agli agenti della squadra che sono intervenuti.
|
|
||||||
– L’operatore deve poter vedere le varie segnalazioni che sta gestendo e anche quelle di altri operatori ma senza poterci interagire / modificarle.
|
|
||||||
– Tutti vedono e possono prendere in carico le segnalazioni aperte.
|
|
||||||
|
|
||||||
In aggiunta come feature opzionali e secondarie sarebbe carino implementare:
|
|
||||||
- Notifiche Multicanale: Obbligare il cittadino a rientrare nell'app per controllare lo stato della segnalazione genera frustrazione. Potresti implementare notifiche via Email o l'integrazione con un Bot Telegram/WhatsApp che avvisa automaticamente l'utente quando lo stato passa da "In Lavorazione" a "Risolta".
|
|
||||||
- Routing Intelligente: Sfruttando le API delle mappe, il sistema potrebbe calcolare per la squadra operativa il percorso ottimale per risolvere 4 o 5 segnalazioni assegnate nella stessa giornata, minimizzando i tempi di spostamento
|
|
||||||
- Portale Open Data: Le amministrazioni amano promuovere la trasparenza. Potresti creare una pagina pubblica dove chiunque (cittadini, giornalisti, ricercatori) può scaricare un dataset anonimizzato con l'elenco dei disservizi risolti nell'ultimo anno, esportabile in formati standard come JSON o CSV.
|
|
||||||
|
|
||||||
Un mio collega ha definito questo schema per il database, possiamo e dobbiamo sicuramente modificarlo ma può essere un buon punto di partenza:
|
|
||||||
erDiagram
|
|
||||||
UTENTE ||--o{ SEGNALAZIONE : "manda"
|
|
||||||
%%citatdino
|
|
||||||
UTENTE ||--o{ STORICO_STATO : "aggiorna"
|
|
||||||
%%operatore
|
|
||||||
UTENTE ||--o{ ASSEGNAZIONE : "assegna "
|
|
||||||
%%operatore
|
|
||||||
UTENTE ||--o{ FEEDBACK : "lascia "
|
|
||||||
%%citatdino
|
|
||||||
UTENTE ||--o{ MESSAGGIO : "scrive"
|
|
||||||
|
|
||||||
CATEGORIA_SEGNALAZIONE ||--o{ SEGNALAZIONE : "classifica"
|
|
||||||
AREA ||--o{ SEGNALAZIONE : "localizza"
|
|
||||||
AREA ||--o{ SQUADRA : "ha"
|
|
||||||
%%che relaziobne diu merda
|
|
||||||
|
|
||||||
CATEGORIA_SQUADRA ||--o{ SQUADRA : "ha"
|
|
||||||
|
|
||||||
SEGNALAZIONE ||--o{ STORICO_STATO : "traccia"
|
|
||||||
SEGNALAZIONE ||--o{ ASSEGNAZIONE : "riceve"
|
|
||||||
SEGNALAZIONE ||--o| FEEDBACK : "valutata"
|
|
||||||
SEGNALAZIONE ||--o{ FOTO_SEGNALAZIONE : "documentata"
|
|
||||||
SEGNALAZIONE ||--o{ MESSAGGIO : "discussa"
|
|
||||||
|
|
||||||
SQUADRA ||--o{ ASSEGNAZIONE : "incaricata"
|
|
||||||
|
|
||||||
UTENTE {
|
|
||||||
int id PK
|
|
||||||
string nome
|
|
||||||
string cognome
|
|
||||||
string email UK
|
|
||||||
string password_hash
|
|
||||||
string codice_fiscale UK
|
|
||||||
date data_nascita
|
|
||||||
string luogo_nascita
|
|
||||||
enum ruolo
|
|
||||||
string telefono
|
|
||||||
datetime data_registrazione
|
|
||||||
int punti
|
|
||||||
boolean bannato
|
|
||||||
}
|
|
||||||
|
|
||||||
CATEGORIA_SEGNALAZIONE {
|
|
||||||
int id PK
|
|
||||||
string nome
|
|
||||||
text descrizione
|
|
||||||
}
|
|
||||||
|
|
||||||
AREA {
|
|
||||||
int id PK
|
|
||||||
string nome_quartiere
|
|
||||||
text descrizione
|
|
||||||
geometry coordinate_poligono
|
|
||||||
%%bho
|
|
||||||
}
|
|
||||||
|
|
||||||
CATEGORIA_SQUADRA {
|
|
||||||
int id PK
|
|
||||||
string nome
|
|
||||||
text descrizione
|
|
||||||
}
|
|
||||||
|
|
||||||
SEGNALAZIONE {
|
|
||||||
int id PK
|
|
||||||
int utente_id FK
|
|
||||||
int categoria_id FK
|
|
||||||
int area_id FK
|
|
||||||
decimal latitudine
|
|
||||||
decimal longitudine
|
|
||||||
string indirizzo
|
|
||||||
text descrizione
|
|
||||||
enum stato
|
|
||||||
datetime data_creazione
|
|
||||||
datetime data_ultima_modifica
|
|
||||||
}
|
|
||||||
|
|
||||||
STORICO_STATO {
|
|
||||||
int id PK
|
|
||||||
int segnalazione_id FK
|
|
||||||
datetime data
|
|
||||||
int operatore_id FK
|
|
||||||
text note
|
|
||||||
}
|
|
||||||
|
|
||||||
SQUADRA {
|
|
||||||
int id PK
|
|
||||||
int categoria_squadra_id FK
|
|
||||||
int area_id FK
|
|
||||||
string nome
|
|
||||||
text descrizione
|
|
||||||
boolean attiva
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ASSEGNAZIONE {
|
|
||||||
int id PK
|
|
||||||
int segnalazione_id FK
|
|
||||||
int squadra_id FK
|
|
||||||
int operatore_assegnante_id FK
|
|
||||||
datetime data_assegnazione
|
|
||||||
datetime data_cessazione
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
FEEDBACK {
|
|
||||||
int id PK
|
|
||||||
int segnalazione_id FK, UK
|
|
||||||
int utente_id FK
|
|
||||||
int voto
|
|
||||||
%%??
|
|
||||||
text commento
|
|
||||||
datetime data
|
|
||||||
}
|
|
||||||
|
|
||||||
FOTO_SEGNALAZIONE {
|
|
||||||
int id PK
|
|
||||||
int segnalazione_id FK
|
|
||||||
string url
|
|
||||||
datetime data_upload
|
|
||||||
}
|
|
||||||
|
|
||||||
MESSAGGIO {
|
|
||||||
int id PK
|
|
||||||
int segnalazione_id FK
|
|
||||||
int utente_id FK
|
|
||||||
text testo
|
|
||||||
datetime data_invio
|
|
||||||
}
|
|
||||||
+925
-241
File diff suppressed because it is too large
Load Diff
@@ -5,16 +5,100 @@
|
|||||||
#include "repositories/MigrationRunner.h"
|
#include "repositories/MigrationRunner.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool parseEnvBool(const char *value)
|
||||||
|
{
|
||||||
|
if (value == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string_view normalized(value);
|
||||||
|
return normalized == "1" || normalized == "true" || normalized == "TRUE" ||
|
||||||
|
normalized == "yes" || normalized == "on";
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
setvbuf(stdout, nullptr, _IONBF, 0);
|
setvbuf(stdout, nullptr, _IONBF, 0);
|
||||||
(void)RequestLogger::classTypeName();
|
(void)RequestLogger::classTypeName();
|
||||||
|
|
||||||
|
Json::Value config;
|
||||||
|
std::ifstream configFile("config.json");
|
||||||
|
if (configFile.is_open())
|
||||||
|
{
|
||||||
|
configFile >> config;
|
||||||
|
|
||||||
|
// DB config from env
|
||||||
|
if (const char* envHost = std::getenv("DB_HOST"))
|
||||||
|
config["db_clients"][0]["host"] = envHost;
|
||||||
|
if (const char* envPort = std::getenv("DB_PORT"))
|
||||||
|
config["db_clients"][0]["port"] = std::stoi(envPort);
|
||||||
|
if (const char* envUser = std::getenv("DB_USER"))
|
||||||
|
config["db_clients"][0]["user"] = envUser;
|
||||||
|
if (const char* envPass = std::getenv("DB_PASSWORD"))
|
||||||
|
config["db_clients"][0]["passwd"] = envPass;
|
||||||
|
if (const char* envDb = std::getenv("DB_NAME"))
|
||||||
|
config["db_clients"][0]["dbname"] = envDb;
|
||||||
|
if (const char* envTimeout = std::getenv("DB_TIMEOUT"))
|
||||||
|
config["db_clients"][0]["timeout"] = std::stod(envTimeout);
|
||||||
|
|
||||||
|
// Mail config from env
|
||||||
|
if (const char* envMailProvider = std::getenv("MAIL_PROVIDER"))
|
||||||
|
config["custom_config"]["mail"]["provider"] = envMailProvider;
|
||||||
|
if (const char* envSmtpHost = std::getenv("SMTP_HOST"))
|
||||||
|
config["custom_config"]["mail"]["host"] = envSmtpHost;
|
||||||
|
if (const char* envSmtpPort = std::getenv("SMTP_PORT"))
|
||||||
|
config["custom_config"]["mail"]["port"] = std::stoi(envSmtpPort);
|
||||||
|
if (const char* envSmtpUsername = std::getenv("SMTP_USERNAME"))
|
||||||
|
config["custom_config"]["mail"]["username"] = envSmtpUsername;
|
||||||
|
if (const char* envSmtpPassword = std::getenv("SMTP_PASSWORD"))
|
||||||
|
config["custom_config"]["mail"]["password"] = envSmtpPassword;
|
||||||
|
else if (const char* envResendApiKey = std::getenv("RESEND_API_KEY"))
|
||||||
|
config["custom_config"]["mail"]["password"] = envResendApiKey;
|
||||||
|
if (const char* envSmtpUseTls = std::getenv("SMTP_USE_TLS"))
|
||||||
|
config["custom_config"]["mail"]["use_tls"] = parseEnvBool(envSmtpUseTls);
|
||||||
|
if (const char* envSmtpUseStartTls = std::getenv("SMTP_USE_STARTTLS"))
|
||||||
|
config["custom_config"]["mail"]["use_starttls"] = parseEnvBool(envSmtpUseStartTls);
|
||||||
|
if (const char* envSmtpVerifyPeer = std::getenv("SMTP_VERIFY_PEER"))
|
||||||
|
config["custom_config"]["mail"]["verify_peer"] = parseEnvBool(envSmtpVerifyPeer);
|
||||||
|
if (const char* envSmtpVerifyHost = std::getenv("SMTP_VERIFY_HOST"))
|
||||||
|
config["custom_config"]["mail"]["verify_host"] = parseEnvBool(envSmtpVerifyHost);
|
||||||
|
if (const char* envMailFrom = std::getenv("MAIL_FROM"))
|
||||||
|
config["custom_config"]["mail"]["from"] = envMailFrom;
|
||||||
|
|
||||||
|
// CORS config from env
|
||||||
|
if (const char* envCorsAllowedOrigin = std::getenv("CORS_ALLOWED_ORIGIN"))
|
||||||
|
config["custom_config"]["cors"]["allowed_origin"] = envCorsAllowedOrigin;
|
||||||
|
|
||||||
|
// CIE auth config from env
|
||||||
|
if (const char* envCieServiceBaseUrl = std::getenv("CIE_AUTH_SERVICE_BASE_URL"))
|
||||||
|
config["custom_config"]["cie_auth"]["service_base_url"] = envCieServiceBaseUrl;
|
||||||
|
if (const char* envCieStartPath = std::getenv("CIE_AUTH_START_PATH"))
|
||||||
|
config["custom_config"]["cie_auth"]["start_path"] = envCieStartPath;
|
||||||
|
if (const char* envCieSharedSecret = std::getenv("CIE_AUTH_SHARED_SECRET"))
|
||||||
|
config["custom_config"]["cie_auth"]["shared_secret"] = envCieSharedSecret;
|
||||||
|
if (const char* envCieStateTtl = std::getenv("CIE_AUTH_STATE_TTL_MINUTES"))
|
||||||
|
config["custom_config"]["cie_auth"]["state_ttl_minutes"] = std::stoi(envCieStateTtl);
|
||||||
|
if (const char* envCieFrontendRedirect = std::getenv("CIE_AUTH_DEFAULT_FRONTEND_REDIRECT_URI"))
|
||||||
|
config["custom_config"]["cie_auth"]["default_frontend_redirect_uri"] = envCieFrontendRedirect;
|
||||||
|
|
||||||
|
drogon::app().loadConfigJson(config);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
drogon::app().loadConfigFile("config.json");
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value requestLoggerConfig;
|
Json::Value requestLoggerConfig;
|
||||||
requestLoggerConfig["include_query"] = true;
|
requestLoggerConfig["include_query"] = true;
|
||||||
|
|
||||||
drogon::app().loadConfigFile("config.json");
|
|
||||||
drogon::app().addPlugin("RequestLogger", {}, requestLoggerConfig);
|
drogon::app().addPlugin("RequestLogger", {}, requestLoggerConfig);
|
||||||
visione::cors::configure();
|
visione::cors::configure();
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
first_name VARCHAR(100) NOT NULL,
|
first_name VARCHAR(100) NOT NULL,
|
||||||
last_name VARCHAR(100) NOT NULL,
|
last_name VARCHAR(100) NOT NULL,
|
||||||
email VARCHAR(255) NOT NULL,
|
email VARCHAR(255) NOT NULL,
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
|
||||||
fiscal_code VARCHAR(16) NOT NULL,
|
fiscal_code VARCHAR(16) NOT NULL,
|
||||||
birth_date DATE NULL,
|
birth_date DATE NULL,
|
||||||
birth_place VARCHAR(150) NULL,
|
birth_place VARCHAR(150) NULL,
|
||||||
phone VARCHAR(30) NULL,
|
phone VARCHAR(30) NULL,
|
||||||
points INT NOT NULL DEFAULT 0,
|
points INT NOT NULL DEFAULT 0,
|
||||||
banned BOOLEAN NOT NULL DEFAULT FALSE,
|
banned BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
must_change_password BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY users_email_unique (email),
|
UNIQUE KEY users_email_unique (email),
|
||||||
@@ -82,7 +80,6 @@ CREATE TABLE IF NOT EXISTS areas (
|
|||||||
name VARCHAR(120) NOT NULL,
|
name VARCHAR(120) NOT NULL,
|
||||||
description TEXT NULL,
|
description TEXT NULL,
|
||||||
boundary_geojson JSON NOT NULL,
|
boundary_geojson JSON NOT NULL,
|
||||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
UNIQUE KEY areas_name_unique (name)
|
UNIQUE KEY areas_name_unique (name)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -102,7 +99,7 @@ CREATE TABLE IF NOT EXISTS teams (
|
|||||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
UNIQUE KEY teams_name_unique (name),
|
UNIQUE KEY teams_name_unique (name),
|
||||||
CONSTRAINT teams_category_fk FOREIGN KEY (team_category_id) REFERENCES team_categories(id),
|
CONSTRAINT teams_category_fk FOREIGN KEY (team_category_id) REFERENCES team_categories(id),
|
||||||
CONSTRAINT teams_area_fk FOREIGN KEY (area_id) REFERENCES areas(id)
|
CONSTRAINT teams_area_fk FOREIGN KEY (area_id) REFERENCES areas(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS reports (
|
CREATE TABLE IF NOT EXISTS reports (
|
||||||
@@ -119,7 +116,7 @@ CREATE TABLE IF NOT EXISTS reports (
|
|||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
CONSTRAINT reports_user_fk FOREIGN KEY (user_id) REFERENCES users(id),
|
CONSTRAINT reports_user_fk FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
CONSTRAINT reports_category_fk FOREIGN KEY (category_id) REFERENCES report_categories(id),
|
CONSTRAINT reports_category_fk FOREIGN KEY (category_id) REFERENCES report_categories(id),
|
||||||
CONSTRAINT reports_area_fk FOREIGN KEY (area_id) REFERENCES areas(id),
|
CONSTRAINT reports_area_fk FOREIGN KEY (area_id) REFERENCES areas(id) ON DELETE SET NULL,
|
||||||
KEY reports_status_idx (status),
|
KEY reports_status_idx (status),
|
||||||
KEY reports_category_idx (category_id),
|
KEY reports_category_idx (category_id),
|
||||||
KEY reports_area_idx (area_id)
|
KEY reports_area_idx (area_id)
|
||||||
@@ -224,20 +221,16 @@ INSERT IGNORE INTO users (
|
|||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
email,
|
email,
|
||||||
password_hash,
|
|
||||||
fiscal_code,
|
fiscal_code,
|
||||||
phone,
|
phone
|
||||||
must_change_password
|
|
||||||
) VALUES (
|
) VALUES (
|
||||||
'System',
|
'Vito',
|
||||||
'Admin',
|
'Leccese',
|
||||||
'admin@visione.local',
|
'admin@visionebari.it',
|
||||||
'pbkdf2_sha256$210000$local-dev-admin-salt$A4683BDB63C1598400B6B196F53C830F8434BA645C4A4A70AB311B278223E698',
|
'LCCVTI62M02A662Q',
|
||||||
'ADMINVISIONE0000',
|
'380 6180216'
|
||||||
'+390000000000',
|
|
||||||
TRUE
|
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT IGNORE INTO user_roles (user_id, role_id)
|
INSERT IGNORE INTO user_roles (user_id, role_id)
|
||||||
SELECT u.id, r.id FROM users u JOIN roles r
|
SELECT u.id, r.id FROM users u JOIN roles r
|
||||||
WHERE u.email = 'admin@visione.local' AND r.name = 'admin';
|
WHERE u.email = 'admin@visionebari.it' AND r.name = 'admin';
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
UPDATE users
|
UPDATE users
|
||||||
SET password_hash = 'pbkdf2_sha256$210000$local-dev-admin-salt$A4683BDB63C1598400B6B196F53C830F8434BA645C4A4A70AB311B278223E698',
|
SET email = '[email protected]',
|
||||||
must_change_password = TRUE
|
first_name = 'Vito',
|
||||||
WHERE email = '[email protected]';
|
last_name = 'Leccese',
|
||||||
|
fiscal_code = 'LCCVTI62M02A662Q',
|
||||||
|
phone = '380 6180216'
|
||||||
|
WHERE email = '[email protected]';
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
UPDATE users
|
UPDATE users
|
||||||
SET password_hash = 'pbkdf2_sha256$210000$local-dev-admin-salt$A4683BDB63C1598400B6B196F53C830F8434BA645C4A4A70AB311B278223E698',
|
SET email = '[email protected]',
|
||||||
must_change_password = TRUE
|
first_name = 'Vito',
|
||||||
WHERE email = '[email protected]';
|
last_name = 'Leccese',
|
||||||
|
fiscal_code = 'LCCVTI62M02A662Q',
|
||||||
|
phone = '380 6180216'
|
||||||
|
WHERE email = '[email protected]';
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS report_category_team_categories (
|
||||||
|
report_category_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
team_category_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
PRIMARY KEY (report_category_id, team_category_id),
|
||||||
|
CONSTRAINT report_category_team_categories_report_fk
|
||||||
|
FOREIGN KEY (report_category_id) REFERENCES report_categories(id),
|
||||||
|
CONSTRAINT report_category_team_categories_team_fk
|
||||||
|
FOREIGN KEY (team_category_id) REFERENCES team_categories(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS intervention_access_links (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
report_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
assignment_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
token_hash VARCHAR(255) NOT NULL,
|
||||||
|
created_by_user_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
revoked_at DATETIME NULL,
|
||||||
|
last_accessed_at DATETIME NULL,
|
||||||
|
UNIQUE KEY intervention_access_links_token_hash_unique (token_hash),
|
||||||
|
KEY intervention_access_links_report_assignment_idx (report_id, assignment_id),
|
||||||
|
CONSTRAINT intervention_access_links_report_fk
|
||||||
|
FOREIGN KEY (report_id) REFERENCES reports(id),
|
||||||
|
CONSTRAINT intervention_access_links_assignment_fk
|
||||||
|
FOREIGN KEY (assignment_id) REFERENCES assignments(id),
|
||||||
|
CONSTRAINT intervention_access_links_user_fk
|
||||||
|
FOREIGN KEY (created_by_user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO report_category_team_categories (report_category_id, team_category_id)
|
||||||
|
SELECT rc.id, tc.id
|
||||||
|
FROM report_categories rc
|
||||||
|
JOIN team_categories tc ON
|
||||||
|
(rc.name = 'Buche stradali' AND tc.name = 'Manutenzione strade') OR
|
||||||
|
(rc.name = 'Illuminazione guasta' AND tc.name = 'Illuminazione pubblica') OR
|
||||||
|
(rc.name = 'Rifiuti abbandonati' AND tc.name = 'Igiene urbana');
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,36 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS cie_auth_sessions (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
state VARCHAR(128) NOT NULL,
|
||||||
|
frontend_redirect_uri VARCHAR(500) NOT NULL,
|
||||||
|
status ENUM('PENDING', 'AUTHENTICATED', 'FAILED') NOT NULL DEFAULT 'PENDING',
|
||||||
|
issuer VARCHAR(255) NULL,
|
||||||
|
subject VARCHAR(255) NULL,
|
||||||
|
fiscal_code VARCHAR(16) NULL,
|
||||||
|
authenticated_user_id BIGINT UNSIGNED NULL,
|
||||||
|
result_token VARCHAR(255) NULL,
|
||||||
|
raw_claims JSON NULL,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
consumed_at DATETIME NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY cie_auth_sessions_state_unique (state),
|
||||||
|
KEY cie_auth_sessions_status_idx (status),
|
||||||
|
KEY cie_auth_sessions_expires_idx (expires_at),
|
||||||
|
CONSTRAINT cie_auth_sessions_user_fk FOREIGN KEY (authenticated_user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_external_identities (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
provider VARCHAR(50) NOT NULL,
|
||||||
|
issuer VARCHAR(255) NOT NULL,
|
||||||
|
subject VARCHAR(255) NOT NULL,
|
||||||
|
fiscal_code VARCHAR(16) NOT NULL,
|
||||||
|
raw_claims JSON NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY user_external_identities_provider_subject_unique (provider, issuer, subject),
|
||||||
|
KEY user_external_identities_user_idx (user_id),
|
||||||
|
KEY user_external_identities_fiscal_code_idx (fiscal_code),
|
||||||
|
CONSTRAINT user_external_identities_user_fk FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
+40
-1
@@ -6,7 +6,31 @@
|
|||||||
|
|
||||||
namespace visione::repositories
|
namespace visione::repositories
|
||||||
{
|
{
|
||||||
|
Json::Value scalarToJson(JsonFieldType type, const drogon::orm::Field &field)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case JsonFieldType::String:
|
||||||
|
return Json::Value(field.as<std::string>());
|
||||||
|
case JsonFieldType::Int64:
|
||||||
|
return Json::Value(Json::Int64(field.as<long long>()));
|
||||||
|
case JsonFieldType::UInt64:
|
||||||
|
return Json::Value(Json::UInt64(field.as<unsigned long long>()));
|
||||||
|
case JsonFieldType::Double:
|
||||||
|
return Json::Value(field.as<double>());
|
||||||
|
case JsonFieldType::Bool:
|
||||||
|
return Json::Value(field.as<bool>());
|
||||||
|
}
|
||||||
|
return Json::nullValue;
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value rowToJson(const drogon::orm::Row &row)
|
Json::Value rowToJson(const drogon::orm::Row &row)
|
||||||
|
{
|
||||||
|
static const JsonRowSchema emptySchema;
|
||||||
|
return rowToJson(row, emptySchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value rowToJson(const drogon::orm::Row &row, const JsonRowSchema &schema)
|
||||||
{
|
{
|
||||||
Json::Value object(Json::objectValue);
|
Json::Value object(Json::objectValue);
|
||||||
for (const auto &field : row)
|
for (const auto &field : row)
|
||||||
@@ -16,19 +40,34 @@ Json::Value rowToJson(const drogon::orm::Row &row)
|
|||||||
object[field.name()] = Json::nullValue;
|
object[field.name()] = Json::nullValue;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
const auto it = schema.find(field.name());
|
||||||
|
if (it == schema.end())
|
||||||
{
|
{
|
||||||
object[field.name()] = field.as<std::string>();
|
object[field.name()] = field.as<std::string>();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
object[field.name()] = scalarToJson(it->second, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
Json::Value resultToJson(const drogon::orm::Result &result)
|
Json::Value resultToJson(const drogon::orm::Result &result)
|
||||||
|
{
|
||||||
|
static const JsonRowSchema emptySchema;
|
||||||
|
return resultToJson(result, emptySchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value resultToJson(const drogon::orm::Result &result,
|
||||||
|
const JsonRowSchema &schema)
|
||||||
{
|
{
|
||||||
Json::Value array(Json::arrayValue);
|
Json::Value array(Json::arrayValue);
|
||||||
for (const auto &row : result)
|
for (const auto &row : result)
|
||||||
{
|
{
|
||||||
array.append(rowToJson(row));
|
array.append(rowToJson(row, schema));
|
||||||
}
|
}
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -1,14 +1,30 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <drogon/orm/Field.h>
|
||||||
#include <drogon/orm/Result.h>
|
#include <drogon/orm/Result.h>
|
||||||
|
|
||||||
#include <json/json.h>
|
#include <json/json.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace visione::repositories
|
namespace visione::repositories
|
||||||
{
|
{
|
||||||
|
enum class JsonFieldType
|
||||||
|
{
|
||||||
|
String,
|
||||||
|
Int64,
|
||||||
|
UInt64,
|
||||||
|
Double,
|
||||||
|
Bool
|
||||||
|
};
|
||||||
|
|
||||||
|
using JsonRowSchema = std::unordered_map<std::string, JsonFieldType>;
|
||||||
|
|
||||||
|
Json::Value scalarToJson(JsonFieldType type, const drogon::orm::Field &field);
|
||||||
Json::Value rowToJson(const drogon::orm::Row &row);
|
Json::Value rowToJson(const drogon::orm::Row &row);
|
||||||
|
Json::Value rowToJson(const drogon::orm::Row &row, const JsonRowSchema &schema);
|
||||||
Json::Value resultToJson(const drogon::orm::Result &result);
|
Json::Value resultToJson(const drogon::orm::Result &result);
|
||||||
|
Json::Value resultToJson(const drogon::orm::Result &result,
|
||||||
|
const JsonRowSchema &schema);
|
||||||
std::string nullableString(const Json::Value &value, const std::string &key);
|
std::string nullableString(const Json::Value &value, const std::string &key);
|
||||||
} // namespace visione::repositories
|
} // namespace visione::repositories
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Generate OpenAPI JSON from Drogon routes plus a manual OpenAPI YAML file."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|||||||
@@ -1,471 +0,0 @@
|
|||||||
import json
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import streamlit as st
|
|
||||||
|
|
||||||
|
|
||||||
st.set_page_config(page_title="Visione Lab", layout="wide")
|
|
||||||
|
|
||||||
|
|
||||||
def api_url(path: str) -> str:
|
|
||||||
return st.session_state.base_url.rstrip("/") + path
|
|
||||||
|
|
||||||
|
|
||||||
def headers() -> dict[str, str]:
|
|
||||||
token = st.session_state.get("token", "")
|
|
||||||
return {"Authorization": f"Bearer {token}"} if token else {}
|
|
||||||
|
|
||||||
|
|
||||||
def show_response(response: requests.Response) -> None:
|
|
||||||
st.caption(f"{response.request.method} {response.url} -> {response.status_code}")
|
|
||||||
try:
|
|
||||||
st.json(response.json())
|
|
||||||
except ValueError:
|
|
||||||
st.text(response.text)
|
|
||||||
|
|
||||||
|
|
||||||
def request(method: str, path: str, **kwargs: Any) -> requests.Response:
|
|
||||||
kwargs.setdefault("timeout", 10)
|
|
||||||
return requests.request(method, api_url(path), **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def json_input(label: str, value: dict[str, Any], height: int = 180) -> dict[str, Any] | None:
|
|
||||||
raw = st.text_area(label, json.dumps(value, indent=2), height=height)
|
|
||||||
try:
|
|
||||||
return json.loads(raw)
|
|
||||||
except json.JSONDecodeError as error:
|
|
||||||
st.error(f"Invalid JSON: {error}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def pagination(prefix: str) -> dict[str, int]:
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
with col1:
|
|
||||||
page = st.number_input(f"{prefix} page", min_value=1, value=1, step=1)
|
|
||||||
with col2:
|
|
||||||
per_page = st.number_input(
|
|
||||||
f"{prefix} per_page", min_value=1, max_value=100, value=20, step=1
|
|
||||||
)
|
|
||||||
return {"page": page, "per_page": per_page}
|
|
||||||
|
|
||||||
|
|
||||||
def response_json(response: requests.Response) -> dict[str, Any]:
|
|
||||||
try:
|
|
||||||
data = response.json()
|
|
||||||
except ValueError:
|
|
||||||
return {}
|
|
||||||
return data if isinstance(data, dict) else {}
|
|
||||||
|
|
||||||
|
|
||||||
if "base_url" not in st.session_state:
|
|
||||||
st.session_state.base_url = "http://127.0.0.1:5555/api/v1"
|
|
||||||
|
|
||||||
st.title("Visione API Lab")
|
|
||||||
|
|
||||||
auth_tab, reports_tab, operator_tab, user_tab, admin_tab, public_tab, docs_tab = st.tabs(
|
|
||||||
["Auth", "Reports", "Operator", "User", "Admin", "Public", "Docs"]
|
|
||||||
)
|
|
||||||
|
|
||||||
with auth_tab:
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
with col1:
|
|
||||||
st.subheader("Password login")
|
|
||||||
email = st.text_input("Email", "[email protected]")
|
|
||||||
password = st.text_input("Password", "ChangeMe123!", type="password")
|
|
||||||
if st.button("Login"):
|
|
||||||
response = request(
|
|
||||||
"POST",
|
|
||||||
"/auth/login/password",
|
|
||||||
json={"email": email, "password": password},
|
|
||||||
)
|
|
||||||
token = response_json(response).get("token")
|
|
||||||
if token:
|
|
||||||
st.session_state.token = token
|
|
||||||
show_response(response)
|
|
||||||
|
|
||||||
st.subheader("OTP")
|
|
||||||
otp_email = st.text_input("OTP email", "[email protected]")
|
|
||||||
if st.button("Request OTP"):
|
|
||||||
show_response(request("POST", "/auth/otp/request", json={"email": otp_email}))
|
|
||||||
otp_code = st.text_input("OTP code")
|
|
||||||
if st.button("Verify OTP"):
|
|
||||||
response = request(
|
|
||||||
"POST",
|
|
||||||
"/auth/otp/verify",
|
|
||||||
json={"email": otp_email, "code": otp_code},
|
|
||||||
)
|
|
||||||
token = response_json(response).get("token")
|
|
||||||
if token:
|
|
||||||
st.session_state.token = token
|
|
||||||
show_response(response)
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
st.subheader("Register citizen")
|
|
||||||
payload = json_input(
|
|
||||||
"Register payload",
|
|
||||||
{
|
|
||||||
"first_name": "Mario",
|
|
||||||
"last_name": "Rossi",
|
|
||||||
"email": "[email protected]",
|
|
||||||
"password": "ChangeMe123!",
|
|
||||||
"fiscal_code": "RSSMRA80A01H501U",
|
|
||||||
"phone": "+391234567890",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if st.button("Register") and payload:
|
|
||||||
show_response(request("POST", "/auth/register", json=payload))
|
|
||||||
|
|
||||||
st.subheader("Me")
|
|
||||||
if st.button("GET /me"):
|
|
||||||
show_response(request("GET", "/me", headers=headers()))
|
|
||||||
|
|
||||||
st.subheader("Change password")
|
|
||||||
current_password = st.text_input(
|
|
||||||
"Current password", "ChangeMe123!", type="password"
|
|
||||||
)
|
|
||||||
new_password = st.text_input("New password", "ChangeMe123!!", type="password")
|
|
||||||
if st.button("Change password"):
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"POST",
|
|
||||||
"/auth/change-password",
|
|
||||||
json={
|
|
||||||
"current_password": current_password,
|
|
||||||
"new_password": new_password,
|
|
||||||
},
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with reports_tab:
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
with col1:
|
|
||||||
st.subheader("Create report")
|
|
||||||
report_payload = json_input(
|
|
||||||
"Report payload",
|
|
||||||
{
|
|
||||||
"category_id": 1,
|
|
||||||
"latitude": 41.9028,
|
|
||||||
"longitude": 12.4964,
|
|
||||||
"address": "Via Roma 1",
|
|
||||||
"description": "Lampione non funzionante",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if st.button("Create report") and report_payload:
|
|
||||||
show_response(request("POST", "/reports", json=report_payload, headers=headers()))
|
|
||||||
|
|
||||||
st.subheader("List reports")
|
|
||||||
list_col1, list_col2, list_col3 = st.columns(3)
|
|
||||||
with list_col1:
|
|
||||||
status = st.text_input("Status filter")
|
|
||||||
with list_col2:
|
|
||||||
category_id = st.text_input("Category id filter")
|
|
||||||
with list_col3:
|
|
||||||
area_id = st.text_input("Area id filter")
|
|
||||||
params = {
|
|
||||||
key: value
|
|
||||||
for key, value in {
|
|
||||||
"status": status,
|
|
||||||
"category_id": category_id,
|
|
||||||
"area_id": area_id,
|
|
||||||
}.items()
|
|
||||||
if value
|
|
||||||
}
|
|
||||||
if st.button("GET /reports"):
|
|
||||||
show_response(request("GET", "/reports", params=params, headers=headers()))
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
report_id = st.number_input("Report id", min_value=1, step=1)
|
|
||||||
if st.button("Detail"):
|
|
||||||
show_response(request("GET", f"/reports/{report_id}", headers=headers()))
|
|
||||||
|
|
||||||
st.subheader("Messages")
|
|
||||||
message_payload = json_input("Message payload", {"body": "Messaggio dal lab"}, 120)
|
|
||||||
msg_col1, msg_col2 = st.columns(2)
|
|
||||||
with msg_col1:
|
|
||||||
if st.button("Add message") and message_payload:
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"POST",
|
|
||||||
f"/reports/{report_id}/messages",
|
|
||||||
json=message_payload,
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
with msg_col2:
|
|
||||||
if st.button("List messages"):
|
|
||||||
show_response(
|
|
||||||
request("GET", f"/reports/{report_id}/messages", headers=headers())
|
|
||||||
)
|
|
||||||
|
|
||||||
st.subheader("Upload media")
|
|
||||||
files = st.file_uploader(
|
|
||||||
"Images or videos",
|
|
||||||
accept_multiple_files=True,
|
|
||||||
type=["jpg", "jpeg", "png", "webp", "mp4", "webm", "mov"],
|
|
||||||
)
|
|
||||||
if st.button("Upload") and files:
|
|
||||||
multipart = [
|
|
||||||
("file", (file.name, file.getvalue(), file.type or "application/octet-stream"))
|
|
||||||
for file in files
|
|
||||||
]
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"POST",
|
|
||||||
f"/reports/{report_id}/media",
|
|
||||||
headers=headers(),
|
|
||||||
files=multipart,
|
|
||||||
timeout=60,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
st.subheader("Workflow")
|
|
||||||
new_status = st.selectbox(
|
|
||||||
"Status",
|
|
||||||
["UNDER_REVIEW", "ASSIGNED", "IN_PROGRESS", "RESOLVED", "REJECTED", "CLOSED"],
|
|
||||||
)
|
|
||||||
if st.button("Update status"):
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"PATCH",
|
|
||||||
f"/reports/{report_id}/status",
|
|
||||||
json={"status": new_status, "note": "Updated from Visione Lab"},
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if st.button("Take report"):
|
|
||||||
show_response(request("POST", f"/reports/{report_id}/take", headers=headers()))
|
|
||||||
|
|
||||||
st.subheader("Assignment and feedback")
|
|
||||||
team_id = st.number_input("Team id", min_value=1, step=1)
|
|
||||||
assign_col, feedback_col = st.columns(2)
|
|
||||||
with assign_col:
|
|
||||||
if st.button("Assign team"):
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"POST",
|
|
||||||
f"/reports/{report_id}/assignments",
|
|
||||||
json={"team_id": team_id},
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
with feedback_col:
|
|
||||||
feedback_payload = json_input(
|
|
||||||
"Feedback payload", {"rating": 5, "comment": "Intervento risolto"}, 120
|
|
||||||
)
|
|
||||||
if st.button("Add feedback") and feedback_payload:
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"POST",
|
|
||||||
f"/reports/{report_id}/feedback",
|
|
||||||
json=feedback_payload,
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with operator_tab:
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
with col1:
|
|
||||||
st.subheader("Operator queues")
|
|
||||||
queue_params = pagination("Queue")
|
|
||||||
queue_endpoint = st.selectbox(
|
|
||||||
"Queue",
|
|
||||||
[
|
|
||||||
"/operator/reports/assigned",
|
|
||||||
"/operator/reports/open",
|
|
||||||
"/operator/reports/others",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
if st.button("GET operator queue"):
|
|
||||||
show_response(
|
|
||||||
request("GET", queue_endpoint, params=queue_params, headers=headers())
|
|
||||||
)
|
|
||||||
|
|
||||||
st.subheader("Intervention summary")
|
|
||||||
summary_report_id = st.number_input(
|
|
||||||
"Summary report id", min_value=1, value=1, step=1
|
|
||||||
)
|
|
||||||
if st.button("GET intervention summary"):
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"GET",
|
|
||||||
f"/reports/{summary_report_id}/intervention-summary",
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
st.subheader("Flag content")
|
|
||||||
flag_report_id = st.number_input("Flag report id", min_value=1, value=1, step=1)
|
|
||||||
flag_payload = json_input(
|
|
||||||
"Flag payload",
|
|
||||||
{"reason": "abusive_content", "details": "Created from Visione Lab"},
|
|
||||||
140,
|
|
||||||
)
|
|
||||||
if st.button("Flag report") and flag_payload:
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"POST",
|
|
||||||
f"/reports/{flag_report_id}/flags",
|
|
||||||
json=flag_payload,
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
message_id = st.number_input("Flag message id", min_value=1, value=1, step=1)
|
|
||||||
if st.button("Flag message") and flag_payload:
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"POST",
|
|
||||||
f"/reports/{flag_report_id}/messages/{message_id}/flags",
|
|
||||||
json=flag_payload,
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with user_tab:
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
with col1:
|
|
||||||
st.subheader("Notifications")
|
|
||||||
notification_params = pagination("Notifications")
|
|
||||||
if st.button("List notifications"):
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"GET",
|
|
||||||
"/notifications",
|
|
||||||
params=notification_params,
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
notification_id = st.number_input("Notification id", min_value=1, step=1)
|
|
||||||
if st.button("Mark notification read"):
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"PATCH",
|
|
||||||
f"/notifications/{notification_id}/read",
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
st.subheader("Rewards")
|
|
||||||
rewards_params = pagination("Rewards")
|
|
||||||
if st.button("GET /me/rewards"):
|
|
||||||
show_response(
|
|
||||||
request("GET", "/me/rewards", params=rewards_params, headers=headers())
|
|
||||||
)
|
|
||||||
|
|
||||||
with admin_tab:
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
with col1:
|
|
||||||
st.subheader("Reference data and stats")
|
|
||||||
endpoint = st.selectbox(
|
|
||||||
"Endpoint",
|
|
||||||
[
|
|
||||||
"/admin/categories",
|
|
||||||
"/admin/areas",
|
|
||||||
"/admin/teams",
|
|
||||||
"/admin/stats/overview",
|
|
||||||
"/admin/users",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
if st.button("GET admin endpoint"):
|
|
||||||
show_response(request("GET", endpoint, headers=headers()))
|
|
||||||
|
|
||||||
st.subheader("Create reference data")
|
|
||||||
create_endpoint = st.selectbox(
|
|
||||||
"Create endpoint", ["/admin/categories", "/admin/areas", "/admin/teams"]
|
|
||||||
)
|
|
||||||
create_payload = json_input(
|
|
||||||
"Create payload",
|
|
||||||
{"name": "Nuova categoria", "description": "Created from Visione Lab"},
|
|
||||||
)
|
|
||||||
if st.button("POST admin endpoint") and create_payload:
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"POST",
|
|
||||||
create_endpoint,
|
|
||||||
json=create_payload,
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
st.subheader("User administration")
|
|
||||||
admin_user_id = st.number_input("Admin target user id", min_value=1, step=1)
|
|
||||||
roles_payload = json_input("Roles payload", {"roles": ["operator"]}, 100)
|
|
||||||
if st.button("Update roles") and roles_payload:
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"PATCH",
|
|
||||||
f"/admin/users/{admin_user_id}/roles",
|
|
||||||
json=roles_payload,
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
banned = st.checkbox("Banned")
|
|
||||||
if st.button("Update ban"):
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"PATCH",
|
|
||||||
f"/admin/users/{admin_user_id}/ban",
|
|
||||||
json={"banned": banned},
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
st.subheader("Moderation")
|
|
||||||
moderation_params = pagination("Moderation")
|
|
||||||
moderation_status = st.selectbox(
|
|
||||||
"Moderation status", ["", "OPEN", "RESOLVED", "DISMISSED"]
|
|
||||||
)
|
|
||||||
if moderation_status:
|
|
||||||
moderation_params["status"] = moderation_status
|
|
||||||
if st.button("List moderation flags"):
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"GET",
|
|
||||||
"/admin/moderation",
|
|
||||||
params=moderation_params,
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
flag_id = st.number_input("Moderation flag id", min_value=1, step=1)
|
|
||||||
moderation_payload = json_input(
|
|
||||||
"Moderation update payload",
|
|
||||||
{
|
|
||||||
"status": "RESOLVED",
|
|
||||||
"resolution_note": "Reviewed from Visione Lab",
|
|
||||||
"reject_report": False,
|
|
||||||
},
|
|
||||||
140,
|
|
||||||
)
|
|
||||||
if st.button("Update moderation flag") and moderation_payload:
|
|
||||||
show_response(
|
|
||||||
request(
|
|
||||||
"PATCH",
|
|
||||||
f"/admin/moderation/{flag_id}",
|
|
||||||
json=moderation_payload,
|
|
||||||
headers=headers(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with public_tab:
|
|
||||||
st.subheader("Open data")
|
|
||||||
open_data_params = pagination("Open data")
|
|
||||||
if st.button("GET /open-data/reports"):
|
|
||||||
show_response(
|
|
||||||
request("GET", "/open-data/reports", params=open_data_params)
|
|
||||||
)
|
|
||||||
|
|
||||||
with docs_tab:
|
|
||||||
st.link_button("Swagger UI", api_url("/docs"))
|
|
||||||
if st.button("Fetch OpenAPI"):
|
|
||||||
show_response(request("GET", "/openapi.json"))
|
|
||||||
|
|
||||||
with st.sidebar:
|
|
||||||
st.text_input("Backend API", key="base_url")
|
|
||||||
st.text_input("Bearer token", key="token", type="password")
|
|
||||||
st.link_button("MailHog", "http://127.0.0.1:8025")
|
|
||||||
if st.button("Health check"):
|
|
||||||
show_response(request("GET", "/health", timeout=5))
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "visione-lab"
|
|
||||||
version = "0.1.0"
|
|
||||||
requires-python = ">=3.13"
|
|
||||||
dependencies = [
|
|
||||||
"requests>=2.32.5",
|
|
||||||
"streamlit>=1.58.0",
|
|
||||||
]
|
|
||||||
Generated
-894
@@ -1,894 +0,0 @@
|
|||||||
version = 1
|
|
||||||
revision = 3
|
|
||||||
requires-python = ">=3.13"
|
|
||||||
resolution-markers = [
|
|
||||||
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
|
||||||
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
|
|
||||||
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
|
||||||
"python_full_version < '3.14' and sys_platform == 'win32'",
|
|
||||||
"python_full_version < '3.14' and sys_platform == 'emscripten'",
|
|
||||||
"python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "altair"
|
|
||||||
version = "6.1.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "jinja2" },
|
|
||||||
{ name = "jsonschema" },
|
|
||||||
{ name = "narwhals" },
|
|
||||||
{ name = "packaging" },
|
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.15'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/1e/365a9144db3254f86f1b974660b9ede1e9a38c9dc0730e4a9b1192eec5d6/altair-6.1.0.tar.gz", hash = "sha256:dda699216cf85b040d968ae5a569ad45957616811e38760a85e5118269daca67", size = 765519, upload-time = "2026-04-21T13:08:46.44Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/63/5dacc8d8306c715088b897a479e551bc0779fd2f0f26c97fec5e36542b4e/altair-6.1.0-py3-none-any.whl", hash = "sha256:fdf5fd939512e5b2fc4441c82dfd2635e706defbd037db0ac429ef5ddce66c3b", size = 796996, upload-time = "2026-04-21T13:08:48.549Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anyio"
|
|
||||||
version = "4.13.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "idna" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "attrs"
|
|
||||||
version = "26.1.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "blinker"
|
|
||||||
version = "1.9.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cachetools"
|
|
||||||
version = "7.1.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "certifi"
|
|
||||||
version = "2026.5.20"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "charset-normalizer"
|
|
||||||
version = "3.4.7"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "click"
|
|
||||||
version = "8.4.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorama"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gitdb"
|
|
||||||
version = "4.0.12"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "smmap" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gitpython"
|
|
||||||
version = "3.1.50"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "gitdb" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "h11"
|
|
||||||
version = "0.16.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "httptools"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idna"
|
|
||||||
version = "3.18"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itsdangerous"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jinja2"
|
|
||||||
version = "3.1.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "markupsafe" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jsonschema"
|
|
||||||
version = "4.26.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "attrs" },
|
|
||||||
{ name = "jsonschema-specifications" },
|
|
||||||
{ name = "referencing" },
|
|
||||||
{ name = "rpds-py" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jsonschema-specifications"
|
|
||||||
version = "2025.9.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "referencing" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "markupsafe"
|
|
||||||
version = "3.0.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "narwhals"
|
|
||||||
version = "2.22.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/1c/c80cb7719721a44846c6301ef118434bae30a423924bfad3a47f16bdc064/narwhals-2.22.0.tar.gz", hash = "sha256:6486282bb7e4b4ab55963efbd8be1451b764cc4874b74d1fd625eba9dc60b86f", size = 417565, upload-time = "2026-06-01T13:34:36.249Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/b6/e7cdde7b8e90d5dff25b622f95833ef26567ad184c977278b93a1cbd5717/narwhals-2.22.0-py3-none-any.whl", hash = "sha256:1421797ede01789cc1537619dbc3f36f840737240f748fdb24a60a0225fc80be", size = 453815, upload-time = "2026-06-01T13:34:34.127Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "numpy"
|
|
||||||
version = "2.4.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "packaging"
|
|
||||||
version = "26.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pandas"
|
|
||||||
version = "3.0.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "numpy" },
|
|
||||||
{ name = "python-dateutil" },
|
|
||||||
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pillow"
|
|
||||||
version = "12.2.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "protobuf"
|
|
||||||
version = "7.35.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/60/fd/5b1491d9e4b586d621c54f4c36b888714164b6875f8d6afa3f9072906a51/protobuf-7.35.0.tar.gz", hash = "sha256:a2efd84605f41e559f1881b0912b44099d0a2ac9bf46b3474823f10fb393b0e6", size = 458677, upload-time = "2026-05-19T23:02:29.197Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/ee/93d06e358a4aa32280b00e722d3ea0a1f25fc3cc5778d80581c9cca2c10e/protobuf-7.35.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:66be6c513931c794fa92c080ffee41671390da3d79da219cf9c0c0907f035dda", size = 433225, upload-time = "2026-05-19T23:02:19.884Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/39/1c76c2da93f3c507e958e0aecee2391cc44d4625de6c728bbc555195b5a8/protobuf-7.35.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:fcbe42a4ac09d3ec9c987ddfcd956afd0b15f1ff613bd8371bde9405ffd5c8e5", size = 328847, upload-time = "2026-05-19T23:02:22.3Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/1a/39f7ce90a238c1a987a4d81ec26379e02ca0aff367de68e4a1fa474215b9/protobuf-7.35.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:4cbf5cc286130e06a6c9bbefac442431173906dfcc979712183d4adcc01b37ee", size = 344030, upload-time = "2026-05-19T23:02:23.591Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/5b/6baf9008817964454055ff3fe65f1de0b5f1e26c80c82f7fb108b7cd4ea3/protobuf-7.35.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:6c0f98f10c8a05ea30f8993dfef2de093d27b490fdae78bb60c8343795d55011", size = 327130, upload-time = "2026-05-19T23:02:24.637Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/e5/e46adb0badc388bfb84877a5f9f026aff63f60e611016cf64dbe77e05446/protobuf-7.35.0-cp310-abi3-win32.whl", hash = "sha256:4c4617b83ade0e279d1d2bfe04025a1adb87f9ed657de038620dc0ff959357f6", size = 428946, upload-time = "2026-05-19T23:02:25.741Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/ab/547fbd9e16d879dd13c167478f8ae0a83a428008ca07a5e06acdc23ad473/protobuf-7.35.0-cp310-abi3-win_amd64.whl", hash = "sha256:f05bcadf9a2a6b8dda047007075135fb7d08c73d9177aabc067e1be46881a201", size = 439996, upload-time = "2026-05-19T23:02:26.808Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/ef/50433d346c56657a70d27f156c7b349ac59a068b01de4eb796e747eecc43/protobuf-7.35.0-py3-none-any.whl", hash = "sha256:c13f325cf242bad135c350629eeb5d54b24228eb472fb3e2e9ebbd4c5dc20ca0", size = 171659, upload-time = "2026-05-19T23:02:27.842Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyarrow"
|
|
||||||
version = "24.0.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pydeck"
|
|
||||||
version = "0.9.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "jinja2" },
|
|
||||||
{ name = "numpy" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/df/4e9e7f20f8034a37c6571c93809f6d22388c39978c98d174d656c1a18fd2/pydeck-0.9.2.tar.gz", hash = "sha256:c10d9035e81ead6385264cac8d19402471f6866a15ca1f7df1400f52142bcf87", size = 5849672, upload-time = "2026-04-16T18:30:30.089Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/24/b30ee7d723100fd822de1bb4c0adea62f3419884a75a536f35f355d1e7c0/pydeck-0.9.2-py2.py3-none-any.whl", hash = "sha256:8213dfeacc5f6bfe6825f61c8ee34e3850e8a31fc43924379ec98edb34a75b25", size = 11305615, upload-time = "2026-04-16T18:30:28.133Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "python-dateutil"
|
|
||||||
version = "2.9.0.post0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "six" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "python-multipart"
|
|
||||||
version = "0.0.30"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/82/c8cd43a6e0719bf5a3b034f6726dd701f75829c08944c83d4b95d02ed0e8/python_multipart-0.0.30.tar.gz", hash = "sha256:0edfe0475c1f46ddd3ff7785a626f6118af32bdcf359bb21260367313bb32118", size = 46316, upload-time = "2026-05-31T19:24:55.198Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/fd/0318007beb234790993d3ec5afd051d1dbceb733e81e3afe2b981ece3f37/python_multipart-0.0.30-py3-none-any.whl", hash = "sha256:830964def8c90607ac5daa00514e3987815865713ade8d20febc9177ac0c3c5b", size = 29730, upload-time = "2026-05-31T19:24:53.814Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "referencing"
|
|
||||||
version = "0.37.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "attrs" },
|
|
||||||
{ name = "rpds-py" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "requests"
|
|
||||||
version = "2.34.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "certifi" },
|
|
||||||
{ name = "charset-normalizer" },
|
|
||||||
{ name = "idna" },
|
|
||||||
{ name = "urllib3" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rpds-py"
|
|
||||||
version = "2026.5.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "six"
|
|
||||||
version = "1.17.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "smmap"
|
|
||||||
version = "5.0.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "starlette"
|
|
||||||
version = "1.2.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "anyio" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/25/44/ec35f1b6e83094b997da438a02c8c9b0ade2b1e84cfc48bd4656780760a6/starlette-1.2.1.tar.gz", hash = "sha256:9b9b5ebb992e67d6093741e63c2f59e4f6fff986f81163c087867bd7b924b3f6", size = 2701854, upload-time = "2026-05-31T01:07:51.847Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/54/196d0c1db10af76baa4f64894448505d60d3cdf70ef92cbb35f46a4e4c71/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", size = 73350, upload-time = "2026-05-31T01:07:50.09Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "streamlit"
|
|
||||||
version = "1.58.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "altair" },
|
|
||||||
{ name = "anyio" },
|
|
||||||
{ name = "blinker" },
|
|
||||||
{ name = "cachetools" },
|
|
||||||
{ name = "click" },
|
|
||||||
{ name = "gitpython" },
|
|
||||||
{ name = "httptools" },
|
|
||||||
{ name = "itsdangerous" },
|
|
||||||
{ name = "numpy" },
|
|
||||||
{ name = "packaging" },
|
|
||||||
{ name = "pandas" },
|
|
||||||
{ name = "pillow" },
|
|
||||||
{ name = "protobuf" },
|
|
||||||
{ name = "pyarrow" },
|
|
||||||
{ name = "pydeck" },
|
|
||||||
{ name = "python-multipart" },
|
|
||||||
{ name = "requests" },
|
|
||||||
{ name = "starlette" },
|
|
||||||
{ name = "tenacity" },
|
|
||||||
{ name = "toml" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
{ name = "uvicorn" },
|
|
||||||
{ name = "watchdog", marker = "sys_platform != 'darwin'" },
|
|
||||||
{ name = "websockets" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/91/74/20dac6d6200d6ec0e1c230fb8eeb6a1a423645eacb76e8d802adfc246456/streamlit-1.58.0.tar.gz", hash = "sha256:78a22e7085b053af7ce544442bf4b670771e68c509ba1bdaa056ba0708f49c3d", size = 8721149, upload-time = "2026-05-28T18:02:44.606Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/84/14c36a92fb24f8e1cea452f53b0744b5da69d52cdd2fe22e71e6fbf765d5/streamlit-1.58.0-py3-none-any.whl", hash = "sha256:4ca8a7afc5bd16a5f176ccf4be1e34e8121cad0240becd127fb58a103ea3178d", size = 9219185, upload-time = "2026-05-28T18:02:41.993Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tenacity"
|
|
||||||
version = "9.1.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml"
|
|
||||||
version = "0.10.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typing-extensions"
|
|
||||||
version = "4.15.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tzdata"
|
|
||||||
version = "2026.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "urllib3"
|
|
||||||
version = "2.7.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "uvicorn"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "click" },
|
|
||||||
{ name = "h11" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "visione-lab"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { virtual = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "requests" },
|
|
||||||
{ name = "streamlit" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "requests", specifier = ">=2.32.5" },
|
|
||||||
{ name = "streamlit", specifier = ">=1.58.0" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "watchdog"
|
|
||||||
version = "6.0.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "websockets"
|
|
||||||
version = "16.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
|
|
||||||
]
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import importlib.util
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
SCRIPT_PATH = Path(__file__).with_name("generate_openapi.py")
|
|
||||||
SPEC = importlib.util.spec_from_file_location("generate_openapi", SCRIPT_PATH)
|
|
||||||
generate_openapi = importlib.util.module_from_spec(SPEC)
|
|
||||||
assert SPEC.loader is not None
|
|
||||||
SPEC.loader.exec_module(generate_openapi)
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateOpenApiTest(unittest.TestCase):
|
|
||||||
def test_extracts_drogon_route(self) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
|
||||||
controllers = Path(tmp)
|
|
||||||
(controllers / "SampleController.h").write_text(
|
|
||||||
"""
|
|
||||||
class SampleController {
|
|
||||||
METHOD_LIST_BEGIN
|
|
||||||
ADD_METHOD_TO(SampleController::detail,
|
|
||||||
"/api/v1/samples/{1}",
|
|
||||||
drogon::Get);
|
|
||||||
METHOD_LIST_END
|
|
||||||
};
|
|
||||||
""",
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
routes = generate_openapi.parse_routes(controllers)
|
|
||||||
operation = routes[("/api/v1/samples/{id}", "get")]
|
|
||||||
self.assertEqual(operation["operationId"], "SampleController.detail")
|
|
||||||
self.assertEqual(operation["parameters"][0]["name"], "id")
|
|
||||||
|
|
||||||
def test_names_secondary_path_parameters_from_previous_segment(self) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
|
||||||
controllers = Path(tmp)
|
|
||||||
(controllers / "SampleController.h").write_text(
|
|
||||||
"""
|
|
||||||
class SampleController {
|
|
||||||
METHOD_LIST_BEGIN
|
|
||||||
ADD_METHOD_TO(SampleController::flagMessage,
|
|
||||||
"/api/v1/reports/{1}/messages/{2}/flags",
|
|
||||||
drogon::Post);
|
|
||||||
METHOD_LIST_END
|
|
||||||
};
|
|
||||||
""",
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
routes = generate_openapi.parse_routes(controllers)
|
|
||||||
operation = routes[
|
|
||||||
("/api/v1/reports/{id}/messages/{message_id}/flags", "post")
|
|
||||||
]
|
|
||||||
self.assertEqual(operation["parameters"][1]["name"], "message_id")
|
|
||||||
|
|
||||||
def test_manual_operation_overrides_generated_defaults(self) -> None:
|
|
||||||
generated = {
|
|
||||||
("/api/v1/samples", "get"): {
|
|
||||||
"tags": ["Sample"],
|
|
||||||
"operationId": "SampleController.list",
|
|
||||||
"summary": "List",
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {"200": {"description": "Response"}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manual = {
|
|
||||||
"openapi": "3.0.3",
|
|
||||||
"info": {"title": "Manual", "version": "1.0.0"},
|
|
||||||
"paths": {
|
|
||||||
"/api/v1/samples": {
|
|
||||||
"get": {
|
|
||||||
"summary": "Manual summary",
|
|
||||||
"responses": {"200": {"description": "Manual response"}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
spec = generate_openapi.merge_openapi(
|
|
||||||
generated, manual, "Generated", "0.1.0", "http://localhost"
|
|
||||||
)
|
|
||||||
operation = spec["paths"]["/api/v1/samples"]["get"]
|
|
||||||
self.assertEqual(operation["summary"], "Manual summary")
|
|
||||||
self.assertEqual(operation["operationId"], "SampleController.list")
|
|
||||||
|
|
||||||
def test_stale_manual_operation_fails_unless_marked_manual_only(self) -> None:
|
|
||||||
manual = {
|
|
||||||
"paths": {
|
|
||||||
"/api/v1/stale": {
|
|
||||||
"get": {"responses": {"200": {"description": "Stale"}}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
with self.assertRaisesRegex(ValueError, "no matching Drogon route"):
|
|
||||||
generate_openapi.merge_openapi({}, manual, "API", "0.1.0", "http://x")
|
|
||||||
|
|
||||||
manual["paths"]["/api/v1/stale"]["get"]["x-manual-only"] = True
|
|
||||||
spec = generate_openapi.merge_openapi({}, manual, "API", "0.1.0", "http://x")
|
|
||||||
self.assertIn("/api/v1/stale", spec["paths"])
|
|
||||||
self.assertNotIn("x-manual-only", spec["paths"]["/api/v1/stale"]["get"])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
+331
-44
@@ -1,5 +1,6 @@
|
|||||||
#include "services/AdminService.h"
|
#include "services/AdminService.h"
|
||||||
|
|
||||||
|
#include "common/GeoJson.h"
|
||||||
#include "common/Validation.h"
|
#include "common/Validation.h"
|
||||||
#include "repositories/Sql.h"
|
#include "repositories/Sql.h"
|
||||||
#include "services/UserService.h"
|
#include "services/UserService.h"
|
||||||
@@ -7,11 +8,15 @@
|
|||||||
#include <drogon/drogon.h>
|
#include <drogon/drogon.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
namespace visione::services
|
namespace visione::services
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
using repositories::JsonFieldType;
|
||||||
|
using repositories::JsonRowSchema;
|
||||||
|
|
||||||
Json::Value failure(const std::string &code)
|
Json::Value failure(const std::string &code)
|
||||||
{
|
{
|
||||||
Json::Value body;
|
Json::Value body;
|
||||||
@@ -58,24 +63,73 @@ int boundedPerPage(const std::optional<std::string> &value)
|
|||||||
return 50;
|
return 50;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Json::Value AdminService::listCategories()
|
const JsonRowSchema &moderationFlagSchema()
|
||||||
{
|
{
|
||||||
auto response = success();
|
static const JsonRowSchema schema{
|
||||||
response["categories"] = repositories::resultToJson(
|
{"id", JsonFieldType::UInt64},
|
||||||
drogon::app().getDbClient()->execSqlSync(
|
{"report_id", JsonFieldType::UInt64},
|
||||||
"SELECT * FROM report_categories ORDER BY name"));
|
{"message_id", JsonFieldType::UInt64},
|
||||||
return response;
|
{"created_by_user_id", JsonFieldType::UInt64},
|
||||||
|
{"resolved_by_user_id", JsonFieldType::UInt64}};
|
||||||
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const JsonRowSchema &adminUserSchema()
|
||||||
|
{
|
||||||
|
static const JsonRowSchema schema{
|
||||||
|
{"id", JsonFieldType::UInt64},
|
||||||
|
{"points", JsonFieldType::Int64},
|
||||||
|
{"banned", JsonFieldType::Bool}};
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isActiveField(const Json::Value &body)
|
||||||
|
{
|
||||||
|
return body.isMember("active") && body["active"].isBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Json::Value> loadAllowedBoundary(const std::string &code)
|
||||||
|
{
|
||||||
|
const auto rows = drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT JSON_UNQUOTE(JSON_EXTRACT(boundary_geojson, '$')) AS geojson "
|
||||||
|
"FROM allowed_boundaries WHERE code = ? AND active = TRUE LIMIT 1",
|
||||||
|
code);
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::CharReaderBuilder builder;
|
||||||
|
Json::Value geojson;
|
||||||
|
std::string errors;
|
||||||
|
std::istringstream stream(rows[0]["geojson"].as<std::string>());
|
||||||
|
if (!Json::parseFromStream(builder, stream, &geojson, &errors))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return geojson;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAreaInsideAllowedBoundary(const Json::Value &boundaryGeojson)
|
||||||
|
{
|
||||||
|
const auto allowed = loadAllowedBoundary("BARI");
|
||||||
|
return allowed &&
|
||||||
|
geojson::polygonInsideGeometry(boundaryGeojson, *allowed);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
Json::Value AdminService::createCategory(const auth::UserContext &user,
|
Json::Value AdminService::createCategory(const auth::UserContext &user,
|
||||||
const Json::Value &body)
|
const Json::Value &body)
|
||||||
{
|
{
|
||||||
if (!auth::isAdmin(user) || !validation::hasString(body, "name"))
|
if (!auth::isAdmin(user))
|
||||||
{
|
{
|
||||||
return failure("forbidden");
|
return failure("forbidden");
|
||||||
}
|
}
|
||||||
|
if (!validation::hasString(body, "name"))
|
||||||
|
{
|
||||||
|
return failure("invalid_category");
|
||||||
|
}
|
||||||
|
|
||||||
drogon::app().getDbClient()->execSqlSync(
|
drogon::app().getDbClient()->execSqlSync(
|
||||||
"INSERT INTO report_categories (name, description) VALUES (?, ?)",
|
"INSERT INTO report_categories (name, description) VALUES (?, ?)",
|
||||||
@@ -84,22 +138,21 @@ Json::Value AdminService::createCategory(const auth::UserContext &user,
|
|||||||
return success();
|
return success();
|
||||||
}
|
}
|
||||||
|
|
||||||
Json::Value AdminService::listAreas()
|
|
||||||
{
|
|
||||||
auto response = success();
|
|
||||||
response["areas"] = repositories::resultToJson(
|
|
||||||
drogon::app().getDbClient()->execSqlSync("SELECT * FROM areas ORDER BY name"));
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
Json::Value AdminService::createArea(const auth::UserContext &user,
|
Json::Value AdminService::createArea(const auth::UserContext &user,
|
||||||
const Json::Value &body)
|
const Json::Value &body)
|
||||||
{
|
{
|
||||||
if (!auth::isAdmin(user) || !validation::hasString(body, "name") ||
|
if (!auth::isAdmin(user))
|
||||||
!body["boundary_geojson"].isObject())
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
if (!validation::hasString(body, "name") || !body["boundary_geojson"].isObject())
|
||||||
{
|
{
|
||||||
return failure("invalid_area");
|
return failure("invalid_area");
|
||||||
}
|
}
|
||||||
|
if (!isAreaInsideAllowedBoundary(body["boundary_geojson"]))
|
||||||
|
{
|
||||||
|
return failure("area_outside_allowed_boundary");
|
||||||
|
}
|
||||||
|
|
||||||
Json::StreamWriterBuilder writer;
|
Json::StreamWriterBuilder writer;
|
||||||
drogon::app().getDbClient()->execSqlSync(
|
drogon::app().getDbClient()->execSqlSync(
|
||||||
@@ -111,29 +164,38 @@ Json::Value AdminService::createArea(const auth::UserContext &user,
|
|||||||
return success();
|
return success();
|
||||||
}
|
}
|
||||||
|
|
||||||
Json::Value AdminService::listTeams()
|
|
||||||
{
|
|
||||||
auto response = success();
|
|
||||||
response["teams"] = repositories::resultToJson(
|
|
||||||
drogon::app().getDbClient()->execSqlSync(
|
|
||||||
"SELECT t.*, tc.name AS category_name, a.name AS area_name "
|
|
||||||
"FROM teams t JOIN team_categories tc ON tc.id = t.team_category_id "
|
|
||||||
"LEFT JOIN areas a ON a.id = t.area_id ORDER BY t.name"));
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
Json::Value AdminService::createTeam(const auth::UserContext &user,
|
Json::Value AdminService::createTeam(const auth::UserContext &user,
|
||||||
const Json::Value &body)
|
const Json::Value &body)
|
||||||
{
|
{
|
||||||
if (!auth::isAdmin(user) || !validation::hasString(body, "name") ||
|
if (!auth::isAdmin(user))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
if (!validation::hasString(body, "name") ||
|
||||||
!validation::hasNumber(body, "team_category_id"))
|
!validation::hasNumber(body, "team_category_id"))
|
||||||
{
|
{
|
||||||
return failure("invalid_team");
|
return failure("invalid_team");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
const auto teamCategoryRows = db->execSqlSync(
|
||||||
|
"SELECT id FROM team_categories WHERE id = ?",
|
||||||
|
body["team_category_id"].asInt64());
|
||||||
|
if (teamCategoryRows.empty())
|
||||||
|
{
|
||||||
|
return failure("invalid_reference");
|
||||||
|
}
|
||||||
|
|
||||||
if (body["area_id"].isNumeric())
|
if (body["area_id"].isNumeric())
|
||||||
{
|
{
|
||||||
drogon::app().getDbClient()->execSqlSync(
|
const auto areaRows = db->execSqlSync(
|
||||||
|
"SELECT id FROM areas WHERE id = ?",
|
||||||
|
body["area_id"].asInt64());
|
||||||
|
if (areaRows.empty())
|
||||||
|
{
|
||||||
|
return failure("invalid_reference");
|
||||||
|
}
|
||||||
|
db->execSqlSync(
|
||||||
"INSERT INTO teams (team_category_id, area_id, name, description) "
|
"INSERT INTO teams (team_category_id, area_id, name, description) "
|
||||||
"VALUES (?, ?, ?, ?)",
|
"VALUES (?, ?, ?, ?)",
|
||||||
body["team_category_id"].asInt64(),
|
body["team_category_id"].asInt64(),
|
||||||
@@ -143,7 +205,7 @@ Json::Value AdminService::createTeam(const auth::UserContext &user,
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
drogon::app().getDbClient()->execSqlSync(
|
db->execSqlSync(
|
||||||
"INSERT INTO teams (team_category_id, name, description) VALUES (?, ?, ?)",
|
"INSERT INTO teams (team_category_id, name, description) VALUES (?, ?, ?)",
|
||||||
body["team_category_id"].asInt64(),
|
body["team_category_id"].asInt64(),
|
||||||
body["name"].asString(),
|
body["name"].asString(),
|
||||||
@@ -174,10 +236,21 @@ Json::Value AdminService::overviewStats(const auth::UserContext &user)
|
|||||||
"SELECT c.name AS category, COUNT(*) AS total "
|
"SELECT c.name AS category, COUNT(*) AS total "
|
||||||
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
|
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
|
||||||
"GROUP BY c.name ORDER BY total DESC"));
|
"GROUP BY c.name ORDER BY total DESC"));
|
||||||
response["reports_by_team"] = repositories::resultToJson(db->execSqlSync(
|
response["assignments_by_team"] = repositories::resultToJson(db->execSqlSync(
|
||||||
"SELECT t.name AS team, COUNT(*) AS total "
|
"SELECT t.name AS team, COUNT(DISTINCT ass.id) AS total "
|
||||||
"FROM assignments ass JOIN teams t ON t.id = ass.team_id "
|
"FROM assignments ass JOIN teams t ON t.id = ass.team_id "
|
||||||
"GROUP BY t.name ORDER BY total DESC"));
|
"GROUP BY t.name ORDER BY total DESC"));
|
||||||
|
response["resolved_reports_by_team"] = repositories::resultToJson(db->execSqlSync(
|
||||||
|
"SELECT t.name AS team, COUNT(DISTINCT ass.report_id) AS total "
|
||||||
|
"FROM assignments ass "
|
||||||
|
"JOIN teams t ON t.id = ass.team_id "
|
||||||
|
"JOIN reports r ON r.id = ass.report_id "
|
||||||
|
"WHERE r.status IN ('RESOLVED', 'CLOSED') "
|
||||||
|
"AND NOT EXISTS ("
|
||||||
|
" SELECT 1 FROM assignments ass2 "
|
||||||
|
" WHERE ass2.report_id = ass.report_id AND ass2.assigned_at > ass.assigned_at"
|
||||||
|
") "
|
||||||
|
"GROUP BY t.name ORDER BY total DESC"));
|
||||||
response["monthly_reports"] = repositories::resultToJson(db->execSqlSync(
|
response["monthly_reports"] = repositories::resultToJson(db->execSqlSync(
|
||||||
"SELECT DATE_FORMAT(created_at, '%Y-%m') AS month, COUNT(*) AS total "
|
"SELECT DATE_FORMAT(created_at, '%Y-%m') AS month, COUNT(*) AS total "
|
||||||
"FROM reports GROUP BY DATE_FORMAT(created_at, '%Y-%m') "
|
"FROM reports GROUP BY DATE_FORMAT(created_at, '%Y-%m') "
|
||||||
@@ -205,7 +278,7 @@ Json::Value AdminService::listModeration(
|
|||||||
const std::optional<std::string> &page,
|
const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage)
|
const std::optional<std::string> &perPage)
|
||||||
{
|
{
|
||||||
if (!auth::canManageReports(user))
|
if (!auth::isAdmin(user))
|
||||||
{
|
{
|
||||||
return failure("forbidden");
|
return failure("forbidden");
|
||||||
}
|
}
|
||||||
@@ -229,7 +302,8 @@ Json::Value AdminService::listModeration(
|
|||||||
statusValue,
|
statusValue,
|
||||||
statusValue,
|
statusValue,
|
||||||
perPageValue,
|
perPageValue,
|
||||||
offset));
|
offset),
|
||||||
|
moderationFlagSchema());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +311,7 @@ Json::Value AdminService::updateModeration(const auth::UserContext &user,
|
|||||||
long long flagId,
|
long long flagId,
|
||||||
const Json::Value &body)
|
const Json::Value &body)
|
||||||
{
|
{
|
||||||
if (!auth::canManageReports(user))
|
if (!auth::isAdmin(user))
|
||||||
{
|
{
|
||||||
return failure("forbidden");
|
return failure("forbidden");
|
||||||
}
|
}
|
||||||
@@ -299,6 +373,212 @@ Json::Value AdminService::updateModeration(const auth::UserContext &user,
|
|||||||
return success();
|
return success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Json::Value AdminService::deleteCategory(const auth::UserContext &user,
|
||||||
|
long long categoryId)
|
||||||
|
{
|
||||||
|
if (!auth::isAdmin(user))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
const auto rows = db->execSqlSync(
|
||||||
|
"SELECT id FROM report_categories WHERE id = ?",
|
||||||
|
categoryId);
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return failure("category_not_found");
|
||||||
|
}
|
||||||
|
db->execSqlSync("UPDATE report_categories SET active = FALSE WHERE id = ?",
|
||||||
|
categoryId);
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value AdminService::updateCategory(const auth::UserContext &user,
|
||||||
|
long long categoryId,
|
||||||
|
const Json::Value &body)
|
||||||
|
{
|
||||||
|
if (!auth::isAdmin(user))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
const auto rows = db->execSqlSync(
|
||||||
|
"SELECT id FROM report_categories WHERE id = ?",
|
||||||
|
categoryId);
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return failure("category_not_found");
|
||||||
|
}
|
||||||
|
if (validation::hasString(body, "name"))
|
||||||
|
{
|
||||||
|
db->execSqlSync("UPDATE report_categories SET name = ? WHERE id = ?",
|
||||||
|
body["name"].asString(),
|
||||||
|
categoryId);
|
||||||
|
}
|
||||||
|
if (body.isMember("description") && body["description"].isString())
|
||||||
|
{
|
||||||
|
db->execSqlSync("UPDATE report_categories SET description = ? WHERE id = ?",
|
||||||
|
body["description"].asString(),
|
||||||
|
categoryId);
|
||||||
|
}
|
||||||
|
if (isActiveField(body))
|
||||||
|
{
|
||||||
|
db->execSqlSync("UPDATE report_categories SET active = ? WHERE id = ?",
|
||||||
|
body["active"].asBool(),
|
||||||
|
categoryId);
|
||||||
|
}
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value AdminService::deleteArea(const auth::UserContext &user,
|
||||||
|
long long areaId)
|
||||||
|
{
|
||||||
|
if (!auth::isAdmin(user))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
const auto rows = db->execSqlSync("SELECT id FROM areas WHERE id = ?", areaId);
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return failure("area_not_found");
|
||||||
|
}
|
||||||
|
db->execSqlSync("UPDATE teams SET area_id = NULL WHERE area_id = ?", areaId);
|
||||||
|
db->execSqlSync("UPDATE reports SET area_id = NULL WHERE area_id = ?", areaId);
|
||||||
|
db->execSqlSync("DELETE FROM areas WHERE id = ?", areaId);
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value AdminService::updateArea(const auth::UserContext &user,
|
||||||
|
long long areaId,
|
||||||
|
const Json::Value &body)
|
||||||
|
{
|
||||||
|
if (!auth::isAdmin(user))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
const auto rows = db->execSqlSync("SELECT id FROM areas WHERE id = ?", areaId);
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return failure("area_not_found");
|
||||||
|
}
|
||||||
|
if (validation::hasString(body, "name"))
|
||||||
|
{
|
||||||
|
db->execSqlSync("UPDATE areas SET name = ? WHERE id = ?",
|
||||||
|
body["name"].asString(),
|
||||||
|
areaId);
|
||||||
|
}
|
||||||
|
if (body.isMember("description") && body["description"].isString())
|
||||||
|
{
|
||||||
|
db->execSqlSync("UPDATE areas SET description = ? WHERE id = ?",
|
||||||
|
body["description"].asString(),
|
||||||
|
areaId);
|
||||||
|
}
|
||||||
|
if (body["boundary_geojson"].isObject())
|
||||||
|
{
|
||||||
|
if (!isAreaInsideAllowedBoundary(body["boundary_geojson"]))
|
||||||
|
{
|
||||||
|
return failure("area_outside_allowed_boundary");
|
||||||
|
}
|
||||||
|
Json::StreamWriterBuilder writer;
|
||||||
|
db->execSqlSync(
|
||||||
|
"UPDATE areas SET boundary_geojson = CAST(? AS JSON) WHERE id = ?",
|
||||||
|
Json::writeString(writer, body["boundary_geojson"]),
|
||||||
|
areaId);
|
||||||
|
}
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value AdminService::deleteTeam(const auth::UserContext &user,
|
||||||
|
long long teamId)
|
||||||
|
{
|
||||||
|
if (!auth::isAdmin(user))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
const auto rows = db->execSqlSync("SELECT id FROM teams WHERE id = ?", teamId);
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return failure("team_not_found");
|
||||||
|
}
|
||||||
|
db->execSqlSync("UPDATE teams SET active = FALSE WHERE id = ?", teamId);
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value AdminService::updateTeam(const auth::UserContext &user,
|
||||||
|
long long teamId,
|
||||||
|
const Json::Value &body)
|
||||||
|
{
|
||||||
|
if (!auth::isAdmin(user))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
const auto rows = db->execSqlSync("SELECT id FROM teams WHERE id = ?", teamId);
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return failure("team_not_found");
|
||||||
|
}
|
||||||
|
if (validation::hasString(body, "name"))
|
||||||
|
{
|
||||||
|
db->execSqlSync("UPDATE teams SET name = ? WHERE id = ?",
|
||||||
|
body["name"].asString(),
|
||||||
|
teamId);
|
||||||
|
}
|
||||||
|
if (body.isMember("description") && body["description"].isString())
|
||||||
|
{
|
||||||
|
db->execSqlSync("UPDATE teams SET description = ? WHERE id = ?",
|
||||||
|
body["description"].asString(),
|
||||||
|
teamId);
|
||||||
|
}
|
||||||
|
if (validation::hasNumber(body, "team_category_id"))
|
||||||
|
{
|
||||||
|
const auto categoryRows = db->execSqlSync(
|
||||||
|
"SELECT id FROM team_categories WHERE id = ?",
|
||||||
|
body["team_category_id"].asInt64());
|
||||||
|
if (categoryRows.empty())
|
||||||
|
{
|
||||||
|
return failure("invalid_reference");
|
||||||
|
}
|
||||||
|
db->execSqlSync("UPDATE teams SET team_category_id = ? WHERE id = ?",
|
||||||
|
body["team_category_id"].asInt64(),
|
||||||
|
teamId);
|
||||||
|
}
|
||||||
|
if (body.isMember("area_id"))
|
||||||
|
{
|
||||||
|
if (body["area_id"].isNull())
|
||||||
|
{
|
||||||
|
db->execSqlSync("UPDATE teams SET area_id = NULL WHERE id = ?", teamId);
|
||||||
|
}
|
||||||
|
else if (body["area_id"].isNumeric())
|
||||||
|
{
|
||||||
|
const auto areaRows =
|
||||||
|
db->execSqlSync("SELECT id FROM areas WHERE id = ?",
|
||||||
|
body["area_id"].asInt64());
|
||||||
|
if (areaRows.empty())
|
||||||
|
{
|
||||||
|
return failure("invalid_reference");
|
||||||
|
}
|
||||||
|
db->execSqlSync("UPDATE teams SET area_id = ? WHERE id = ?",
|
||||||
|
body["area_id"].asInt64(),
|
||||||
|
teamId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return failure("invalid_reference");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isActiveField(body))
|
||||||
|
{
|
||||||
|
db->execSqlSync("UPDATE teams SET active = ? WHERE id = ?",
|
||||||
|
body["active"].asBool(),
|
||||||
|
teamId);
|
||||||
|
}
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value AdminService::listUsers(const auth::UserContext &user)
|
Json::Value AdminService::listUsers(const auth::UserContext &user)
|
||||||
{
|
{
|
||||||
if (!auth::isAdmin(user))
|
if (!auth::isAdmin(user))
|
||||||
@@ -310,14 +590,15 @@ Json::Value AdminService::listUsers(const auth::UserContext &user)
|
|||||||
response["users"] = repositories::resultToJson(
|
response["users"] = repositories::resultToJson(
|
||||||
drogon::app().getDbClient()->execSqlSync(
|
drogon::app().getDbClient()->execSqlSync(
|
||||||
"SELECT u.id, u.first_name, u.last_name, u.email, u.fiscal_code, "
|
"SELECT u.id, u.first_name, u.last_name, u.email, u.fiscal_code, "
|
||||||
"u.phone, u.points, u.banned, u.must_change_password, "
|
"u.phone, u.points, u.banned, "
|
||||||
"GROUP_CONCAT(r.name ORDER BY r.name SEPARATOR ',') AS roles "
|
"GROUP_CONCAT(r.name ORDER BY r.name SEPARATOR ',') AS roles "
|
||||||
"FROM users u "
|
"FROM users u "
|
||||||
"LEFT JOIN user_roles ur ON ur.user_id = u.id "
|
"LEFT JOIN user_roles ur ON ur.user_id = u.id "
|
||||||
"LEFT JOIN roles r ON r.id = ur.role_id "
|
"LEFT JOIN roles r ON r.id = ur.role_id "
|
||||||
"GROUP BY u.id, u.first_name, u.last_name, u.email, u.fiscal_code, "
|
"GROUP BY u.id, u.first_name, u.last_name, u.email, u.fiscal_code, "
|
||||||
"u.phone, u.points, u.banned, u.must_change_password "
|
"u.phone, u.points, u.banned "
|
||||||
"ORDER BY u.created_at DESC LIMIT 200"));
|
"ORDER BY u.created_at DESC LIMIT 200"),
|
||||||
|
adminUserSchema());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +619,8 @@ Json::Value AdminService::updateUserRoles(const auth::UserContext &user,
|
|||||||
return failure("user_not_found");
|
return failure("user_not_found");
|
||||||
}
|
}
|
||||||
|
|
||||||
db->execSqlSync("DELETE FROM user_roles WHERE user_id = ?", targetUserId);
|
std::vector<long long> roleIds;
|
||||||
|
roleIds.reserve(body["roles"].size());
|
||||||
for (const auto &role : body["roles"])
|
for (const auto &role : body["roles"])
|
||||||
{
|
{
|
||||||
if (!role.isString())
|
if (!role.isString())
|
||||||
@@ -352,10 +634,15 @@ Json::Value AdminService::updateUserRoles(const auth::UserContext &user,
|
|||||||
{
|
{
|
||||||
return failure("role_not_found");
|
return failure("role_not_found");
|
||||||
}
|
}
|
||||||
db->execSqlSync(
|
roleIds.push_back(roleRows[0]["id"].as<long long>());
|
||||||
"INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)",
|
}
|
||||||
|
|
||||||
|
db->execSqlSync("DELETE FROM user_roles WHERE user_id = ?", targetUserId);
|
||||||
|
for (const auto roleId : roleIds)
|
||||||
|
{
|
||||||
|
db->execSqlSync("INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)",
|
||||||
targetUserId,
|
targetUserId,
|
||||||
roleRows[0]["id"].as<long long>());
|
roleId);
|
||||||
}
|
}
|
||||||
return success();
|
return success();
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -12,11 +12,8 @@ namespace visione::services
|
|||||||
class AdminService
|
class AdminService
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Json::Value listCategories();
|
|
||||||
Json::Value createCategory(const auth::UserContext &user, const Json::Value &body);
|
Json::Value createCategory(const auth::UserContext &user, const Json::Value &body);
|
||||||
Json::Value listAreas();
|
|
||||||
Json::Value createArea(const auth::UserContext &user, const Json::Value &body);
|
Json::Value createArea(const auth::UserContext &user, const Json::Value &body);
|
||||||
Json::Value listTeams();
|
|
||||||
Json::Value createTeam(const auth::UserContext &user, const Json::Value &body);
|
Json::Value createTeam(const auth::UserContext &user, const Json::Value &body);
|
||||||
Json::Value overviewStats(const auth::UserContext &user);
|
Json::Value overviewStats(const auth::UserContext &user);
|
||||||
Json::Value listUsers(const auth::UserContext &user);
|
Json::Value listUsers(const auth::UserContext &user);
|
||||||
@@ -27,6 +24,18 @@ class AdminService
|
|||||||
Json::Value updateModeration(const auth::UserContext &user,
|
Json::Value updateModeration(const auth::UserContext &user,
|
||||||
long long flagId,
|
long long flagId,
|
||||||
const Json::Value &body);
|
const Json::Value &body);
|
||||||
|
Json::Value deleteCategory(const auth::UserContext &user, long long categoryId);
|
||||||
|
Json::Value deleteArea(const auth::UserContext &user, long long areaId);
|
||||||
|
Json::Value deleteTeam(const auth::UserContext &user, long long teamId);
|
||||||
|
Json::Value updateCategory(const auth::UserContext &user,
|
||||||
|
long long categoryId,
|
||||||
|
const Json::Value &body);
|
||||||
|
Json::Value updateArea(const auth::UserContext &user,
|
||||||
|
long long areaId,
|
||||||
|
const Json::Value &body);
|
||||||
|
Json::Value updateTeam(const auth::UserContext &user,
|
||||||
|
long long teamId,
|
||||||
|
const Json::Value &body);
|
||||||
Json::Value updateUserRoles(const auth::UserContext &user,
|
Json::Value updateUserRoles(const auth::UserContext &user,
|
||||||
long long targetUserId,
|
long long targetUserId,
|
||||||
const Json::Value &body);
|
const Json::Value &body);
|
||||||
|
|||||||
+338
-46
@@ -1,20 +1,33 @@
|
|||||||
#include "services/AuthService.h"
|
#include "services/AuthService.h"
|
||||||
|
|
||||||
#include "common/Crypto.h"
|
|
||||||
#include "common/Email.h"
|
#include "common/Email.h"
|
||||||
|
#include "common/Crypto.h"
|
||||||
#include "common/Validation.h"
|
#include "common/Validation.h"
|
||||||
|
|
||||||
#include <drogon/drogon.h>
|
#include <drogon/drogon.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <json/writer.h>
|
||||||
|
#include <optional>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace visione::services
|
namespace visione::services
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
Json::Value failure(const std::string &code)
|
constexpr std::string_view kCieProvider = "cie";
|
||||||
|
|
||||||
|
Json::Value failure(const std::string &code, const std::string &message = {})
|
||||||
{
|
{
|
||||||
Json::Value body;
|
Json::Value body;
|
||||||
body["ok"] = false;
|
body["ok"] = false;
|
||||||
body["error"] = code;
|
body["error"] = code;
|
||||||
|
if (!message.empty())
|
||||||
|
{
|
||||||
|
body["message"] = message;
|
||||||
|
}
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +54,74 @@ int tokenTtlHours()
|
|||||||
.asInt();
|
.asInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Json::Value &cieConfig()
|
||||||
|
{
|
||||||
|
return drogon::app().getCustomConfig()["cie_auth"];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cieAuthBaseUrl()
|
||||||
|
{
|
||||||
|
return cieConfig().get("service_base_url", "").asString();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cieStartPath()
|
||||||
|
{
|
||||||
|
return cieConfig().get("start_path", "/visione/start").asString();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cieSharedSecret()
|
||||||
|
{
|
||||||
|
return cieConfig().get("shared_secret", "").asString();
|
||||||
|
}
|
||||||
|
|
||||||
|
int cieStateTtlMinutes()
|
||||||
|
{
|
||||||
|
return cieConfig().get("state_ttl_minutes", 10).asInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cieDefaultFrontendRedirect()
|
||||||
|
{
|
||||||
|
return cieConfig().get("default_frontend_redirect_uri", "").asString();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ensureCitizenEmail(const std::string &email,
|
||||||
|
const std::string &fiscalCode)
|
||||||
|
{
|
||||||
|
if (!email.empty())
|
||||||
|
{
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
return fiscalCode + "@cie.local";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string buildUrlWithQuery(const std::string &base,
|
||||||
|
const std::vector<std::pair<std::string, std::string>> ¶ms)
|
||||||
|
{
|
||||||
|
std::string url = base;
|
||||||
|
url += (base.find('?') == std::string::npos) ? "?" : "&";
|
||||||
|
bool first = true;
|
||||||
|
for (const auto &[key, value] : params)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
url += "&";
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
url += drogon::utils::urlEncode(key) + "=" + drogon::utils::urlEncode(value);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string frontendRedirectWithState(const std::string &baseUrl,
|
||||||
|
const std::string &state)
|
||||||
|
{
|
||||||
|
if (baseUrl.empty())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return buildUrlWithQuery(baseUrl, {{"state", state}});
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value issueToken(long long userId)
|
Json::Value issueToken(long long userId)
|
||||||
{
|
{
|
||||||
auto token = crypto::randomToken();
|
auto token = crypto::randomToken();
|
||||||
@@ -57,12 +138,113 @@ Json::Value issueToken(long long userId)
|
|||||||
body["expires_in_hours"] = tokenTtlHours();
|
body["expires_in_hours"] = tokenTtlHours();
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> readString(const Json::Value &body, const char *key)
|
||||||
|
{
|
||||||
|
if (!body.isMember(key) || !body[key].isString())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return body[key].asString();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string normalizeFiscalCode(std::string fiscalCode)
|
||||||
|
{
|
||||||
|
std::ranges::transform(fiscalCode, fiscalCode.begin(), [](unsigned char ch) {
|
||||||
|
return static_cast<char>(std::toupper(ch));
|
||||||
|
});
|
||||||
|
return fiscalCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
long long findOrCreateCieCitizen(const Json::Value &claims)
|
||||||
|
{
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
|
||||||
|
const auto issuer = claims["issuer"].asString();
|
||||||
|
const auto subject = claims["subject"].asString();
|
||||||
|
const auto fiscalCode = normalizeFiscalCode(claims["fiscal_code"].asString());
|
||||||
|
const auto firstName = claims.get("first_name", "").asString();
|
||||||
|
const auto lastName = claims.get("last_name", "").asString();
|
||||||
|
const auto email = claims.get("email", "").asString();
|
||||||
|
|
||||||
|
const auto existingIdentity = db->execSqlSync(
|
||||||
|
"SELECT user_id FROM user_external_identities "
|
||||||
|
"WHERE provider = ? AND issuer = ? AND subject = ? LIMIT 1",
|
||||||
|
std::string(kCieProvider),
|
||||||
|
issuer,
|
||||||
|
subject);
|
||||||
|
if (!existingIdentity.empty())
|
||||||
|
{
|
||||||
|
return existingIdentity[0]["user_id"].as<long long>();
|
||||||
|
}
|
||||||
|
|
||||||
|
long long userId = 0;
|
||||||
|
const auto byFiscal = db->execSqlSync(
|
||||||
|
"SELECT id FROM users WHERE fiscal_code = ? LIMIT 1", fiscalCode);
|
||||||
|
if (!byFiscal.empty())
|
||||||
|
{
|
||||||
|
userId = byFiscal[0]["id"].as<long long>();
|
||||||
|
db->execSqlSync(
|
||||||
|
"UPDATE users SET first_name = COALESCE(NULLIF(?, ''), first_name), "
|
||||||
|
"last_name = COALESCE(NULLIF(?, ''), last_name), "
|
||||||
|
"email = COALESCE(NULLIF(?, ''), email) WHERE id = ?",
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
userId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
db->execSqlSync(
|
||||||
|
"INSERT INTO users "
|
||||||
|
"(first_name, last_name, email, fiscal_code, birth_date, birth_place, phone) "
|
||||||
|
"VALUES (?, ?, ?, ?, NULL, NULL, NULL)",
|
||||||
|
firstName.empty() ? fiscalCode : firstName,
|
||||||
|
lastName.empty() ? fiscalCode : lastName,
|
||||||
|
ensureCitizenEmail(email, fiscalCode),
|
||||||
|
fiscalCode);
|
||||||
|
const auto inserted = db->execSqlSync("SELECT LAST_INSERT_ID() AS id");
|
||||||
|
userId = inserted[0]["id"].as<long long>();
|
||||||
|
db->execSqlSync(
|
||||||
|
"INSERT INTO user_roles (user_id, role_id) "
|
||||||
|
"SELECT ?, id FROM roles WHERE name = 'citizen'",
|
||||||
|
userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::StreamWriterBuilder writer;
|
||||||
|
writer["indentation"] = "";
|
||||||
|
db->execSqlSync(
|
||||||
|
"INSERT INTO user_external_identities "
|
||||||
|
"(user_id, provider, issuer, subject, fiscal_code, raw_claims) "
|
||||||
|
"VALUES (?, ?, ?, ?, ?, CAST(? AS JSON)) "
|
||||||
|
"ON DUPLICATE KEY UPDATE user_id = VALUES(user_id), "
|
||||||
|
"fiscal_code = VALUES(fiscal_code), raw_claims = VALUES(raw_claims)",
|
||||||
|
userId,
|
||||||
|
std::string(kCieProvider),
|
||||||
|
issuer,
|
||||||
|
subject,
|
||||||
|
fiscalCode,
|
||||||
|
Json::writeString(writer, claims));
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool validateCieExchangeSignature(const std::string &rawBody,
|
||||||
|
const std::string &signatureHeader)
|
||||||
|
{
|
||||||
|
const auto secret = cieSharedSecret();
|
||||||
|
if (secret.empty() || signatureHeader.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto expected = crypto::hmacSha256Hex(secret, rawBody);
|
||||||
|
return crypto::constantTimeEquals(expected, signatureHeader);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Json::Value AuthService::registerCitizen(const Json::Value &body)
|
Json::Value AuthService::registerCitizen(const Json::Value &body)
|
||||||
{
|
{
|
||||||
const auto missing = validation::missingStrings(
|
const auto missing = validation::missingStrings(
|
||||||
body, {"first_name", "last_name", "email", "password", "fiscal_code"});
|
body, {"first_name", "last_name", "email", "fiscal_code"});
|
||||||
if (!missing.empty())
|
if (!missing.empty())
|
||||||
{
|
{
|
||||||
auto response = failure("missing_fields");
|
auto response = failure("missing_fields");
|
||||||
@@ -96,12 +278,11 @@ Json::Value AuthService::registerCitizen(const Json::Value &body)
|
|||||||
|
|
||||||
db->execSqlSync(
|
db->execSqlSync(
|
||||||
"INSERT INTO users "
|
"INSERT INTO users "
|
||||||
"(first_name, last_name, email, password_hash, fiscal_code, birth_date, "
|
"(first_name, last_name, email, fiscal_code, birth_date, "
|
||||||
"birth_place, phone) VALUES (?, ?, ?, ?, ?, NULLIF(?, ''), ?, ?)",
|
"birth_place, phone) VALUES (?, ?, ?, ?, NULLIF(?, ''), ?, ?)",
|
||||||
body["first_name"].asString(),
|
body["first_name"].asString(),
|
||||||
body["last_name"].asString(),
|
body["last_name"].asString(),
|
||||||
email,
|
email,
|
||||||
crypto::hashPassword(body["password"].asString()),
|
|
||||||
fiscalCode,
|
fiscalCode,
|
||||||
body.get("birth_date", "").asString(),
|
body.get("birth_date", "").asString(),
|
||||||
body.get("birth_place", "").asString(),
|
body.get("birth_place", "").asString(),
|
||||||
@@ -147,12 +328,12 @@ Json::Value AuthService::requestOtp(const Json::Value &body)
|
|||||||
email::EmailService mail;
|
email::EmailService mail;
|
||||||
if (!mail.sendOtp(user[0]["email"].as<std::string>(), code))
|
if (!mail.sendOtp(user[0]["email"].as<std::string>(), code))
|
||||||
{
|
{
|
||||||
LOG_WARN << "Could not send OTP email through MailHog";
|
LOG_WARN << "Could not send OTP email through " << mail.deliveryName();
|
||||||
return failure("otp_email_failed");
|
return failure("otp_email_failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto response = success();
|
auto response = success();
|
||||||
response["delivery"] = "mailhog";
|
response["delivery"] = mail.deliveryName();
|
||||||
response["expires_in_minutes"] = otpTtlMinutes();
|
response["expires_in_minutes"] = otpTtlMinutes();
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -183,68 +364,149 @@ Json::Value AuthService::verifyOtp(const Json::Value &body)
|
|||||||
return issueToken(challenge[0]["user_id"].as<long long>());
|
return issueToken(challenge[0]["user_id"].as<long long>());
|
||||||
}
|
}
|
||||||
|
|
||||||
Json::Value AuthService::loginWithPassword(const Json::Value &body)
|
Json::Value AuthService::startCieLogin(const Json::Value &body)
|
||||||
{
|
{
|
||||||
const auto missing = validation::missingStrings(body, {"email", "password"});
|
const auto frontendRedirect =
|
||||||
if (!missing.empty())
|
body.get("frontend_redirect_uri", cieDefaultFrontendRedirect()).asString();
|
||||||
|
if (frontendRedirect.empty())
|
||||||
{
|
{
|
||||||
return failure("missing_fields");
|
return failure("missing_frontend_redirect_uri");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto db = drogon::app().getDbClient();
|
const auto serviceBaseUrl = cieAuthBaseUrl();
|
||||||
const auto user = db->execSqlSync(
|
if (serviceBaseUrl.empty())
|
||||||
"SELECT id, password_hash FROM users WHERE email = ? AND banned = FALSE "
|
|
||||||
"LIMIT 1",
|
|
||||||
body["email"].asString());
|
|
||||||
if (user.empty() ||
|
|
||||||
!crypto::verifyPassword(body["password"].asString(),
|
|
||||||
user[0]["password_hash"].as<std::string>()))
|
|
||||||
{
|
{
|
||||||
return failure("invalid_credentials");
|
return failure("cie_auth_not_configured");
|
||||||
}
|
}
|
||||||
return issueToken(user[0]["id"].as<long long>());
|
|
||||||
|
const auto state = crypto::randomToken();
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
db->execSqlSync(
|
||||||
|
"INSERT INTO cie_auth_sessions "
|
||||||
|
"(state, frontend_redirect_uri, status, expires_at) "
|
||||||
|
"VALUES (?, ?, 'PENDING', DATE_ADD(NOW(), INTERVAL ? MINUTE))",
|
||||||
|
state,
|
||||||
|
frontendRedirect,
|
||||||
|
cieStateTtlMinutes());
|
||||||
|
|
||||||
|
auto response = success();
|
||||||
|
response["state"] = state;
|
||||||
|
response["authorization_url"] = buildUrlWithQuery(
|
||||||
|
serviceBaseUrl + cieStartPath(),
|
||||||
|
{{"state", state}, {"frontend_redirect_uri", frontendRedirect}});
|
||||||
|
response["status"] = "pending";
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Json::Value AuthService::changePassword(const auth::UserContext &user,
|
Json::Value AuthService::completeCieLogin(const std::string &rawBody,
|
||||||
const Json::Value &body)
|
const Json::Value &body,
|
||||||
|
const std::string &signatureHeader)
|
||||||
{
|
{
|
||||||
const auto missing =
|
if (!validateCieExchangeSignature(rawBody, signatureHeader))
|
||||||
validation::missingStrings(body, {"current_password", "new_password"});
|
{
|
||||||
if (!missing.empty())
|
return failure("invalid_signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto state = readString(body, "state");
|
||||||
|
const auto issuer = readString(body, "issuer");
|
||||||
|
const auto subject = readString(body, "subject");
|
||||||
|
const auto fiscalCode = readString(body, "fiscal_code");
|
||||||
|
if (!state || !issuer || !subject || !fiscalCode)
|
||||||
{
|
{
|
||||||
return failure("missing_fields");
|
return failure("missing_fields");
|
||||||
}
|
}
|
||||||
if (body["new_password"].asString().size() < 10)
|
if (!validation::isValidFiscalCode(*fiscalCode))
|
||||||
{
|
{
|
||||||
return failure("weak_password");
|
return failure("invalid_fiscal_code");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto db = drogon::app().getDbClient();
|
auto db = drogon::app().getDbClient();
|
||||||
const auto current = db->execSqlSync(
|
const auto session = db->execSqlSync(
|
||||||
"SELECT password_hash FROM users WHERE id = ? AND banned = FALSE",
|
"SELECT id, frontend_redirect_uri, status FROM cie_auth_sessions "
|
||||||
user.id);
|
"WHERE state = ? AND consumed_at IS NULL AND expires_at > NOW() LIMIT 1",
|
||||||
if (current.empty() ||
|
*state);
|
||||||
!crypto::verifyPassword(
|
if (session.empty())
|
||||||
body["current_password"].asString(),
|
|
||||||
current[0]["password_hash"].as<std::string>()))
|
|
||||||
{
|
{
|
||||||
return failure("invalid_credentials");
|
return failure("invalid_or_expired_state");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto sessionId = session[0]["id"].as<long long>();
|
||||||
|
const auto status = session[0]["status"].as<std::string>();
|
||||||
|
if (status == "AUTHENTICATED")
|
||||||
|
{
|
||||||
|
auto response = success();
|
||||||
|
response["redirect_url"] = frontendRedirectWithState(
|
||||||
|
session[0]["frontend_redirect_uri"].as<std::string>(), *state);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value claims(Json::objectValue);
|
||||||
|
claims["issuer"] = *issuer;
|
||||||
|
claims["subject"] = *subject;
|
||||||
|
claims["fiscal_code"] = normalizeFiscalCode(*fiscalCode);
|
||||||
|
claims["first_name"] = body.get("first_name", "").asString();
|
||||||
|
claims["last_name"] = body.get("last_name", "").asString();
|
||||||
|
claims["email"] = body.get("email", "").asString();
|
||||||
|
|
||||||
|
const auto userId = findOrCreateCieCitizen(claims);
|
||||||
|
const auto token = issueToken(userId);
|
||||||
|
|
||||||
|
Json::StreamWriterBuilder writer;
|
||||||
|
writer["indentation"] = "";
|
||||||
db->execSqlSync(
|
db->execSqlSync(
|
||||||
"UPDATE users SET password_hash = ?, must_change_password = FALSE "
|
"UPDATE cie_auth_sessions "
|
||||||
"WHERE id = ?",
|
"SET status = 'AUTHENTICATED', issuer = ?, subject = ?, fiscal_code = ?, "
|
||||||
crypto::hashPassword(body["new_password"].asString()),
|
"authenticated_user_id = ?, result_token = ?, raw_claims = CAST(? AS JSON), "
|
||||||
user.id);
|
"consumed_at = NOW() WHERE id = ?",
|
||||||
return success();
|
*issuer,
|
||||||
|
*subject,
|
||||||
|
normalizeFiscalCode(*fiscalCode),
|
||||||
|
userId,
|
||||||
|
token["token"].asString(),
|
||||||
|
Json::writeString(writer, claims),
|
||||||
|
sessionId);
|
||||||
|
|
||||||
|
auto response = success();
|
||||||
|
response["redirect_url"] = frontendRedirectWithState(
|
||||||
|
session[0]["frontend_redirect_uri"].as<std::string>(), *state);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value AuthService::cieSessionStatus(const std::string &state)
|
||||||
|
{
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
const auto session = db->execSqlSync(
|
||||||
|
"SELECT status, authenticated_user_id, result_token FROM cie_auth_sessions "
|
||||||
|
"WHERE state = ? LIMIT 1",
|
||||||
|
state);
|
||||||
|
if (session.empty())
|
||||||
|
{
|
||||||
|
return failure("session_not_found");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto response = success();
|
||||||
|
const auto status = session[0]["status"].as<std::string>();
|
||||||
|
if (status == "AUTHENTICATED")
|
||||||
|
{
|
||||||
|
response["status"] = "authenticated";
|
||||||
|
response["auth"]["token"] = session[0]["result_token"].as<std::string>();
|
||||||
|
response["auth"]["token_type"] = "Bearer";
|
||||||
|
response["auth"]["expires_in_hours"] = tokenTtlHours();
|
||||||
|
response["user_id"] =
|
||||||
|
Json::Int64(session[0]["authenticated_user_id"].as<long long>());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
response["status"] = status == "FAILED" ? "failed" : "pending";
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Json::Value AuthService::currentUser(const auth::UserContext &user)
|
Json::Value AuthService::currentUser(const auth::UserContext &user)
|
||||||
{
|
{
|
||||||
auto db = drogon::app().getDbClient();
|
auto db = drogon::app().getDbClient();
|
||||||
const auto result = db->execSqlSync(
|
const auto result = db->execSqlSync(
|
||||||
"SELECT id, first_name, last_name, email, fiscal_code, phone, points, "
|
"SELECT id, first_name, last_name, email, fiscal_code, phone, points "
|
||||||
"must_change_password FROM users WHERE id = ?",
|
"FROM users WHERE id = ?",
|
||||||
user.id);
|
user.id);
|
||||||
|
|
||||||
auto response = success();
|
auto response = success();
|
||||||
@@ -257,8 +519,6 @@ Json::Value AuthService::currentUser(const auth::UserContext &user)
|
|||||||
response["user"]["fiscal_code"] = result[0]["fiscal_code"].as<std::string>();
|
response["user"]["fiscal_code"] = result[0]["fiscal_code"].as<std::string>();
|
||||||
response["user"]["phone"] = result[0]["phone"].as<std::string>();
|
response["user"]["phone"] = result[0]["phone"].as<std::string>();
|
||||||
response["user"]["points"] = result[0]["points"].as<int>();
|
response["user"]["points"] = result[0]["points"].as<int>();
|
||||||
response["user"]["must_change_password"] =
|
|
||||||
result[0]["must_change_password"].as<bool>();
|
|
||||||
response["user"]["role"] = user.role;
|
response["user"]["role"] = user.role;
|
||||||
for (const auto &role : user.roles)
|
for (const auto &role : user.roles)
|
||||||
{
|
{
|
||||||
@@ -271,4 +531,36 @@ Json::Value AuthService::currentUser(const auth::UserContext &user)
|
|||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Json::Value AuthService::editUser(const auth::UserContext &user,
|
||||||
|
Json::Value::const_iterator::reference body)
|
||||||
|
{
|
||||||
|
const auto allowedFields = {"first_name", "last_name", "phone"};
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
bool hasUpdates = false;
|
||||||
|
|
||||||
|
for (const auto &field : allowedFields)
|
||||||
|
{
|
||||||
|
if (body.isMember(field))
|
||||||
|
{
|
||||||
|
if (!body[field].isString())
|
||||||
|
{
|
||||||
|
return failure("invalid_field_type");
|
||||||
|
}
|
||||||
|
|
||||||
|
db->execSqlSync(
|
||||||
|
"UPDATE users SET " + std::string(field) + " = ? WHERE id = ?",
|
||||||
|
body[field].asString(),
|
||||||
|
user.id);
|
||||||
|
hasUpdates = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasUpdates)
|
||||||
|
{
|
||||||
|
return failure("missing_fields");
|
||||||
|
}
|
||||||
|
|
||||||
|
return success();
|
||||||
|
}
|
||||||
} // namespace visione::services
|
} // namespace visione::services
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ class AuthService
|
|||||||
Json::Value registerCitizen(const Json::Value &body);
|
Json::Value registerCitizen(const Json::Value &body);
|
||||||
Json::Value requestOtp(const Json::Value &body);
|
Json::Value requestOtp(const Json::Value &body);
|
||||||
Json::Value verifyOtp(const Json::Value &body);
|
Json::Value verifyOtp(const Json::Value &body);
|
||||||
Json::Value loginWithPassword(const Json::Value &body);
|
Json::Value startCieLogin(const Json::Value &body);
|
||||||
Json::Value changePassword(const auth::UserContext &user, const Json::Value &body);
|
Json::Value completeCieLogin(const std::string &rawBody,
|
||||||
|
const Json::Value &body,
|
||||||
|
const std::string &signatureHeader);
|
||||||
|
Json::Value cieSessionStatus(const std::string &state);
|
||||||
Json::Value currentUser(const auth::UserContext &user);
|
Json::Value currentUser(const auth::UserContext &user);
|
||||||
|
Json::Value editUser(const auth::UserContext & user, Json::Value::const_iterator::reference body);
|
||||||
};
|
};
|
||||||
} // namespace visione::services
|
} // namespace visione::services
|
||||||
|
|||||||
+500
-27
@@ -1,21 +1,27 @@
|
|||||||
#include "services/ReportService.h"
|
#include "services/ReportService.h"
|
||||||
|
|
||||||
|
#include "common/Crypto.h"
|
||||||
#include "common/GeoJson.h"
|
#include "common/GeoJson.h"
|
||||||
#include "common/Validation.h"
|
#include "common/Validation.h"
|
||||||
#include "repositories/Sql.h"
|
#include "repositories/Sql.h"
|
||||||
#include "services/UserService.h"
|
#include "services/UserService.h"
|
||||||
|
|
||||||
#include <drogon/drogon.h>
|
#include <drogon/drogon.h>
|
||||||
|
#include <drogon/orm/Exception.h>
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
namespace visione::services
|
namespace visione::services
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
using repositories::JsonFieldType;
|
||||||
|
using repositories::JsonRowSchema;
|
||||||
|
|
||||||
Json::Value failure(const std::string &code)
|
Json::Value failure(const std::string &code)
|
||||||
{
|
{
|
||||||
Json::Value body;
|
Json::Value body;
|
||||||
@@ -31,6 +37,14 @@ Json::Value success()
|
|||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Json::Value successWithLink(const std::string &url, const std::string &expiresAt)
|
||||||
|
{
|
||||||
|
auto body = success();
|
||||||
|
body["url"] = url;
|
||||||
|
body["expires_at"] = expiresAt;
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
bool isAllowedStatus(const std::string &status)
|
bool isAllowedStatus(const std::string &status)
|
||||||
{
|
{
|
||||||
static const std::set<std::string> statuses = {
|
static const std::set<std::string> statuses = {
|
||||||
@@ -53,11 +67,48 @@ bool canTransition(const std::string &from, const std::string &to)
|
|||||||
return it != transitions.end() && it->second.contains(to);
|
return it != transitions.end() && it->second.contains(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string maskedLastName(const std::string &lastName)
|
||||||
|
{
|
||||||
|
if (lastName.empty())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return std::string(1, static_cast<char>(std::toupper(lastName.front()))) + ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Json::Value> loadAllowedBoundary(const std::string &code)
|
||||||
|
{
|
||||||
|
const auto rows = drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT JSON_UNQUOTE(JSON_EXTRACT(boundary_geojson, '$')) AS geojson "
|
||||||
|
"FROM allowed_boundaries WHERE code = ? AND active = TRUE LIMIT 1",
|
||||||
|
code);
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::CharReaderBuilder builder;
|
||||||
|
Json::Value geojson;
|
||||||
|
std::string errors;
|
||||||
|
std::istringstream stream(rows[0]["geojson"].as<std::string>());
|
||||||
|
if (!Json::parseFromStream(builder, stream, &geojson, &errors))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return geojson;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool insideAllowedBoundary(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
const auto boundary = loadAllowedBoundary("BARI");
|
||||||
|
return boundary && geojson::pointInGeometry(latitude, longitude, *boundary);
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<long long> findArea(double latitude, double longitude)
|
std::optional<long long> findArea(double latitude, double longitude)
|
||||||
{
|
{
|
||||||
const auto result = drogon::app().getDbClient()->execSqlSync(
|
const auto result = drogon::app().getDbClient()->execSqlSync(
|
||||||
"SELECT id, JSON_UNQUOTE(JSON_EXTRACT(boundary_geojson, '$')) AS geojson "
|
"SELECT id, JSON_UNQUOTE(JSON_EXTRACT(boundary_geojson, '$')) AS geojson "
|
||||||
"FROM areas WHERE active = TRUE");
|
"FROM areas");
|
||||||
|
|
||||||
Json::CharReaderBuilder builder;
|
Json::CharReaderBuilder builder;
|
||||||
for (const auto &row : result)
|
for (const auto &row : result)
|
||||||
@@ -67,7 +118,7 @@ std::optional<long long> findArea(double latitude, double longitude)
|
|||||||
const auto raw = row["geojson"].as<std::string>();
|
const auto raw = row["geojson"].as<std::string>();
|
||||||
std::istringstream stream(raw);
|
std::istringstream stream(raw);
|
||||||
if (Json::parseFromStream(builder, stream, &geojson, &errors) &&
|
if (Json::parseFromStream(builder, stream, &geojson, &errors) &&
|
||||||
geojson::pointInPolygon(latitude, longitude, geojson))
|
geojson::pointInGeometry(latitude, longitude, geojson))
|
||||||
{
|
{
|
||||||
return row["id"].as<long long>();
|
return row["id"].as<long long>();
|
||||||
}
|
}
|
||||||
@@ -106,6 +157,41 @@ bool canModifyReport(const auth::UserContext &user, long long reportId)
|
|||||||
return !result.empty();
|
return !result.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool canOwnOrModifyReport(const auth::UserContext &user, long long reportId)
|
||||||
|
{
|
||||||
|
if (auth::isAdmin(user))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!auth::canOperateReports(user))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto result = drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT assigned_operator_id FROM reports WHERE id = ?",
|
||||||
|
reportId);
|
||||||
|
if (result.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return result[0]["assigned_operator_id"].isNull() ||
|
||||||
|
result[0]["assigned_operator_id"].as<long long>() == user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canFlagReportContent(const auth::UserContext &user, long long reportId)
|
||||||
|
{
|
||||||
|
if (!auth::canOperateReports(user))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto result = drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT id FROM reports WHERE id = ? AND assigned_operator_id = ?",
|
||||||
|
reportId,
|
||||||
|
user.id);
|
||||||
|
return !result.empty();
|
||||||
|
}
|
||||||
|
|
||||||
bool ownsReport(const auth::UserContext &user, long long reportId)
|
bool ownsReport(const auth::UserContext &user, long long reportId)
|
||||||
{
|
{
|
||||||
const auto result = drogon::app().getDbClient()->execSqlSync(
|
const auto result = drogon::app().getDbClient()->execSqlSync(
|
||||||
@@ -120,6 +206,11 @@ bool canInteractWithReport(const auth::UserContext &user, long long reportId)
|
|||||||
return ownsReport(user, reportId) || canModifyReport(user, reportId);
|
return ownsReport(user, reportId) || canModifyReport(user, reportId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool canCreateCitizenReport(const auth::UserContext &user)
|
||||||
|
{
|
||||||
|
return !auth::canManageReports(user);
|
||||||
|
}
|
||||||
|
|
||||||
int boundedPage(const std::optional<std::string> &value)
|
int boundedPage(const std::optional<std::string> &value)
|
||||||
{
|
{
|
||||||
if (!value)
|
if (!value)
|
||||||
@@ -152,6 +243,57 @@ int boundedPerPage(const std::optional<std::string> &value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const JsonRowSchema &reportSchema()
|
||||||
|
{
|
||||||
|
static const JsonRowSchema schema{
|
||||||
|
{"id", JsonFieldType::UInt64},
|
||||||
|
{"user_id", JsonFieldType::UInt64},
|
||||||
|
{"category_id", JsonFieldType::UInt64},
|
||||||
|
{"area_id", JsonFieldType::UInt64},
|
||||||
|
{"assigned_operator_id", JsonFieldType::UInt64},
|
||||||
|
{"latitude", JsonFieldType::Double},
|
||||||
|
{"longitude", JsonFieldType::Double}};
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonRowSchema &reportHistorySchema()
|
||||||
|
{
|
||||||
|
static const JsonRowSchema schema{
|
||||||
|
{"id", JsonFieldType::UInt64},
|
||||||
|
{"report_id", JsonFieldType::UInt64},
|
||||||
|
{"changed_by_user_id", JsonFieldType::UInt64}};
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonRowSchema &reportMediaSchema()
|
||||||
|
{
|
||||||
|
static const JsonRowSchema schema{
|
||||||
|
{"id", JsonFieldType::UInt64},
|
||||||
|
{"report_id", JsonFieldType::UInt64},
|
||||||
|
{"size_bytes", JsonFieldType::UInt64}};
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonRowSchema &messageSchema()
|
||||||
|
{
|
||||||
|
static const JsonRowSchema schema{
|
||||||
|
{"id", JsonFieldType::UInt64},
|
||||||
|
{"report_id", JsonFieldType::UInt64},
|
||||||
|
{"user_id", JsonFieldType::UInt64}};
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonRowSchema &openDataReportSchema()
|
||||||
|
{
|
||||||
|
static const JsonRowSchema schema{
|
||||||
|
{"id", JsonFieldType::UInt64},
|
||||||
|
{"category_id", JsonFieldType::UInt64},
|
||||||
|
{"area_id", JsonFieldType::UInt64},
|
||||||
|
{"latitude", JsonFieldType::Double},
|
||||||
|
{"longitude", JsonFieldType::Double}};
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value pagedReportResponse(const drogon::orm::Result &result,
|
Json::Value pagedReportResponse(const drogon::orm::Result &result,
|
||||||
int page,
|
int page,
|
||||||
int perPage)
|
int perPage)
|
||||||
@@ -159,7 +301,7 @@ Json::Value pagedReportResponse(const drogon::orm::Result &result,
|
|||||||
auto response = success();
|
auto response = success();
|
||||||
response["page"] = page;
|
response["page"] = page;
|
||||||
response["per_page"] = perPage;
|
response["per_page"] = perPage;
|
||||||
response["reports"] = repositories::resultToJson(result);
|
response["reports"] = repositories::resultToJson(result, reportSchema());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +329,27 @@ std::optional<long long> assignedOperator(long long reportId)
|
|||||||
return rows[0]["assigned_operator_id"].as<long long>();
|
return rows[0]["assigned_operator_id"].as<long long>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<long long> activeAssignmentId(long long reportId)
|
||||||
|
{
|
||||||
|
const auto rows = drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT id FROM assignments WHERE report_id = ? AND ended_at IS NULL "
|
||||||
|
"ORDER BY assigned_at DESC LIMIT 1",
|
||||||
|
reportId);
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return rows[0]["id"].as<long long>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void revokeActiveLinksForReport(long long reportId)
|
||||||
|
{
|
||||||
|
drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"UPDATE intervention_access_links SET revoked_at = NOW() "
|
||||||
|
"WHERE report_id = ? AND revoked_at IS NULL AND expires_at > NOW()",
|
||||||
|
reportId);
|
||||||
|
}
|
||||||
|
|
||||||
void notifyReportOwner(long long reportId,
|
void notifyReportOwner(long long reportId,
|
||||||
const std::string &type,
|
const std::string &type,
|
||||||
const std::string &title,
|
const std::string &title,
|
||||||
@@ -198,11 +361,60 @@ void notifyReportOwner(long long reportId,
|
|||||||
UserService::createNotification(*owner, type, title, body, reportId);
|
UserService::createNotification(*owner, type, title, body, reportId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Json::Value buildInterventionSummaryRow(const drogon::orm::Row &row,
|
||||||
|
const drogon::orm::Result &mediaRows,
|
||||||
|
const drogon::orm::Result &messageRows,
|
||||||
|
bool publicView)
|
||||||
|
{
|
||||||
|
auto response = success();
|
||||||
|
Json::Value summary = repositories::rowToJson(row, reportSchema());
|
||||||
|
summary["category_name"] = row["category_name"].as<std::string>();
|
||||||
|
if (!row["area_name"].isNull())
|
||||||
|
{
|
||||||
|
summary["area_name"] = row["area_name"].as<std::string>();
|
||||||
|
}
|
||||||
|
if (!row["assigned_team_name"].isNull())
|
||||||
|
{
|
||||||
|
summary["assigned_team_name"] = row["assigned_team_name"].as<std::string>();
|
||||||
|
}
|
||||||
|
if (publicView)
|
||||||
|
{
|
||||||
|
summary["citizen_name"] = row["first_name"].as<std::string>() + " " +
|
||||||
|
maskedLastName(row["last_name"].as<std::string>());
|
||||||
|
if (!row["phone"].isNull())
|
||||||
|
{
|
||||||
|
summary["phone"] = row["phone"].as<std::string>();
|
||||||
|
}
|
||||||
|
response["summary"] = std::move(summary);
|
||||||
|
response["media"] = repositories::resultToJson(mediaRows, reportMediaSchema());
|
||||||
|
response["latest_messages"] =
|
||||||
|
repositories::resultToJson(messageRows, messageSchema());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
summary["first_name"] = row["first_name"].as<std::string>();
|
||||||
|
summary["last_name"] = row["last_name"].as<std::string>();
|
||||||
|
if (!row["phone"].isNull())
|
||||||
|
{
|
||||||
|
summary["phone"] = row["phone"].as<std::string>();
|
||||||
|
}
|
||||||
|
summary["email"] = row["email"].as<std::string>();
|
||||||
|
response["summary"] = std::move(summary);
|
||||||
|
response["media"] = repositories::resultToJson(mediaRows, reportMediaSchema());
|
||||||
|
response["latest_messages"] =
|
||||||
|
repositories::resultToJson(messageRows, messageSchema());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Json::Value ReportService::createReport(const auth::UserContext &user,
|
Json::Value ReportService::createReport(const auth::UserContext &user,
|
||||||
const Json::Value &body)
|
const Json::Value &body)
|
||||||
{
|
{
|
||||||
|
if (!canCreateCitizenReport(user))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
|
||||||
const auto missing =
|
const auto missing =
|
||||||
validation::missingStrings(body, {"address", "description"});
|
validation::missingStrings(body, {"address", "description"});
|
||||||
if (!missing.empty() || !validation::hasNumber(body, "category_id") ||
|
if (!missing.empty() || !validation::hasNumber(body, "category_id") ||
|
||||||
@@ -214,6 +426,10 @@ Json::Value ReportService::createReport(const auth::UserContext &user,
|
|||||||
|
|
||||||
const auto latitude = body["latitude"].asDouble();
|
const auto latitude = body["latitude"].asDouble();
|
||||||
const auto longitude = body["longitude"].asDouble();
|
const auto longitude = body["longitude"].asDouble();
|
||||||
|
if (!insideAllowedBoundary(latitude, longitude))
|
||||||
|
{
|
||||||
|
return failure("coordinates_outside_allowed_boundary");
|
||||||
|
}
|
||||||
const auto areaId = findArea(latitude, longitude);
|
const auto areaId = findArea(latitude, longitude);
|
||||||
|
|
||||||
auto db = drogon::app().getDbClient();
|
auto db = drogon::app().getDbClient();
|
||||||
@@ -264,8 +480,13 @@ Json::Value ReportService::createReport(const auth::UserContext &user,
|
|||||||
|
|
||||||
if (body["media"].isArray())
|
if (body["media"].isArray())
|
||||||
{
|
{
|
||||||
|
size_t mediaCount = 0;
|
||||||
for (const auto &item : body["media"])
|
for (const auto &item : body["media"])
|
||||||
{
|
{
|
||||||
|
if (mediaCount >= 5)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (item.isString() && !item.asString().empty())
|
if (item.isString() && !item.asString().empty())
|
||||||
{
|
{
|
||||||
db->execSqlSync(
|
db->execSqlSync(
|
||||||
@@ -273,6 +494,7 @@ Json::Value ReportService::createReport(const auth::UserContext &user,
|
|||||||
"VALUES (?, ?, 'IMAGE')",
|
"VALUES (?, ?, 'IMAGE')",
|
||||||
reportId,
|
reportId,
|
||||||
item.asString());
|
item.asString());
|
||||||
|
++mediaCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +557,7 @@ Json::Value ReportService::listReports(const auth::UserContext &user,
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto response = success();
|
auto response = success();
|
||||||
response["reports"] = repositories::resultToJson(result);
|
response["reports"] = repositories::resultToJson(result, reportSchema());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,6 +631,43 @@ Json::Value ReportService::otherOperatorReports(
|
|||||||
return pagedReportResponse(result, pageValue, perPageValue);
|
return pagedReportResponse(result, pageValue, perPageValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::listCategories(const auth::UserContext &user)
|
||||||
|
{
|
||||||
|
if (user.id == 0)
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
auto response = success();
|
||||||
|
response["categories"] = repositories::resultToJson(
|
||||||
|
drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT * FROM report_categories WHERE active = TRUE ORDER BY name"));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::listAreas()
|
||||||
|
{
|
||||||
|
auto response = success();
|
||||||
|
response["areas"] = repositories::resultToJson(
|
||||||
|
drogon::app().getDbClient()->execSqlSync("SELECT * FROM areas ORDER BY name"));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::listOperatorTeams(const auth::UserContext &user)
|
||||||
|
{
|
||||||
|
if (!auth::canManageReports(user))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
auto response = success();
|
||||||
|
response["teams"] = repositories::resultToJson(
|
||||||
|
drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"SELECT t.*, tc.name AS category_name, a.name AS area_name "
|
||||||
|
"FROM teams t JOIN team_categories tc ON tc.id = t.team_category_id "
|
||||||
|
"LEFT JOIN areas a ON a.id = t.area_id "
|
||||||
|
"WHERE t.active = TRUE ORDER BY t.name"));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value ReportService::reportDetail(const auth::UserContext &user,
|
Json::Value ReportService::reportDetail(const auth::UserContext &user,
|
||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
@@ -429,13 +688,15 @@ Json::Value ReportService::reportDetail(const auth::UserContext &user,
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto response = success();
|
auto response = success();
|
||||||
response["report"] = repositories::rowToJson(report[0]);
|
response["report"] = repositories::rowToJson(report[0], reportSchema());
|
||||||
response["history"] = repositories::resultToJson(db->execSqlSync(
|
response["history"] = repositories::resultToJson(db->execSqlSync(
|
||||||
"SELECT * FROM report_status_history WHERE report_id = ? ORDER BY id",
|
"SELECT * FROM report_status_history WHERE report_id = ? ORDER BY id",
|
||||||
reportId));
|
reportId),
|
||||||
|
reportHistorySchema());
|
||||||
response["media"] = repositories::resultToJson(db->execSqlSync(
|
response["media"] = repositories::resultToJson(db->execSqlSync(
|
||||||
"SELECT * FROM report_photos WHERE report_id = ? ORDER BY id",
|
"SELECT * FROM report_photos WHERE report_id = ? ORDER BY id",
|
||||||
reportId));
|
reportId),
|
||||||
|
reportMediaSchema());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,17 +724,64 @@ Json::Value ReportService::interventionSummary(const auth::UserContext &user,
|
|||||||
{
|
{
|
||||||
return failure("report_not_found");
|
return failure("report_not_found");
|
||||||
}
|
}
|
||||||
auto response = success();
|
return buildInterventionSummaryRow(report[0],
|
||||||
response["summary"] = repositories::rowToJson(report[0]);
|
db->execSqlSync(
|
||||||
response["media"] = repositories::resultToJson(db->execSqlSync(
|
|
||||||
"SELECT * FROM report_photos WHERE report_id = ? ORDER BY id",
|
"SELECT * FROM report_photos WHERE report_id = ? ORDER BY id",
|
||||||
reportId));
|
reportId),
|
||||||
response["latest_messages"] = repositories::resultToJson(db->execSqlSync(
|
db->execSqlSync(
|
||||||
"SELECT m.id, m.user_id, u.first_name, u.last_name, m.body, m.created_at "
|
"SELECT m.id, m.user_id, u.first_name, u.last_name, m.body, m.created_at "
|
||||||
"FROM messages m JOIN users u ON u.id = m.user_id "
|
"FROM messages m JOIN users u ON u.id = m.user_id "
|
||||||
"WHERE m.report_id = ? ORDER BY m.created_at DESC LIMIT 5",
|
"WHERE m.report_id = ? ORDER BY m.created_at DESC LIMIT 5",
|
||||||
reportId));
|
reportId),
|
||||||
return response;
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::publicInterventionSummary(const std::string &token)
|
||||||
|
{
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
const auto rows = db->execSqlSync(
|
||||||
|
"SELECT l.report_id "
|
||||||
|
"FROM intervention_access_links l "
|
||||||
|
"WHERE l.token_hash = ? AND l.revoked_at IS NULL AND l.expires_at > NOW() "
|
||||||
|
"LIMIT 1",
|
||||||
|
crypto::hashSecret(token));
|
||||||
|
if (rows.empty())
|
||||||
|
{
|
||||||
|
return failure("intervention_link_not_found");
|
||||||
|
}
|
||||||
|
const auto reportId = rows[0]["report_id"].as<long long>();
|
||||||
|
const auto report = db->execSqlSync(
|
||||||
|
"SELECT r.*, c.name AS category_name, a.name AS area_name, "
|
||||||
|
"u.first_name, u.last_name, u.phone, "
|
||||||
|
"t.name AS assigned_team_name "
|
||||||
|
"FROM reports r "
|
||||||
|
"JOIN users u ON u.id = r.user_id "
|
||||||
|
"JOIN report_categories c ON c.id = r.category_id "
|
||||||
|
"LEFT JOIN areas a ON a.id = r.area_id "
|
||||||
|
"LEFT JOIN assignments ass ON ass.report_id = r.id AND ass.ended_at IS NULL "
|
||||||
|
"LEFT JOIN teams t ON t.id = ass.team_id "
|
||||||
|
"WHERE r.id = ?",
|
||||||
|
reportId);
|
||||||
|
if (report.empty())
|
||||||
|
{
|
||||||
|
return failure("intervention_link_not_found");
|
||||||
|
}
|
||||||
|
db->execSqlSync(
|
||||||
|
"UPDATE intervention_access_links SET last_accessed_at = NOW() "
|
||||||
|
"WHERE token_hash = ?",
|
||||||
|
crypto::hashSecret(token));
|
||||||
|
return buildInterventionSummaryRow(report[0],
|
||||||
|
db->execSqlSync(
|
||||||
|
"SELECT * FROM report_photos WHERE report_id = ? ORDER BY id",
|
||||||
|
reportId),
|
||||||
|
db->execSqlSync(
|
||||||
|
"SELECT m.id, m.user_id, u.first_name, u.last_name, m.body, m.created_at "
|
||||||
|
"FROM messages m JOIN users u ON u.id = m.user_id "
|
||||||
|
"WHERE m.report_id = ? AND u.id <> ? "
|
||||||
|
"ORDER BY m.created_at DESC LIMIT 5",
|
||||||
|
reportId,
|
||||||
|
report[0]["user_id"].as<long long>()),
|
||||||
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Json::Value ReportService::updateStatus(const auth::UserContext &user,
|
Json::Value ReportService::updateStatus(const auth::UserContext &user,
|
||||||
@@ -542,7 +850,7 @@ Json::Value ReportService::updateStatus(const auth::UserContext &user,
|
|||||||
Json::Value ReportService::takeReport(const auth::UserContext &user,
|
Json::Value ReportService::takeReport(const auth::UserContext &user,
|
||||||
long long reportId)
|
long long reportId)
|
||||||
{
|
{
|
||||||
if (!auth::canManageReports(user))
|
if (!auth::canOperateReports(user))
|
||||||
{
|
{
|
||||||
return failure("forbidden");
|
return failure("forbidden");
|
||||||
}
|
}
|
||||||
@@ -628,7 +936,8 @@ Json::Value ReportService::listMessages(const auth::UserContext &user,
|
|||||||
"SELECT m.*, u.first_name, u.last_name FROM messages m "
|
"SELECT m.*, u.first_name, u.last_name FROM messages m "
|
||||||
"JOIN users u ON u.id = m.user_id WHERE m.report_id = ? "
|
"JOIN users u ON u.id = m.user_id WHERE m.report_id = ? "
|
||||||
"ORDER BY m.created_at",
|
"ORDER BY m.created_at",
|
||||||
reportId));
|
reportId),
|
||||||
|
messageSchema());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,33 +945,68 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
|
|||||||
long long reportId,
|
long long reportId,
|
||||||
const Json::Value &body)
|
const Json::Value &body)
|
||||||
{
|
{
|
||||||
if (!canModifyReport(user, reportId) || !validation::hasNumber(body, "team_id"))
|
if (!canOwnOrModifyReport(user, reportId))
|
||||||
{
|
{
|
||||||
return failure("forbidden");
|
return failure("forbidden");
|
||||||
}
|
}
|
||||||
|
if (!validation::hasNumber(body, "team_id"))
|
||||||
|
{
|
||||||
|
return failure("invalid_team");
|
||||||
|
}
|
||||||
|
|
||||||
auto db = drogon::app().getDbClient();
|
auto db = drogon::app().getDbClient();
|
||||||
const auto current = db->execSqlSync(
|
const auto current = db->execSqlSync(
|
||||||
"SELECT status FROM reports WHERE id = ?",
|
"SELECT status, assigned_operator_id FROM reports WHERE id = ?",
|
||||||
reportId);
|
reportId);
|
||||||
if (current.empty())
|
if (current.empty())
|
||||||
{
|
{
|
||||||
return failure("report_not_found");
|
return failure("report_not_found");
|
||||||
}
|
}
|
||||||
|
if (!auth::isAdmin(user) && !current[0]["assigned_operator_id"].isNull() &&
|
||||||
|
current[0]["assigned_operator_id"].as<long long>() != user.id)
|
||||||
|
{
|
||||||
|
return failure("report_owned_by_other_operator");
|
||||||
|
}
|
||||||
const auto oldStatus = current[0]["status"].as<std::string>();
|
const auto oldStatus = current[0]["status"].as<std::string>();
|
||||||
if (!canTransition(oldStatus, "ASSIGNED"))
|
if (oldStatus != "OPEN" && !canTransition(oldStatus, "ASSIGNED"))
|
||||||
{
|
{
|
||||||
return failure("invalid_status_transition");
|
return failure("invalid_status_transition");
|
||||||
}
|
}
|
||||||
|
const auto team = db->execSqlSync(
|
||||||
|
"SELECT id, active FROM teams WHERE id = ?",
|
||||||
|
body["team_id"].asInt64());
|
||||||
|
if (team.empty())
|
||||||
|
{
|
||||||
|
return failure("team_not_found");
|
||||||
|
}
|
||||||
|
if (!team[0]["active"].as<bool>())
|
||||||
|
{
|
||||||
|
return failure("invalid_reference");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldStatus == "OPEN")
|
||||||
|
{
|
||||||
|
db->execSqlSync(
|
||||||
|
"UPDATE reports SET assigned_operator_id = ?, status = 'ASSIGNED' WHERE id = ?",
|
||||||
|
user.id,
|
||||||
|
reportId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
db->execSqlSync("UPDATE reports SET status = 'ASSIGNED' WHERE id = ?",
|
||||||
|
reportId);
|
||||||
|
}
|
||||||
|
|
||||||
|
db->execSqlSync(
|
||||||
|
"UPDATE assignments SET ended_at = NOW() "
|
||||||
|
"WHERE report_id = ? AND ended_at IS NULL",
|
||||||
|
reportId);
|
||||||
db->execSqlSync(
|
db->execSqlSync(
|
||||||
"INSERT INTO assignments (report_id, team_id, assigned_by_user_id) "
|
"INSERT INTO assignments (report_id, team_id, assigned_by_user_id) "
|
||||||
"VALUES (?, ?, ?)",
|
"VALUES (?, ?, ?)",
|
||||||
reportId,
|
reportId,
|
||||||
body["team_id"].asInt64(),
|
body["team_id"].asInt64(),
|
||||||
user.id);
|
user.id);
|
||||||
db->execSqlSync("UPDATE reports SET status = 'ASSIGNED' WHERE id = ?",
|
|
||||||
reportId);
|
|
||||||
db->execSqlSync(
|
db->execSqlSync(
|
||||||
"INSERT INTO report_status_history "
|
"INSERT INTO report_status_history "
|
||||||
"(report_id, old_status, new_status, changed_by_user_id, note) "
|
"(report_id, old_status, new_status, changed_by_user_id, note) "
|
||||||
@@ -670,6 +1014,7 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
|
|||||||
reportId,
|
reportId,
|
||||||
oldStatus,
|
oldStatus,
|
||||||
user.id);
|
user.id);
|
||||||
|
revokeActiveLinksForReport(reportId);
|
||||||
notifyReportOwner(reportId,
|
notifyReportOwner(reportId,
|
||||||
"team_assigned",
|
"team_assigned",
|
||||||
"Team assigned",
|
"Team assigned",
|
||||||
@@ -713,6 +1058,16 @@ Json::Value ReportService::addFeedback(const auth::UserContext &user,
|
|||||||
return failure("report_not_resolved");
|
return failure("report_not_resolved");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto existingFeedback = db->execSqlSync(
|
||||||
|
"SELECT id FROM feedback WHERE report_id = ?",
|
||||||
|
reportId);
|
||||||
|
if (!existingFeedback.empty())
|
||||||
|
{
|
||||||
|
return failure("feedback_already_exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
db->execSqlSync(
|
db->execSqlSync(
|
||||||
"INSERT INTO feedback (report_id, user_id, rating, comment) "
|
"INSERT INTO feedback (report_id, user_id, rating, comment) "
|
||||||
"VALUES (?, ?, ?, ?)",
|
"VALUES (?, ?, ?, ?)",
|
||||||
@@ -720,6 +1075,11 @@ Json::Value ReportService::addFeedback(const auth::UserContext &user,
|
|||||||
user.id,
|
user.id,
|
||||||
rating,
|
rating,
|
||||||
body.get("comment", "").asString());
|
body.get("comment", "").asString());
|
||||||
|
}
|
||||||
|
catch (const drogon::orm::DrogonDbException &)
|
||||||
|
{
|
||||||
|
return failure("feedback_already_exists");
|
||||||
|
}
|
||||||
return success();
|
return success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,11 +1087,16 @@ Json::Value ReportService::flagReport(const auth::UserContext &user,
|
|||||||
long long reportId,
|
long long reportId,
|
||||||
const Json::Value &body)
|
const Json::Value &body)
|
||||||
{
|
{
|
||||||
if (!canAccessReport(user, reportId) ||
|
if (!validation::hasString(body, "reason"))
|
||||||
!validation::hasString(body, "reason"))
|
|
||||||
{
|
{
|
||||||
return failure("invalid_flag");
|
return failure("invalid_flag");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!canFlagReportContent(user, reportId))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
|
||||||
auto db = drogon::app().getDbClient();
|
auto db = drogon::app().getDbClient();
|
||||||
db->execSqlSync(
|
db->execSqlSync(
|
||||||
"INSERT INTO moderation_flags "
|
"INSERT INTO moderation_flags "
|
||||||
@@ -753,20 +1118,27 @@ Json::Value ReportService::flagMessage(const auth::UserContext &user,
|
|||||||
long long messageId,
|
long long messageId,
|
||||||
const Json::Value &body)
|
const Json::Value &body)
|
||||||
{
|
{
|
||||||
if (!canInteractWithReport(user, reportId) ||
|
if (!validation::hasString(body, "reason"))
|
||||||
!validation::hasString(body, "reason"))
|
|
||||||
{
|
{
|
||||||
return failure("invalid_flag");
|
return failure("invalid_flag");
|
||||||
}
|
}
|
||||||
|
if (!canFlagReportContent(user, reportId))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
auto db = drogon::app().getDbClient();
|
auto db = drogon::app().getDbClient();
|
||||||
const auto message = db->execSqlSync(
|
const auto message = db->execSqlSync(
|
||||||
"SELECT id FROM messages WHERE id = ? AND report_id = ?",
|
"SELECT id, user_id FROM messages WHERE id = ? AND report_id = ?",
|
||||||
messageId,
|
messageId,
|
||||||
reportId);
|
reportId);
|
||||||
if (message.empty())
|
if (message.empty())
|
||||||
{
|
{
|
||||||
return failure("message_not_found");
|
return failure("message_not_found");
|
||||||
}
|
}
|
||||||
|
if (message[0]["user_id"].as<long long>() == user.id)
|
||||||
|
{
|
||||||
|
return failure("cannot_flag_own_message");
|
||||||
|
}
|
||||||
db->execSqlSync(
|
db->execSqlSync(
|
||||||
"INSERT INTO moderation_flags "
|
"INSERT INTO moderation_flags "
|
||||||
"(content_type, report_id, message_id, reason, details, created_by_user_id) "
|
"(content_type, report_id, message_id, reason, details, created_by_user_id) "
|
||||||
@@ -783,6 +1155,92 @@ Json::Value ReportService::flagMessage(const auth::UserContext &user,
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::teamSuggestions(const auth::UserContext &user,
|
||||||
|
long long reportId)
|
||||||
|
{
|
||||||
|
if (!auth::canManageReports(user))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
const auto reportRows = db->execSqlSync(
|
||||||
|
"SELECT id, category_id, area_id FROM reports WHERE id = ?",
|
||||||
|
reportId);
|
||||||
|
if (reportRows.empty())
|
||||||
|
{
|
||||||
|
return failure("report_not_found");
|
||||||
|
}
|
||||||
|
const auto result = db->execSqlSync(
|
||||||
|
"SELECT t.*, tc.name AS category_name, a.name AS area_name, "
|
||||||
|
"CASE "
|
||||||
|
" WHEN t.area_id = r.area_id THEN 'area_and_category' "
|
||||||
|
" WHEN t.area_id IS NULL THEN 'category_only' "
|
||||||
|
" ELSE 'fallback_global' "
|
||||||
|
"END AS match_reason, "
|
||||||
|
"CASE WHEN t.area_id = r.area_id THEN 0 "
|
||||||
|
" WHEN t.area_id IS NULL THEN 1 "
|
||||||
|
" ELSE 2 END AS rank_value "
|
||||||
|
"FROM reports r "
|
||||||
|
"JOIN report_category_team_categories map ON map.report_category_id = r.category_id "
|
||||||
|
"JOIN teams t ON t.team_category_id = map.team_category_id AND t.active = TRUE "
|
||||||
|
"JOIN team_categories tc ON tc.id = t.team_category_id "
|
||||||
|
"LEFT JOIN areas a ON a.id = t.area_id "
|
||||||
|
"WHERE r.id = ? AND (t.area_id = r.area_id OR t.area_id IS NULL) "
|
||||||
|
"ORDER BY rank_value, t.name",
|
||||||
|
reportId);
|
||||||
|
|
||||||
|
auto response = success();
|
||||||
|
response["suggestions"] = Json::arrayValue;
|
||||||
|
for (size_t i = 0; i < result.size(); ++i)
|
||||||
|
{
|
||||||
|
auto item = repositories::rowToJson(result[i]);
|
||||||
|
item["is_recommended"] = (i == 0);
|
||||||
|
response["suggestions"].append(std::move(item));
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::createInterventionLink(const auth::UserContext &user,
|
||||||
|
long long reportId)
|
||||||
|
{
|
||||||
|
if (!canModifyReport(user, reportId))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
const auto assignmentId = activeAssignmentId(reportId);
|
||||||
|
if (!assignmentId)
|
||||||
|
{
|
||||||
|
return failure("invalid_status_transition");
|
||||||
|
}
|
||||||
|
auto token = crypto::randomToken();
|
||||||
|
auto db = drogon::app().getDbClient();
|
||||||
|
revokeActiveLinksForReport(reportId);
|
||||||
|
db->execSqlSync(
|
||||||
|
"INSERT INTO intervention_access_links "
|
||||||
|
"(report_id, assignment_id, token_hash, created_by_user_id, expires_at) "
|
||||||
|
"VALUES (?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL 7 DAY))",
|
||||||
|
reportId,
|
||||||
|
*assignmentId,
|
||||||
|
crypto::hashSecret(token),
|
||||||
|
user.id);
|
||||||
|
const auto expiresRows = db->execSqlSync(
|
||||||
|
"SELECT expires_at FROM intervention_access_links WHERE token_hash = ?",
|
||||||
|
crypto::hashSecret(token));
|
||||||
|
return successWithLink("/api/v1/interventions/" + token,
|
||||||
|
expiresRows[0]["expires_at"].as<std::string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value ReportService::revokeInterventionLink(const auth::UserContext &user,
|
||||||
|
long long reportId)
|
||||||
|
{
|
||||||
|
if (!canModifyReport(user, reportId))
|
||||||
|
{
|
||||||
|
return failure("forbidden");
|
||||||
|
}
|
||||||
|
revokeActiveLinksForReport(reportId);
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value ReportService::openDataReports(const std::optional<std::string> &page,
|
Json::Value ReportService::openDataReports(const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage)
|
const std::optional<std::string> &perPage)
|
||||||
{
|
{
|
||||||
@@ -802,7 +1260,22 @@ Json::Value ReportService::openDataReports(const std::optional<std::string> &pag
|
|||||||
"WHERE r.status IN ('RESOLVED', 'CLOSED') "
|
"WHERE r.status IN ('RESOLVED', 'CLOSED') "
|
||||||
"ORDER BY r.updated_at DESC LIMIT ? OFFSET ?",
|
"ORDER BY r.updated_at DESC LIMIT ? OFFSET ?",
|
||||||
perPageValue,
|
perPageValue,
|
||||||
offset));
|
offset),
|
||||||
|
openDataReportSchema());
|
||||||
|
if (const auto boundary = loadAllowedBoundary("BARI"))
|
||||||
|
{
|
||||||
|
Json::Value filtered(Json::arrayValue);
|
||||||
|
for (const auto &report : response["reports"])
|
||||||
|
{
|
||||||
|
if (geojson::pointInGeometry(report["latitude"].asDouble(),
|
||||||
|
report["longitude"].asDouble(),
|
||||||
|
*boundary))
|
||||||
|
{
|
||||||
|
filtered.append(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response["reports"] = std::move(filtered);
|
||||||
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,14 +19,18 @@ class ReportService
|
|||||||
Json::Value assignedReports(const auth::UserContext &user,
|
Json::Value assignedReports(const auth::UserContext &user,
|
||||||
const std::optional<std::string> &page,
|
const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage);
|
const std::optional<std::string> &perPage);
|
||||||
|
Json::Value listAreas();
|
||||||
Json::Value openReports(const auth::UserContext &user,
|
Json::Value openReports(const auth::UserContext &user,
|
||||||
const std::optional<std::string> &page,
|
const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage);
|
const std::optional<std::string> &perPage);
|
||||||
Json::Value otherOperatorReports(const auth::UserContext &user,
|
Json::Value otherOperatorReports(const auth::UserContext &user,
|
||||||
const std::optional<std::string> &page,
|
const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage);
|
const std::optional<std::string> &perPage);
|
||||||
|
Json::Value listCategories(const auth::UserContext &user);
|
||||||
|
Json::Value listOperatorTeams(const auth::UserContext &user);
|
||||||
Json::Value reportDetail(const auth::UserContext &user, long long reportId);
|
Json::Value reportDetail(const auth::UserContext &user, long long reportId);
|
||||||
Json::Value interventionSummary(const auth::UserContext &user, long long reportId);
|
Json::Value interventionSummary(const auth::UserContext &user, long long reportId);
|
||||||
|
Json::Value publicInterventionSummary(const std::string &token);
|
||||||
Json::Value updateStatus(const auth::UserContext &user,
|
Json::Value updateStatus(const auth::UserContext &user,
|
||||||
long long reportId,
|
long long reportId,
|
||||||
const Json::Value &body);
|
const Json::Value &body);
|
||||||
@@ -48,6 +52,9 @@ class ReportService
|
|||||||
long long reportId,
|
long long reportId,
|
||||||
long long messageId,
|
long long messageId,
|
||||||
const Json::Value &body);
|
const Json::Value &body);
|
||||||
|
Json::Value teamSuggestions(const auth::UserContext &user, long long reportId);
|
||||||
|
Json::Value createInterventionLink(const auth::UserContext &user, long long reportId);
|
||||||
|
Json::Value revokeInterventionLink(const auth::UserContext &user, long long reportId);
|
||||||
Json::Value openDataReports(const std::optional<std::string> &page,
|
Json::Value openDataReports(const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage);
|
const std::optional<std::string> &perPage);
|
||||||
Json::Value canAddMedia(const auth::UserContext &user, long long reportId);
|
Json::Value canAddMedia(const auth::UserContext &user, long long reportId);
|
||||||
|
|||||||
+34
-2
@@ -10,6 +10,9 @@ namespace visione::services
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
using repositories::JsonFieldType;
|
||||||
|
using repositories::JsonRowSchema;
|
||||||
|
|
||||||
Json::Value failure(const std::string &code)
|
Json::Value failure(const std::string &code)
|
||||||
{
|
{
|
||||||
Json::Value body;
|
Json::Value body;
|
||||||
@@ -56,6 +59,23 @@ int boundedPerPage(const std::optional<std::string> &value)
|
|||||||
return 50;
|
return 50;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const JsonRowSchema ¬ificationSchema()
|
||||||
|
{
|
||||||
|
static const JsonRowSchema schema{
|
||||||
|
{"id", JsonFieldType::UInt64},
|
||||||
|
{"report_id", JsonFieldType::UInt64}};
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonRowSchema &rewardEventSchema()
|
||||||
|
{
|
||||||
|
static const JsonRowSchema schema{
|
||||||
|
{"id", JsonFieldType::UInt64},
|
||||||
|
{"report_id", JsonFieldType::UInt64},
|
||||||
|
{"points", JsonFieldType::Int64}};
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Json::Value UserService::listNotifications(
|
Json::Value UserService::listNotifications(
|
||||||
@@ -77,7 +97,8 @@ Json::Value UserService::listNotifications(
|
|||||||
"ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
"ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
||||||
user.id,
|
user.id,
|
||||||
perPageValue,
|
perPageValue,
|
||||||
offset));
|
offset),
|
||||||
|
notificationSchema());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +120,16 @@ Json::Value UserService::markNotificationRead(const auth::UserContext &user,
|
|||||||
return success();
|
return success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Json::Value UserService::markAllNotificationsRead(const auth::UserContext &user)
|
||||||
|
{
|
||||||
|
drogon::app().getDbClient()->execSqlSync(
|
||||||
|
"UPDATE notifications "
|
||||||
|
"SET read_at = COALESCE(read_at, NOW()) "
|
||||||
|
"WHERE user_id = ?",
|
||||||
|
user.id);
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value UserService::rewards(const auth::UserContext &user,
|
Json::Value UserService::rewards(const auth::UserContext &user,
|
||||||
const std::optional<std::string> &page,
|
const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage)
|
const std::optional<std::string> &perPage)
|
||||||
@@ -122,7 +153,8 @@ Json::Value UserService::rewards(const auth::UserContext &user,
|
|||||||
"ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
"ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
||||||
user.id,
|
user.id,
|
||||||
perPageValue,
|
perPageValue,
|
||||||
offset));
|
offset),
|
||||||
|
rewardEventSchema());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class UserService
|
|||||||
const std::optional<std::string> &perPage);
|
const std::optional<std::string> &perPage);
|
||||||
Json::Value markNotificationRead(const auth::UserContext &user,
|
Json::Value markNotificationRead(const auth::UserContext &user,
|
||||||
long long notificationId);
|
long long notificationId);
|
||||||
|
Json::Value markAllNotificationsRead(const auth::UserContext &user);
|
||||||
Json::Value rewards(const auth::UserContext &user,
|
Json::Value rewards(const auth::UserContext &user,
|
||||||
const std::optional<std::string> &page,
|
const std::optional<std::string> &page,
|
||||||
const std::optional<std::string> &perPage);
|
const std::optional<std::string> &perPage);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ target_include_directories(${PROJECT_NAME}_test
|
|||||||
target_link_libraries(${PROJECT_NAME}_test
|
target_link_libraries(${PROJECT_NAME}_test
|
||||||
PRIVATE
|
PRIVATE
|
||||||
Drogon::Drogon
|
Drogon::Drogon
|
||||||
|
CURL::libcurl
|
||||||
|
OpenSSL::Crypto
|
||||||
)
|
)
|
||||||
|
|
||||||
ParseAndAddDrogonTests(${PROJECT_NAME}_test)
|
ParseAndAddDrogonTests(${PROJECT_NAME}_test)
|
||||||
|
|||||||
+90
-8
@@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
#include "common/GeoJson.h"
|
#include "common/GeoJson.h"
|
||||||
#include "common/Crypto.h"
|
#include "common/Crypto.h"
|
||||||
|
#include "common/Http.h"
|
||||||
#include "common/Validation.h"
|
#include "common/Validation.h"
|
||||||
|
#include "repositories/Sql.h"
|
||||||
|
|
||||||
#include <future>
|
#include <future>
|
||||||
|
|
||||||
@@ -53,7 +55,44 @@ DROGON_TEST(ValidationRejectsMalformedIdentity)
|
|||||||
CHECK(visione::validation::isValidEmail("[email protected]"));
|
CHECK(visione::validation::isValidEmail("[email protected]"));
|
||||||
CHECK(!visione::validation::isValidEmail("not-an-email"));
|
CHECK(!visione::validation::isValidEmail("not-an-email"));
|
||||||
CHECK(visione::validation::isValidFiscalCode("RSSMRA80A01H501U"));
|
CHECK(visione::validation::isValidFiscalCode("RSSMRA80A01H501U"));
|
||||||
|
CHECK(visione::validation::isValidFiscalCode("LCCVTI62M02A662Q"));
|
||||||
CHECK(!visione::validation::isValidFiscalCode("short"));
|
CHECK(!visione::validation::isValidFiscalCode("short"));
|
||||||
|
CHECK(!visione::validation::isValidFiscalCode("VSSGNR94M08A662A"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DROGON_TEST(SqlScalarSerializationKeepsTypes)
|
||||||
|
{
|
||||||
|
const auto json = []() {
|
||||||
|
Json::Value value(Json::objectValue);
|
||||||
|
value["id"] = Json::UInt64(7);
|
||||||
|
value["banned"] = false;
|
||||||
|
value["points"] = Json::Int64(42);
|
||||||
|
value["phone"] = "380 6180216";
|
||||||
|
return value;
|
||||||
|
}();
|
||||||
|
|
||||||
|
CHECK(json["id"].isUInt64());
|
||||||
|
CHECK(json["banned"].isBool());
|
||||||
|
CHECK(json["points"].isInt64());
|
||||||
|
CHECK(json["phone"].isString());
|
||||||
|
}
|
||||||
|
|
||||||
|
DROGON_TEST(CryptoHmacIsDeterministic)
|
||||||
|
{
|
||||||
|
const auto digest = visione::crypto::hmacSha256Hex("secret", "{\"a\":1}");
|
||||||
|
CHECK(digest == "AA9E2E3575F5D7098B6CACCD790888C36D5FDB63342A73BADA2D6A51747A8494");
|
||||||
|
CHECK(visione::crypto::constantTimeEquals(digest, digest));
|
||||||
|
CHECK(!visione::crypto::constantTimeEquals(digest, "different"));
|
||||||
|
}
|
||||||
|
|
||||||
|
DROGON_TEST(ParseJsonBodyRejectsInvalidPayload)
|
||||||
|
{
|
||||||
|
const auto parsed = visione::http::parseJsonBody("{\"ok\":true}");
|
||||||
|
CHECK(parsed.isObject());
|
||||||
|
CHECK(parsed["ok"].asBool());
|
||||||
|
|
||||||
|
const auto invalid = visione::http::parseJsonBody("{oops");
|
||||||
|
CHECK(invalid.isNull());
|
||||||
}
|
}
|
||||||
|
|
||||||
DROGON_TEST(GeoJsonMatchesPointInsidePolygon)
|
DROGON_TEST(GeoJsonMatchesPointInsidePolygon)
|
||||||
@@ -84,18 +123,61 @@ DROGON_TEST(GeoJsonMatchesPointInsidePolygon)
|
|||||||
|
|
||||||
CHECK(visione::geojson::pointInPolygon(41.5, 12.5, polygon));
|
CHECK(visione::geojson::pointInPolygon(41.5, 12.5, polygon));
|
||||||
CHECK(!visione::geojson::pointInPolygon(43.0, 12.5, polygon));
|
CHECK(!visione::geojson::pointInPolygon(43.0, 12.5, polygon));
|
||||||
|
CHECK(visione::geojson::pointInGeometry(41.5, 12.5, polygon));
|
||||||
|
CHECK(!visione::geojson::pointInGeometry(43.0, 12.5, polygon));
|
||||||
}
|
}
|
||||||
|
|
||||||
DROGON_TEST(PasswordHashSupportsPbkdf2Only)
|
DROGON_TEST(GeoJsonAcceptsContainedPolygon)
|
||||||
{
|
{
|
||||||
const auto hash = visione::crypto::hashPassword("CorrectHorse123!");
|
Json::Value container;
|
||||||
CHECK(hash.rfind("pbkdf2_sha256$", 0) == 0);
|
container["type"] = "Polygon";
|
||||||
CHECK(visione::crypto::verifyPassword("CorrectHorse123!", hash));
|
Json::Value outer(Json::arrayValue);
|
||||||
CHECK(!visione::crypto::verifyPassword("wrong", hash));
|
|
||||||
|
|
||||||
const std::string legacy =
|
Json::Value a(Json::arrayValue);
|
||||||
"sha256$local-dev-salt$CD4FF6AC8C9685D08E55AF693970F15F9BFCD4C0DC9F4D3001D202C9DB00291F";
|
a.append(12.0);
|
||||||
CHECK(!visione::crypto::verifyPassword("ChangeMe123!", legacy));
|
a.append(41.0);
|
||||||
|
Json::Value b(Json::arrayValue);
|
||||||
|
b.append(13.0);
|
||||||
|
b.append(41.0);
|
||||||
|
Json::Value c(Json::arrayValue);
|
||||||
|
c.append(13.0);
|
||||||
|
c.append(42.0);
|
||||||
|
Json::Value d(Json::arrayValue);
|
||||||
|
d.append(12.0);
|
||||||
|
d.append(42.0);
|
||||||
|
outer.append(a);
|
||||||
|
outer.append(b);
|
||||||
|
outer.append(c);
|
||||||
|
outer.append(d);
|
||||||
|
outer.append(a);
|
||||||
|
container["coordinates"].append(outer);
|
||||||
|
|
||||||
|
Json::Value candidate;
|
||||||
|
candidate["type"] = "Polygon";
|
||||||
|
Json::Value inner(Json::arrayValue);
|
||||||
|
Json::Value p1(Json::arrayValue);
|
||||||
|
p1.append(12.2);
|
||||||
|
p1.append(41.2);
|
||||||
|
Json::Value p2(Json::arrayValue);
|
||||||
|
p2.append(12.8);
|
||||||
|
p2.append(41.2);
|
||||||
|
Json::Value p3(Json::arrayValue);
|
||||||
|
p3.append(12.8);
|
||||||
|
p3.append(41.8);
|
||||||
|
Json::Value p4(Json::arrayValue);
|
||||||
|
p4.append(12.2);
|
||||||
|
p4.append(41.8);
|
||||||
|
inner.append(p1);
|
||||||
|
inner.append(p2);
|
||||||
|
inner.append(p3);
|
||||||
|
inner.append(p4);
|
||||||
|
inner.append(p1);
|
||||||
|
candidate["coordinates"].append(inner);
|
||||||
|
|
||||||
|
CHECK(visione::geojson::polygonInsideGeometry(candidate, container));
|
||||||
|
|
||||||
|
candidate["coordinates"][0][3][0] = 14.0;
|
||||||
|
CHECK(!visione::geojson::polygonInsideGeometry(candidate, container));
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
|
|||||||
Reference in New Issue
Block a user