remove user-secrets approach & prepare a pilot deploy
This commit is contained in:
+13
-6
@@ -10,14 +10,21 @@ git config core.hooksPath .githooks
|
||||
|
||||
## `pre-commit` — secret scan
|
||||
|
||||
A fast, dependency-free backstop for the root `CLAUDE.md` rule **"Never commit secrets"**
|
||||
A fast, dependency-free backstop against a credential leaking into a file that shouldn't hold one
|
||||
(refinement-phase-5). It rejects a commit that stages:
|
||||
|
||||
- the historically-leaked SQL Server host `87.107.152.16`,
|
||||
- the retired hardcoded admin password `qw123321`,
|
||||
- a **real** connection-string password in any `appsettings*.json` (only the `SET_VIA_USER_SECRETS_OR_ENV`
|
||||
placeholder is allowed — real values belong in user-secrets / environment variables),
|
||||
- private-key material or an AWS access-key id, anywhere.
|
||||
- the retired hardcoded admin password `qw123321`, anywhere,
|
||||
- private-key material or an AWS access-key id, anywhere,
|
||||
- the deployment's SQL Server host `87.107.152.16` **outside the declared config files**,
|
||||
- a **real** connection-string password in any `appsettings*.json` **outside the declared config files**
|
||||
(elsewhere only the `SET_VIA_USER_SECRETS_OR_ENV` placeholder is allowed).
|
||||
|
||||
**Declared config files.** The pre-launch demo deployment configures itself from committed files rather
|
||||
than a secret store ([DEPLOY.md](../DEPLOY.md)), so a short allow-list — `appsettings.Development.json`,
|
||||
`docker-compose.yml`, `telegram-otp-bot/.env.example`, `DEPLOY.md` — is exempt from the last two checks.
|
||||
The list is maintained in the `declared_config` function in the hook and is the honest record of where the
|
||||
repo's secrets are. **Shrink it, never grow it**: once real users exist, those values must be rotated and
|
||||
moved out of git.
|
||||
|
||||
It scans only staged additions, so it is quick. It is **not** a replacement for a full scanner
|
||||
(gitleaks / trufflehog) in CI — it is the local first line of defence.
|
||||
|
||||
+28
-8
@@ -12,6 +12,23 @@ set -euo pipefail
|
||||
# Committed placeholders are allowed — real values are not. Keep in sync with StartupSecretsGuard.
|
||||
PLACEHOLDER='SET_VIA_USER_SECRETS_OR_ENV'
|
||||
|
||||
# Files that deliberately carry live deployment credentials, because the pre-launch demo deployment
|
||||
# configures itself from committed files rather than a secret store (see DEPLOY.md). They are exempt from
|
||||
# the connection-string and known-host checks ONLY — the private-key and AWS-key checks still apply to
|
||||
# them, and every other file in the repo is scanned exactly as strictly as before.
|
||||
#
|
||||
# This list is the honest record of where the repo's secrets are. Shrink it, never grow it: the moment
|
||||
# real users exist, these values must be rotated and moved out of git.
|
||||
declared_config() {
|
||||
case "$1" in
|
||||
server/src/API/Baya.Web.Api/appsettings.Development.json) return 0 ;;
|
||||
docker-compose.yml) return 0 ;;
|
||||
telegram-otp-bot/.env.example) return 0 ;;
|
||||
DEPLOY.md) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Only scan added/changed lines in text files that are staged.
|
||||
staged=$(git diff --cached --name-only --diff-filter=ACM)
|
||||
[ -z "$staged" ] && exit 0
|
||||
@@ -30,21 +47,23 @@ while IFS= read -r file; do
|
||||
added=$(git diff --cached -U0 -- "$file" | grep '^+' | grep -v '^+++' || true)
|
||||
[ -z "$added" ] && continue
|
||||
|
||||
# The historically-leaked SQL Server host — must never reappear.
|
||||
echo "$added" | grep -Eq '87\.107\.152\.16' && report "$file: leaked SQL Server host 87.107.152.16"
|
||||
|
||||
# The retired hardcoded admin password.
|
||||
# The retired hardcoded admin password. Applies everywhere, no exemptions.
|
||||
echo "$added" | grep -Eq 'qw123321' && report "$file: hardcoded admin password 'qw123321'"
|
||||
|
||||
if ! declared_config "$file"; then
|
||||
# The deployment's SQL Server host — outside the declared config files it is a leak.
|
||||
echo "$added" | grep -Eq '87\.107\.152\.16' && report "$file: SQL Server host 87.107.152.16 outside the declared config files"
|
||||
|
||||
# A real (non-placeholder) connection-string password in a committed appsettings file.
|
||||
case "$file" in
|
||||
*appsettings*.json)
|
||||
echo "$added" \
|
||||
| grep -Ei 'Password=[^;"'"'"' ]+' \
|
||||
| grep -viq "Password=${PLACEHOLDER}" \
|
||||
&& report "$file: connection-string password must be '${PLACEHOLDER}' (real value belongs in user-secrets/env)"
|
||||
&& report "$file: connection-string password must be '${PLACEHOLDER}' (see DEPLOY.md for where real values live)"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Private keys and common cloud tokens, anywhere.
|
||||
echo "$added" | grep -Eq -- '-----BEGIN (RSA|EC|OPENSSH|PRIVATE) .*PRIVATE KEY-----' && report "$file: private key material"
|
||||
@@ -53,9 +72,10 @@ done <<< "$staged"
|
||||
|
||||
if [ "$violations" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Commit blocked: $violations potential secret(s) staged. Move the real value to user-secrets"
|
||||
echo "(Development) or an environment variable (deploy) and commit only the '${PLACEHOLDER}' placeholder."
|
||||
echo "See dev/post-phase/refinement/RUNBOOK.md. To override a false positive: git commit --no-verify"
|
||||
echo "Commit blocked: $violations potential secret(s) staged. Real values belong in one of the declared"
|
||||
echo "config files (see the 'declared_config' list in this hook, and DEPLOY.md); everything else commits"
|
||||
echo "only the '${PLACEHOLDER}' placeholder."
|
||||
echo "To override a false positive: git commit --no-verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -55,10 +55,17 @@ solution — each project is built, linted, and run on its own.
|
||||
| [`server/`](server/) | Backend API | ASP.NET Core (.NET 10) · Clean Architecture · CQRS · EF Core | [server/CLAUDE.md](server/CLAUDE.md) |
|
||||
| [`product/`](product/) | Product docs | Markdown | — (see table above) |
|
||||
| [`dev/`](dev/) | Build plan (not app code) | Markdown | [dev/README.md](dev/README.md) |
|
||||
| [`telegram-otp-bot/`](telegram-otp-bot/) | OTP relay (standalone) | Node 18+, zero deps | [telegram-otp-bot/README.md](telegram-otp-bot/README.md) |
|
||||
| [`deploy/`](deploy/) | Reverse-proxy config | Caddyfile | [DEPLOY.md](DEPLOY.md) |
|
||||
|
||||
The two communicate over **HTTP/JSON** (optionally gRPC). The client reads the API base URL from
|
||||
`NEXT_PUBLIC_API_URL`; the server listens on `https://localhost:5002` by default.
|
||||
|
||||
**Deployment** is three Docker containers — one `Dockerfile` per project directory, orchestrated by the
|
||||
root [`docker-compose.yml`](docker-compose.yml) — behind an existing Caddy reverse proxy on the external
|
||||
`caddy_net` network, serving `balinyaar.ir` (client) and `api.balinyaar.ir` (server). The database is
|
||||
**not** containerised; it is a remote SQL Server. Full runbook: [DEPLOY.md](DEPLOY.md).
|
||||
|
||||
[`dev/`](dev/README.md) holds the **phased build plan** that takes the repo from its current baseline to
|
||||
the MVP: a chain of agent-runnable prompt files split into a `backend/` and a `frontend/` track
|
||||
([dev/phases/](dev/phases/README.md)), the cross-project API [`contracts/`](dev/contracts/README.md), and
|
||||
@@ -80,8 +87,14 @@ project — there is nothing to build in it.
|
||||
5. **Don't reintroduce template/starter scaffolding.** Both projects were derived from open-source
|
||||
starters; their branding, demo/showcase pages, and `_TITLE_`/`_DESCRIPTION_` placeholders were
|
||||
intentionally removed. Don't add them back.
|
||||
6. **Never commit secrets.** Use `.env` (client) and `appsettings.*.json` / user-secrets (server).
|
||||
Real connection strings, keys, and tokens never enter git.
|
||||
6. **Configuration lives in files, not in a secret store.** `dotnet user-secrets` is **not** used — the
|
||||
`<UserSecretsId>` was removed from `Baya.Web.Api.csproj`, so that store isn't even read. Server config
|
||||
(including keys) lives in `appsettings.*.json`; client config in `.env.development` / `.env.production`;
|
||||
the deployment's container-specific overrides in `docker-compose.yml`. This is a deliberate pre-launch
|
||||
trade for a demo deployment — **the repo therefore contains live credentials**. Before onboarding real
|
||||
users, rotate them and move the secret half out of git (see [DEPLOY.md](DEPLOY.md) "Going to Production").
|
||||
One value is load-bearing and must never change: `Seams:FieldEncryption:Key`/`:HashKey` decrypt all
|
||||
existing PII and derive the phone-lookup hash.
|
||||
7. **Keep docs honest, and keep the architecture map current.** If you change how something works,
|
||||
update the `CLAUDE.md` that describes it in the same change. Each level documents its architecture
|
||||
in one canonical place — **this file's "Repository layout"** (repo), **client/CLAUDE.md "Project
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# Deploying Balinyaar
|
||||
|
||||
A first, shareable deployment of the whole stack under **balinyaar.ir**, in Docker, behind an existing
|
||||
Caddy reverse proxy that terminates TLS.
|
||||
|
||||
| Host | Serves | Container |
|
||||
| --- | --- | --- |
|
||||
| `balinyaar.ir`, `www.balinyaar.ir` | Next.js web client | `balinyaar-web:3000` |
|
||||
| `api.balinyaar.ir` | ASP.NET Core API | `balinyaar-api:8080` |
|
||||
| *(internal only)* | Telegram OTP relay | `balinyaar-otp-relay:5010` |
|
||||
|
||||
The **database is not containerised** — it is the remote SQL Server already configured in
|
||||
[server/src/API/Baya.Web.Api/appsettings.Development.json](server/src/API/Baya.Web.Api/appsettings.Development.json).
|
||||
Nothing needs to be provisioned for it; the API just needs network reach to `87.107.152.16:1433`.
|
||||
|
||||
---
|
||||
|
||||
## Configuration model
|
||||
|
||||
**There is no `dotnet user-secrets` any more.** The `<UserSecretsId>` was removed from
|
||||
`Baya.Web.Api.csproj`, so the API no longer reads that store at all — a stale `secrets.json` on a dev
|
||||
machine is now inert and can be deleted. Every value lives in a file in the repo:
|
||||
|
||||
| What | Where |
|
||||
| --- | --- |
|
||||
| API config + secrets (DB, JWE keys, field-encryption keys, Telegram key, CORS, trusted proxies) | `server/src/API/Baya.Web.Api/appsettings.Development.json` |
|
||||
| The two values that differ between a laptop and the container network | `docker-compose.yml` → `api.environment` |
|
||||
| Client build-time config (API URL, site origin) | `client/.env.production` |
|
||||
| Telegram relay config (bot token, chat ids, API key, proxy) | `docker-compose.yml` → `otp-relay.environment` |
|
||||
|
||||
The API runs as **`ASPNETCORE_ENVIRONMENT=Development`**, so `appsettings.Development.json` is the file
|
||||
that actually loads. An `appsettings.Production.json` would be ignored — put changes in the Development
|
||||
file, or change the environment name first.
|
||||
|
||||
The relay's shared secret appears twice and the two must match: `Seams:Sms:Telegram:ApiKey` in the
|
||||
appsettings file and `API_KEY` in the compose file. It was rotated away from the value in
|
||||
`telegram-otp-bot/.env.example`, which is published in git and in that project's README —
|
||||
`TelegramSmsSender` now refuses to authenticate with it. **If you run the relay locally**, copy the
|
||||
appsettings value into your own `telegram-otp-bot/.env`.
|
||||
|
||||
> ⚠️ **`Seams:FieldEncryption:Key` and `:HashKey` must never change.** Every encrypted column in that
|
||||
> database — phone numbers, addresses, IBANs, clinical notes — was written with those exact values, and
|
||||
> `users.PhoneHash`, which every login looks up, is derived from `HashKey`. Rotating either makes the
|
||||
> existing data unreadable and locks every account out. The JWE keys (`IdentitySettings:SecretKey` /
|
||||
> `Encryptkey`) are safe to rotate; doing so only signs everyone out.
|
||||
|
||||
---
|
||||
|
||||
## What running as Development means
|
||||
|
||||
This was a deliberate choice so the demo and lifecycle seeders populate the shared database and the
|
||||
screens aren't empty. It has real consequences, all of which are fine for a pre-launch demo among
|
||||
people you trust, and none of which are acceptable once strangers can reach the site:
|
||||
|
||||
- **The developer exception page is public.** Any unhandled 500 on `api.balinyaar.ir` returns a stack
|
||||
trace and configuration detail to the caller.
|
||||
- **`GET /api/v1/dev/last_otp/{phone}` is live.** Anyone who knows a registered phone number can read
|
||||
its login code and sign in as that user. This is the single biggest exposure.
|
||||
- **Swagger is served** at `api.balinyaar.ir/swagger`.
|
||||
- **The seeders re-run on every container boot** (idempotent, so this is safe — they no-op on data that
|
||||
already exists) and **migrations auto-apply on boot** rather than as a separate step.
|
||||
- **gRPC reflection is enabled**, and the demo `bookings/convert` payment-capture simulator is wired.
|
||||
|
||||
### Going to Production later
|
||||
|
||||
1. Set `ASPNETCORE_ENVIRONMENT: Production` in `docker-compose.yml`.
|
||||
2. Create `appsettings.Production.json` with the same content as the Development file, but with **real**
|
||||
`IdentitySettings:SecretKey` / `Encryptkey` — `StartupSecretsGuard` rejects anything containing
|
||||
`not-for-production` outside Development, so the current dev keys will refuse to boot (by design).
|
||||
Keep `Seams:FieldEncryption` byte-identical.
|
||||
3. Run migrations as a one-shot instead of on boot:
|
||||
`docker compose run --rm api dotnet Baya.Web.Api.dll migrate`
|
||||
4. Swap the OTP rail: `Seams:Sms:Provider` → `kavenegar`, with `Seams:Sms:ApiKey`/`Sender` filled in.
|
||||
The Telegram relay broadcasts every code to a fixed recipient list, which stops being acceptable the
|
||||
moment someone outside that list can request one.
|
||||
|
||||
---
|
||||
|
||||
## First deploy
|
||||
|
||||
### 1. Confirm the Caddy network exists
|
||||
|
||||
The compose file joins `caddy_net` as an **external** network — it does not create it.
|
||||
|
||||
```bash
|
||||
docker network ls | grep caddy_net
|
||||
```
|
||||
|
||||
### 2. Add the Balinyaar block to your Caddyfile
|
||||
|
||||
Copy from [deploy/Caddyfile](deploy/Caddyfile) into the Caddyfile your Caddy container already loads:
|
||||
|
||||
```caddyfile
|
||||
balinyaar.ir, www.balinyaar.ir {
|
||||
encode zstd gzip
|
||||
reverse_proxy balinyaar-web:3000
|
||||
}
|
||||
|
||||
api.balinyaar.ir {
|
||||
encode zstd gzip
|
||||
reverse_proxy balinyaar-api:8080
|
||||
}
|
||||
```
|
||||
|
||||
Caddy obtains and renews the certificates for both hostnames itself. Reload it:
|
||||
|
||||
```bash
|
||||
docker exec <caddy-container> caddy reload --config /etc/caddy/Caddyfile
|
||||
```
|
||||
|
||||
### 3. Point DNS at the host
|
||||
|
||||
`balinyaar.ir`, `www.balinyaar.ir` and `api.balinyaar.ir` all need an A record on the server's public IP
|
||||
**before** Caddy can complete the ACME challenge.
|
||||
|
||||
### 4. Confirm the proxy container is up
|
||||
|
||||
The relay's hop to `api.telegram.org` is filtered in Iran and goes out through the proxy already on
|
||||
`caddy_net`, configured as `TELEGRAM_PROXY_URL: http://hysteria-client:8081`. If that container has a
|
||||
different name or port, change it in `docker-compose.yml` — a wrong value fails the relay at boot with a
|
||||
clear message rather than silently per-OTP.
|
||||
|
||||
### 5. Build and start
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
docker compose ps
|
||||
docker compose logs -f api
|
||||
```
|
||||
|
||||
The API's first boot applies any pending migrations and runs the seeders against the remote database, so
|
||||
it takes noticeably longer than later ones.
|
||||
|
||||
---
|
||||
|
||||
## Verifying
|
||||
|
||||
```bash
|
||||
curl https://api.balinyaar.ir/healthz/live # process is up
|
||||
curl https://api.balinyaar.ir/healthz/ready # + database and object storage reachable
|
||||
curl -I https://balinyaar.ir # the public landing page
|
||||
docker compose logs otp-relay | head # should print the bot's @username and the proxy label
|
||||
```
|
||||
|
||||
A full login round-trip is the real check: request an OTP from the site and confirm the code arrives in
|
||||
the Telegram chat. If it doesn't, `docker compose logs otp-relay` names the failing hop — a proxy error
|
||||
and a Telegram API rejection look different.
|
||||
|
||||
---
|
||||
|
||||
## Redeploying
|
||||
|
||||
```bash
|
||||
git pull
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Rebuild the client whenever a `NEXT_PUBLIC_*` value in `client/.env.production` changes — those are
|
||||
compiled into the browser bundle, so restarting the container alone changes nothing.
|
||||
|
||||
## Persisted state
|
||||
|
||||
Two named volumes survive rebuilds. Uploaded verification documents live in the first one; losing it
|
||||
means the admin verification queue shows broken documents.
|
||||
|
||||
| Volume | Holds |
|
||||
| --- | --- |
|
||||
| `api-object-storage` | Uploaded verification documents (local-disk `IObjectStorage` seam) |
|
||||
| `api-logs` | Serilog JSON file sink |
|
||||
@@ -0,0 +1,18 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
coverage
|
||||
.swc
|
||||
graphify-out
|
||||
*.tsbuildinfo
|
||||
|
||||
# Local-only env files — .env.production IS copied, it is the deployed build's input.
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
README.md
|
||||
@@ -0,0 +1,24 @@
|
||||
# Deployed (balinyaar.ir) values, read by `next build` when NODE_ENV=production.
|
||||
#
|
||||
# Every NEXT_PUBLIC_* value here is INLINED INTO THE CLIENT BUNDLE AT BUILD TIME — it is public by
|
||||
# definition, and changing one requires rebuilding the image, not restarting the container.
|
||||
# `.env.development` still owns the local `npm run dev` loop and is untouched by this file.
|
||||
|
||||
# Enables analytics and public resources.
|
||||
NEXT_PUBLIC_ENV = production
|
||||
|
||||
# Off in a deployed build — `true` prints the resolved @/config (incl. the API URL) to the browser console.
|
||||
NEXT_PUBLIC_DEBUG = false
|
||||
|
||||
# Public origin of the web app.
|
||||
NEXT_PUBLIC_PUBLIC_URL = https://balinyaar.ir
|
||||
|
||||
# Absolute origin used only for metadata (OG tags, metadataBase, robots.ts, sitemap.ts) — never for API calls.
|
||||
NEXT_PUBLIC_SITE_URL = https://balinyaar.ir
|
||||
|
||||
# The API, reached from the BROWSER — so it is the public hostname Caddy serves, never the container name.
|
||||
NEXT_PUBLIC_API_URL = https://api.balinyaar.ir
|
||||
|
||||
# Neshan **web** key (client-embeddable maps/search) from https://platform.neshan.org. Unset: the address
|
||||
# map-pin picker falls back to its bounded-canvas grid stand-in. Rebuild the client image after setting it.
|
||||
# NEXT_PUBLIC_NESHAN_KEY = your-neshan-web-key
|
||||
@@ -0,0 +1,36 @@
|
||||
# Balinyaar web client — build context is `client/` (see the root docker-compose.yml).
|
||||
#
|
||||
# NEXT_PUBLIC_* values are inlined into the browser bundle by `next build`, so the API URL and site origin
|
||||
# are BUILD-time inputs, not runtime env vars — setting them in compose would do nothing. They come from the
|
||||
# committed .env.production, which `next build` reads because it runs with NODE_ENV=production; change a value
|
||||
# there and rebuild the image.
|
||||
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine AS final
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
# `output: 'standalone'` traces the runtime dependencies into .next/standalone; static assets and public/
|
||||
# are deliberately NOT included in that trace and must be copied alongside it, or every asset 404s.
|
||||
COPY --from=build --chown=node:node /app/.next/standalone ./
|
||||
COPY --from=build --chown=node:node /app/.next/static ./.next/static
|
||||
COPY --from=build --chown=node:node /app/public ./public
|
||||
|
||||
USER node
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
@@ -7,7 +7,10 @@ const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
turbopack: {
|
||||
root: '.'
|
||||
}
|
||||
},
|
||||
// Emits .next/standalone — a self-contained server bundling only the traced runtime dependencies, so
|
||||
// the Docker image carries no node_modules tree. Harmless for `npm run dev`/`npm run build` locally.
|
||||
output: 'standalone'
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Balinyaar — the block to add to your EXISTING Caddyfile (the Caddy container that owns caddy_net).
|
||||
#
|
||||
# This is not loaded by anything in this repo; it is a copy of what DEPLOY.md tells you to paste, kept
|
||||
# here so the reverse-proxy contract lives next to the compose file that depends on it.
|
||||
#
|
||||
# Both upstreams are plain HTTP on the container network — Caddy is the only TLS terminator, and it
|
||||
# obtains/renews the certificates for both hostnames automatically.
|
||||
|
||||
balinyaar.ir, www.balinyaar.ir {
|
||||
encode zstd gzip
|
||||
reverse_proxy balinyaar-web:3000
|
||||
}
|
||||
|
||||
api.balinyaar.ir {
|
||||
encode zstd gzip
|
||||
|
||||
# The API partitions its rate limiter on the client IP resolved from X-Forwarded-For, and trusts the
|
||||
# docker bridge ranges listed under ForwardedHeaders:KnownNetworks. Caddy sets X-Forwarded-For and
|
||||
# X-Forwarded-Proto by default, so no extra header directives are needed here.
|
||||
reverse_proxy balinyaar-api:8080
|
||||
}
|
||||
@@ -47,24 +47,29 @@ Give it ~20–30s on first start (`docker compose ps` shows `healthy`).
|
||||
|
||||
> Already have a SQL Server? Skip this and point the connection string in step 3 at it instead.
|
||||
|
||||
### 3. Point the API at the local database (via user-secrets — never a committed file)
|
||||
### 3. Point the API at a database
|
||||
|
||||
The committed `appsettings*.json` carry a **placeholder** connection string on purpose. Supply the real
|
||||
local one through `dotnet user-secrets` so no working credential ever lands in git. From the API project:
|
||||
**`dotnet user-secrets` is no longer used** — the `<UserSecretsId>` was removed from
|
||||
`Baya.Web.Api.csproj`, so that store isn't read at all. A leftover `secrets.json` on your machine is inert
|
||||
and can be deleted. All configuration lives in
|
||||
[`appsettings.Development.json`](../../../server/src/API/Baya.Web.Api/appsettings.Development.json), which
|
||||
already points at the **shared remote database** the deployed demo also uses — so a fresh clone boots with
|
||||
no configuration step at all.
|
||||
|
||||
```bash
|
||||
cd server/src/API/Baya.Web.Api
|
||||
dotnet user-secrets set "ConnectionStrings:SqlServer" "Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"
|
||||
To work against the throwaway local container from step 2 instead, edit `ConnectionStrings:SqlServer` in
|
||||
that file (the `Password` must match `MSSQL_SA_PASSWORD` in `server/docker-compose.yml`):
|
||||
|
||||
```jsonc
|
||||
"SqlServer": "Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"
|
||||
```
|
||||
|
||||
The `Password` must match `MSSQL_SA_PASSWORD` in `docker-compose.yml`. User-secrets auto-load only in the
|
||||
Development environment, so this never affects a deployed build.
|
||||
|
||||
> **Env-var alternative** (e.g. for CI/containers): set `ConnectionStrings__SqlServer` (double underscore =
|
||||
> the `:` config separator) instead of using user-secrets.
|
||||
> **Env-var alternative** (CI/containers, or to avoid a local edit showing up in `git status`): set
|
||||
> `ConnectionStrings__SqlServer` (double underscore = the `:` config separator) — it overrides the file.
|
||||
> PowerShell: `$env:ConnectionStrings__SqlServer = "Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"`
|
||||
> bash: `export ConnectionStrings__SqlServer="Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"`
|
||||
|
||||
> Deploying rather than developing? See [DEPLOY.md](../../../DEPLOY.md).
|
||||
|
||||
---
|
||||
|
||||
## Run it (two terminals)
|
||||
@@ -126,12 +131,10 @@ endpoint works). Use them to see the real path populated:
|
||||
| `09120000030` | partner-center owner | بهنام رستگار (male) | owns مرکز پرستاری آرامش (merchant-of-record, sponsors علی کریمی). No admin/nurse role — log in, then navigate to `/partner` manually (REQ-038: no `/me` partner signal yet) |
|
||||
|
||||
> The old username+password `admin`/`qw123321` account is **no longer auto-seeded** (refinement-phase-5 —
|
||||
> no committed credential). To bootstrap a break-glass username+password admin, set both secrets before boot,
|
||||
> then log in via the API (not the web UI, which is phone-OTP only):
|
||||
> ```bash
|
||||
> cd server/src/API/Baya.Web.Api
|
||||
> dotnet user-secrets set "Seed:AdminUsername" "admin"
|
||||
> dotnet user-secrets set "Seed:AdminPassword" "<a-strong-password>"
|
||||
> no committed credential). To bootstrap a break-glass username+password admin, add both keys to
|
||||
> `appsettings.Development.json` before boot, then log in via the API (not the web UI, which is phone-OTP only):
|
||||
> ```jsonc
|
||||
> "Seed": { "AdminUsername": "admin", "AdminPassword": "<a-strong-password>" }
|
||||
> ```
|
||||
|
||||
The **phone-OTP admins** (`09120000020` / `09120000021`, refinement-phase-2) are how you reach the `/admin`
|
||||
@@ -179,11 +182,11 @@ code to every configured chat id, so it is a test-group convenience, not an SMS
|
||||
`api.telegram.org` is filtered in Iran, so set `TELEGRAM_PROXY_URL` in its `.env` to your VPN/proxy
|
||||
client (`http://127.0.0.1:10809`, `socks5://…`, or the proxy container on a VPS). The boot banner prints
|
||||
the bot's `@username` — that line appearing means the token *and* the proxy work.
|
||||
2. **Share the secret with the API** — the same value on both sides (relay `.env` `API_KEY`, API user-secret):
|
||||
```bash
|
||||
cd server/src/API/Baya.Web.Api
|
||||
dotnet user-secrets set "Seams:Sms:Telegram:ApiKey" "<the relay's API_KEY>"
|
||||
```
|
||||
2. **Share the secret with the API** — the same value on both sides: the relay's `.env` `API_KEY` and
|
||||
`Seams:Sms:Telegram:ApiKey` in `appsettings.Development.json`. The appsettings side is already filled
|
||||
in; copy that value into your local `telegram-otp-bot/.env`. Do **not** use the one in `.env.example` —
|
||||
it is published in git, so `TelegramSmsSender` rejects it with
|
||||
`Seams:Sms:Telegram:ApiKey is not configured (unset, or still the published example key)`.
|
||||
3. **Flip the provider** in `server/src/API/Baya.Web.Api/appsettings.Development.json`:
|
||||
```jsonc
|
||||
"Seams": { "Sms": { "Provider": "telegram" } } // committed default is "mock"
|
||||
@@ -202,10 +205,13 @@ error) rather than pretending an undelivered code was sent.
|
||||
- **The API speaks HTTP/1.1 and HTTP/2** (Kestrel `Protocols: Http1AndHttp2`, refinement-phase-5 — the
|
||||
previous HTTP/2-only default broke non-TLS HTTP/1.1 hops). Over TLS the client negotiates h2 via ALPN, so
|
||||
gRPC and `fetch` both work; plain-HTTP hops fall back to HTTP/1.1.
|
||||
- **Secrets fail fast.** On a fresh clone with no user-secrets the API refuses to start with
|
||||
`Refusing to start: required secret configuration is missing…` — set the connection-string user-secret
|
||||
(step 3) and boot again. Deployed environments must additionally supply real `IdentitySettings` JWE keys
|
||||
and `Seams:FieldEncryption` keys (Development uses dev-only defaults from `appsettings.Development.json`).
|
||||
- **Secrets fail fast.** If `ConnectionStrings` is blank or still the base file's
|
||||
`SET_VIA_USER_SECRETS_OR_ENV` placeholder, the API refuses to start with
|
||||
`Refusing to start: required secret configuration is missing…`. Production/Staging must additionally supply
|
||||
real `IdentitySettings` JWE keys and `Seams:FieldEncryption` keys; Development uses the dev-only ones in
|
||||
`appsettings.Development.json`.
|
||||
- **Never change `Seams:FieldEncryption:Key`/`:HashKey`.** They decrypt every PII column in the shared
|
||||
database and derive `users.PhoneHash`, which every login looks up. Changing either locks everyone out.
|
||||
- **Enable the secret-scan pre-commit hook** once per clone so a stray credential can't be committed:
|
||||
`git config core.hooksPath .githooks` (see [`.githooks/README.md`](../../../.githooks/README.md)).
|
||||
- **Behind a reverse proxy**, list its address in `ForwardedHeaders:KnownProxies` (or a CIDR in
|
||||
@@ -243,7 +249,7 @@ world, wipe the volume (`docker compose down -v`) and boot again.
|
||||
| Symptom | Fix |
|
||||
| --- | --- |
|
||||
| Browser: `net::ERR_CERT_AUTHORITY_INVALID` on `:5002` | Run `dotnet dev-certs https --trust` (setup step 1). |
|
||||
| API startup: `Refusing to start: required secret configuration is missing…` | The connection-string user-secret isn't set (or still the placeholder). Do setup step 3. |
|
||||
| API startup: `Login failed for user 'sa'` / connect timeout | DB not up or wrong password — check `docker compose ps` and that the user-secrets password matches `docker-compose.yml`. |
|
||||
| API startup: `Refusing to start: required secret configuration is missing…` | `ConnectionStrings:SqlServer` is blank or still a placeholder. Do setup step 3. |
|
||||
| API startup: `Login failed for user 'sa'` / connect timeout | DB not up or wrong password — check `docker compose ps` and that the password in `appsettings.Development.json` matches `docker-compose.yml`. |
|
||||
| Console: `...has been blocked by CORS policy` | `UseCors` missing/mis-ordered, or the browser origin isn't in `Cors:AllowedOrigins`. It must sit after `UseRouting` and before the rate limiter. |
|
||||
| `dotnet user-secrets` errors with "could not find UserSecretsId" | Run it from `server/src/API/Baya.Web.Api` (the project with `<UserSecretsId>`). |
|
||||
| `dotnet user-secrets` errors with "could not find UserSecretsId" | Expected — user-secrets was removed. Edit `appsettings.Development.json` instead. |
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Balinyaar — full stack for the balinyaar.ir deployment.
|
||||
#
|
||||
# Three containers, no published ports: everything is reached through the EXISTING Caddy on `caddy_net`,
|
||||
# which terminates TLS for balinyaar.ir (→ web) and api.balinyaar.ir (→ api). See DEPLOY.md for the
|
||||
# Caddyfile block to add, and deploy/Caddyfile for a copy of it.
|
||||
#
|
||||
# The database is NOT here — it is the remote SQL Server already configured in
|
||||
# server/src/API/Baya.Web.Api/appsettings.Development.json.
|
||||
#
|
||||
# docker compose up -d --build
|
||||
# docker compose logs -f api
|
||||
|
||||
services:
|
||||
api:
|
||||
build:
|
||||
context: ./server
|
||||
image: balinyaar-api
|
||||
container_name: balinyaar-api
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- otp-relay
|
||||
environment:
|
||||
# Deliberate: the deployed API runs as Development so the demo + lifecycle seeders populate the
|
||||
# shared database and the screens aren't empty. This also exposes the developer exception page and
|
||||
# GET /api/v1/dev/last_otp/{phone} publicly — acceptable for a pre-launch demo, NOT for real users.
|
||||
# Switch to Production (and supply non-placeholder crypto keys) before launch — see DEPLOY.md.
|
||||
ASPNETCORE_ENVIRONMENT: Development
|
||||
|
||||
# The two values that genuinely differ between a laptop and this network. Everything else — crypto
|
||||
# keys, connection strings, CORS origins, trusted proxy networks — lives in appsettings.Development.json.
|
||||
Seams__Sms__Telegram__BaseUrl: http://balinyaar-otp-relay:5010
|
||||
Seams__ObjectStorage__RootPath: /app/data/object-storage
|
||||
volumes:
|
||||
# Uploaded verification documents live on the local-disk object-storage seam; without this they are
|
||||
# inside the container and vanish on the next `up --build`.
|
||||
- api-object-storage:/app/data/object-storage
|
||||
- api-logs:/app/logs
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
web:
|
||||
build:
|
||||
context: ./client
|
||||
image: balinyaar-web
|
||||
container_name: balinyaar-web
|
||||
restart: unless-stopped
|
||||
# No environment here on purpose: every NEXT_PUBLIC_* value is compiled into the browser bundle at
|
||||
# build time from client/.env.production. Setting one here would be silently ignored.
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
otp-relay:
|
||||
build:
|
||||
context: ./telegram-otp-bot
|
||||
image: balinyaar-otp-relay
|
||||
container_name: balinyaar-otp-relay
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
TELEGRAM_BOT_TOKEN: "8968527151:AAFiCuNGkXjOiLZfT6urU8tkW8SCsWDM0ic"
|
||||
# Every id here receives EVERY login code, for every phone number. Keep it to people you trust.
|
||||
TELEGRAM_CHAT_IDS: "1277103616,110209855"
|
||||
# Must equal Seams:Sms:Telegram:ApiKey in the API's appsettings.Development.json.
|
||||
API_KEY: "6a8dfaeea1aa375eb61da7663cadf23a3ea245fd939264dd"
|
||||
# api.telegram.org is filtered in Iran — this hop goes out through the proxy container that already
|
||||
# sits on caddy_net. A wrong value fails at boot with a clear message rather than per-OTP.
|
||||
TELEGRAM_PROXY_URL: http://hysteria-client:8081
|
||||
# The code still arrives on Telegram; keeping it out of `docker logs` means a host-log reader can't
|
||||
# harvest login codes.
|
||||
REDACT_CODE_IN_LOGS: "true"
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:5010/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
volumes:
|
||||
api-object-storage:
|
||||
api-logs:
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
@@ -0,0 +1,18 @@
|
||||
# Build artefacts — copying them in poisons the container's restore/publish with host-built binaries.
|
||||
**/bin/
|
||||
**/obj/
|
||||
**/logs/
|
||||
**/.vs/
|
||||
**/graphify-out/
|
||||
|
||||
# Never needed by the image
|
||||
**/*.user
|
||||
**/*.suo
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
CLAUDE.md
|
||||
CONVENTIONS.md
|
||||
AGENTS.md
|
||||
README.md
|
||||
LICENSE.md
|
||||
+9
-8
@@ -67,8 +67,10 @@ Persistence below). **In deployed environments**, boot instead only *checks* the
|
||||
reachable SQL Server is required to start. Startup **fails fast**
|
||||
(`StartupSecretsGuard`) if a load-bearing secret — the DB connection strings, and in deployed environments the
|
||||
JWE + field-encryption keys — is missing or left at its committed `SET_VIA_USER_SECRETS_OR_ENV` placeholder
|
||||
(refinement-phase-5). Development supplies working dev-only crypto keys via `appsettings.Development.json`; only
|
||||
the connection string must come from user-secrets (see [RUNBOOK](../dev/post-phase/refinement/RUNBOOK.md)).
|
||||
(refinement-phase-5). **`dotnet user-secrets` is no longer used** — the `<UserSecretsId>` was removed from
|
||||
`Baya.Web.Api.csproj`, so that store is not read at all. Every value, connection strings and dev-only crypto keys
|
||||
alike, lives in `appsettings.Development.json`; the deployment's two container-specific overrides live in the root
|
||||
`docker-compose.yml` (see [DEPLOY.md](../DEPLOY.md)).
|
||||
|
||||
---
|
||||
|
||||
@@ -128,11 +130,10 @@ stamped by `AuditFieldInterceptor` (Persistence), not in handlers.
|
||||
**real HTTP adapter** in `Baya.Infrastructure.CrossCutting/Seams/Real/`, **config-selected** by a per-rail
|
||||
`Seams:*:Provider` selector in `AddCrossCuttingSeams` (default = the mock, so an unconfigured env is unchanged;
|
||||
a typo falls closed to the mock). Real adapters use `HttpClient` (typed via `IHttpClientFactory`) +
|
||||
`System.Text.Json` + BCL crypto — **no new NuGet packages**; credentials come from `Seams:*` (user-secrets/env,
|
||||
never committed). Swapping is a registration change; **no handler is touched**. The adapters:
|
||||
`System.Text.Json` + BCL crypto — **no new NuGet packages**; credentials come from `Seams:*` (appsettings/env). Swapping is a registration change; **no handler is touched**. The adapters:
|
||||
`KavenegarSmsSender` (`Sms:Provider=kavenegar` — **launch-critical**; when a real gateway is selected the
|
||||
Development OTP-in-logs bridge is **disabled**, so the OTP is never logged), `TelegramSmsSender`
|
||||
(`Sms:Provider=telegram` — **Development-only, broadcast, not a gateway**: it posts to the standalone
|
||||
(`Sms:Provider=telegram` — **broadcast, not a gateway**; the pre-launch demo OTP rail: it posts to the standalone
|
||||
`telegram-otp-bot/` relay, which pushes *every* code to a fixed list of Telegram chat ids, so manual testing
|
||||
beats reading OTPs out of the log. It is the **one non-mock SMS provider that keeps the OTP-capture bridge
|
||||
enabled** — see Startup wiring — and its `Seams:Sms:Telegram:ApiKey` is a user-secret, never committed),
|
||||
@@ -733,9 +734,9 @@ action to `sender.Send(...)`. Full conventions are in [CONVENTIONS.md](CONVENTIO
|
||||
- Dynamic permission system: `DynamicPermissionHandler` reads `[controller]` + `[action]` route
|
||||
values and checks role claims. Always use `[controller]`/`[action]` tokens so the keys stay
|
||||
consistent (see CONVENTIONS.md §1 Routing).
|
||||
- Settings bound from `appsettings.json` → `IdentitySettings`. **JWE keys are never committed**: the
|
||||
committed values are `SET_VIA_USER_SECRETS_OR_ENV` placeholders (real ones via user-secrets/env; Development
|
||||
uses dev-only keys in `appsettings.Development.json`). `RequireHttpsMetadata` is **on outside Dev/Testing**
|
||||
- Settings bound from `appsettings.json` → `IdentitySettings`. The base `appsettings.json` carries
|
||||
`SET_VIA_USER_SECRETS_OR_ENV` placeholders that `StartupSecretsGuard` rejects; the real values live in the
|
||||
environment-specific file (`appsettings.Development.json` holds the dev-only keys the demo deployment runs on). `RequireHttpsMetadata` is **on outside Dev/Testing**
|
||||
(passed into `RegisterIdentityServices`), the access-token lifetime is `ExpirationMinutes: 60`, and
|
||||
`Issuer`/`Audience` are real (`Balinyaar`/`BalinyaarClient`) — refinement-phase-5.
|
||||
- Auth and OTP endpoints must be rate-limited (CONVENTIONS.md §11) — `request_otp`/`verify_otp` use
|
||||
|
||||
@@ -477,8 +477,8 @@ Place these tests in a dedicated `Baya.Test.Api` project so they can run against
|
||||
|
||||
## 11. Security rules
|
||||
|
||||
- **Never hardcode secrets.** Keys, connection strings, and tokens come from `appsettings.*.json` / user-secrets / environment variables, bound to typed settings classes.
|
||||
- `SecretKey` and `Encryptkey` (in `IdentitySettings`) must be set in environment-specific config, never in `appsettings.json` committed to the repo.
|
||||
- **Never hardcode secrets in C#.** Keys, connection strings, and tokens come from `appsettings.*.json` or environment variables, bound to typed settings classes — never a literal in a handler or service. (`dotnet user-secrets` is not used; see [DEPLOY.md](../DEPLOY.md) for the configuration model.)
|
||||
- `SecretKey` and `Encryptkey` (in `IdentitySettings`) belong in the environment-specific file, never in the base `appsettings.json`, which stays at its `StartupSecretsGuard`-rejected placeholder.
|
||||
- Always validate all external input with FluentValidation before processing.
|
||||
- EF Core parameterizes queries automatically — never concatenate raw SQL.
|
||||
- If you must use raw SQL, use `FromSqlInterpolated` (parameterized), never `FromSqlRaw` with user data.
|
||||
|
||||
+31
-27
@@ -1,36 +1,40 @@
|
||||
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
# Balinyaar API — build context is `server/` (see the root docker-compose.yml).
|
||||
#
|
||||
# The csproj/props files are copied on their own first so `dotnet restore` lands in a layer that only
|
||||
# re-runs when a project reference or package version actually changes; the source copy below it churns
|
||||
# on every commit. Directory.Packages.props is the central version manifest — restore fails without it.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["../Directory.Packages.props", "./"]
|
||||
COPY ["src/API/Baya.Web.Api/Baya.Web.Api.csproj", "src/API/Baya.Web.Api/"]
|
||||
COPY ["src/API/Baya.WebFramework/Baya.WebFramework.csproj", "src/API/Baya.WebFramework/"]
|
||||
COPY ["src/API/Plugins/Baya.Web.Plugins.Grpc/Baya.Web.Plugins.Grpc.csproj", "src/API/Plugins/Baya.Web.Plugins.Grpc/"]
|
||||
COPY ["src/Core/Baya.Application/Baya.Application.csproj", "src/Core/Baya.Application/"]
|
||||
COPY ["src/Core/Baya.Domain/Baya.Domain.csproj", "src/Core/Baya.Domain/"]
|
||||
COPY ["src/Infrastructure/Baya.Infrastructure.CrossCutting/Baya.Infrastructure.CrossCutting.csproj", "src/Infrastructure/Baya.Infrastructure.CrossCutting/"]
|
||||
COPY ["src/Infrastructure/Baya.Infrastructure.Identity/Baya.Infrastructure.Identity.csproj", "src/Infrastructure/Baya.Infrastructure.Identity/"]
|
||||
COPY ["src/Infrastructure/Baya.Infrastructure.Persistence/Baya.Infrastructure.Persistence.csproj", "src/Infrastructure/Baya.Infrastructure.Persistence/"]
|
||||
COPY ["src/Infrastructure/Baya.Infrastructure.Monitoring/Baya.Infrastructure.Monitoring.csproj", "src/Infrastructure/Baya.Infrastructure.Monitoring/"]
|
||||
COPY ["src/Shared/Baya.SharedKernel/Baya.SharedKernel.csproj", "src/Shared/Baya.SharedKernel/"]
|
||||
COPY ["src/Tests/Baya.Test.Infrastructure.Identity/Baya.Test.Infrastructure.Identity/Baya.Test.Infrastructure.Identity.csproj", "src/Tests/Baya.Test.Infrastructure.Identity/Baya.Test.Infrastructure.Identity/"]
|
||||
COPY ["src/Tests/Baya.Tests.Setup/Baya.Tests.Setup.csproj", "src/Tests/Baya.Tests.Setup/"]
|
||||
|
||||
COPY Directory.Packages.props ./
|
||||
COPY src/API/Baya.Web.Api/Baya.Web.Api.csproj src/API/Baya.Web.Api/
|
||||
COPY src/API/Baya.WebFramework/Baya.WebFramework.csproj src/API/Baya.WebFramework/
|
||||
COPY src/API/Plugins/Baya.Web.Plugins.Grpc/Baya.Web.Plugins.Grpc.csproj src/API/Plugins/Baya.Web.Plugins.Grpc/
|
||||
COPY src/Core/Baya.Application/Baya.Application.csproj src/Core/Baya.Application/
|
||||
COPY src/Core/Baya.Domain/Baya.Domain.csproj src/Core/Baya.Domain/
|
||||
COPY src/Infrastructure/Baya.Infrastructure.CrossCutting/Baya.Infrastructure.CrossCutting.csproj src/Infrastructure/Baya.Infrastructure.CrossCutting/
|
||||
COPY src/Infrastructure/Baya.Infrastructure.Identity/Baya.Infrastructure.Identity.csproj src/Infrastructure/Baya.Infrastructure.Identity/
|
||||
COPY src/Infrastructure/Baya.Infrastructure.Monitoring/Baya.Infrastructure.Monitoring.csproj src/Infrastructure/Baya.Infrastructure.Monitoring/
|
||||
COPY src/Infrastructure/Baya.Infrastructure.Persistence/Baya.Infrastructure.Persistence.csproj src/Infrastructure/Baya.Infrastructure.Persistence/
|
||||
COPY src/Shared/Baya.SharedKernel/Baya.SharedKernel.csproj src/Shared/Baya.SharedKernel/
|
||||
|
||||
RUN dotnet restore src/API/Baya.Web.Api/Baya.Web.Api.csproj
|
||||
|
||||
RUN dotnet restore "src/API/Baya.Web.Api/Baya.Web.Api.csproj"
|
||||
COPY . .
|
||||
WORKDIR "src/API/Baya.Web.Api"
|
||||
RUN dotnet build "Baya.Web.Api.csproj" -c Release -o /app/build
|
||||
RUN dotnet publish src/API/Baya.Web.Api/Baya.Web.Api.csproj \
|
||||
-c Release -o /app/publish /p:UseAppHost=false --no-restore
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "Baya.Web.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
|
||||
|
||||
FROM base AS final
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
# TLS is terminated by Caddy; the API speaks plain HTTP on the shared container network only.
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
EXPOSE 8080
|
||||
|
||||
# Written to by the Serilog file sink (Development) and the local-disk object storage seam — both are
|
||||
# bind-mounted in compose so an image rebuild doesn't discard uploaded verification documents.
|
||||
RUN mkdir -p /app/logs /app/data/object-storage
|
||||
|
||||
ENTRYPOINT ["dotnet", "Baya.Web.Api.dll"]
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
<IsPackable>true</IsPackable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
<!-- Enables `dotnet user-secrets` for the local-dev connection string (never a committed secret). -->
|
||||
<UserSecretsId>baya-web-api</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
|
||||
|
||||
@@ -9,9 +9,9 @@ namespace Baya.Web.Api.Configuration;
|
||||
/// A database connection is required in every real environment; the JWE + field-encryption keys are
|
||||
/// required only in <b>deployed</b> environments (Development keeps working dev-only defaults in
|
||||
/// <c>appsettings.Development.json</c>, and the "Testing" environment runs on in-memory SQLite with
|
||||
/// test-injected keys). The effect: a fresh clone with no user-secrets stops at boot with a clear
|
||||
/// message instead of silently connecting somewhere unintended, and a deployment can never fall back
|
||||
/// to a committed placeholder key.
|
||||
/// test-injected keys). The effect: a clone whose configuration was never filled in stops at boot with
|
||||
/// a clear message instead of silently connecting somewhere unintended, and a Production/Staging
|
||||
/// deployment can never fall back to a committed placeholder or a dev-only key.
|
||||
/// </summary>
|
||||
public static class StartupSecretsGuard
|
||||
{
|
||||
@@ -39,7 +39,7 @@ public static class StartupSecretsGuard
|
||||
RequireReal(errors, "ConnectionStrings:logDb", config.GetConnectionString("logDb"));
|
||||
|
||||
// Development supplies working dev-only keys via appsettings.Development.json; only deployed
|
||||
// environments must inject real per-environment secrets (env vars / Key Vault / KMS).
|
||||
// environments must supply real per-environment secrets.
|
||||
if (!builder.Environment.IsDevelopment())
|
||||
{
|
||||
RequireReal(errors, "IdentitySettings:SecretKey", config["IdentitySettings:SecretKey"]);
|
||||
@@ -53,8 +53,8 @@ public static class StartupSecretsGuard
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"Refusing to start: required secret configuration is missing or still a committed placeholder. " +
|
||||
"Provide real values via user-secrets (Development) or environment variables (deployed) — see " +
|
||||
"dev/post-phase/refinement/RUNBOOK.md.\n - " + string.Join("\n - ", errors));
|
||||
"Provide real values in appsettings.<Environment>.json (or as Seams__…-style environment " +
|
||||
"variables) — see DEPLOY.md.\n - " + string.Join("\n - ", errors));
|
||||
}
|
||||
|
||||
private static void RequireReal(List<string> errors, string key, string? value)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"SqlServer": "Server=87.107.152.16,1433;Database=Baya;User Id=hamid_root_un_sa;Password=N8@s5Taw1zWeh@#Hm;TrustServerCertificate=True;Encrypt=False;",
|
||||
"logDb":"Server=87.107.152.16,1433;Database=Baya_Logs;User Id=hamid_root_un_sa;Password=N8@s5Taw1zWeh@#Hm;TrustServerCertificate=True;Encrypt=False;"
|
||||
"logDb": "Server=87.107.152.16,1433;Database=Baya_Logs;User Id=hamid_root_un_sa;Password=N8@s5Taw1zWeh@#Hm;TrustServerCertificate=True;Encrypt=False;"
|
||||
},
|
||||
"IdentitySettings": {
|
||||
"SecretKey": "SET_VIA_USER_SECRETS_OR_ENV",
|
||||
"Encryptkey": "SET_VIA_USER_SECRETS_OR_ENV",
|
||||
"SecretKey": "dev-only-jwe-signing-key-not-for-production-0123456789abcdef",
|
||||
"Encryptkey": "dev-only-16bytes",
|
||||
"Issuer": "Balinyaar",
|
||||
"Audience": "BalinyaarClient",
|
||||
"NotBeforeMinutes": "0",
|
||||
@@ -13,8 +13,9 @@
|
||||
},
|
||||
"Seams": {
|
||||
"FieldEncryption": {
|
||||
"Key": "SET_VIA_USER_SECRETS_OR_ENV",
|
||||
"HashKey": "SET_VIA_USER_SECRETS_OR_ENV"
|
||||
"//": "DO NOT CHANGE. Every encrypted column in the Baya database (phones, addresses, IBANs, clinical notes) was written with these exact values, and users.PhoneHash — which every login looks up — is derived from HashKey. Rotating either makes the existing data unreadable and locks every account out.",
|
||||
"Key": "local-dev-field-encryption-key-not-for-production",
|
||||
"HashKey": "local-dev-field-hash-key-not-for-production"
|
||||
},
|
||||
"ObjectStorage": {
|
||||
"RootPath": ""
|
||||
@@ -23,7 +24,8 @@
|
||||
"Provider": "telegram",
|
||||
"Telegram": {
|
||||
"BaseUrl": "http://127.0.0.1:5010",
|
||||
"ApiKey": "ab8984974bc1fe5ce514d0fd74f71c8738b3aed92a7e4d86",
|
||||
"//": "Must equal the relay's API_KEY. NOT the value in telegram-otp-bot/.env.example — that one is published, so TelegramSmsSender rejects it.",
|
||||
"ApiKey": "6a8dfaeea1aa375eb61da7663cadf23a3ea245fd939264dd",
|
||||
"TimeoutSeconds": 10
|
||||
}
|
||||
},
|
||||
@@ -34,11 +36,20 @@
|
||||
}
|
||||
},
|
||||
"Cors": {
|
||||
"AllowedOrigins": []
|
||||
"AllowedOrigins": [
|
||||
"https://balinyaar.ir",
|
||||
"https://www.balinyaar.ir",
|
||||
"http://localhost:3000"
|
||||
]
|
||||
},
|
||||
"ForwardedHeaders": {
|
||||
"//": "Docker bridge ranges — the reverse proxy (Caddy) shares a container network with the API, so its hop must be trusted for X-Forwarded-For to resolve the real client IP the rate limiter partitions on.",
|
||||
"KnownProxies": [],
|
||||
"KnownNetworks": []
|
||||
"KnownNetworks": [
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"10.0.0.0/8"
|
||||
]
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Kestrel": {
|
||||
|
||||
+12
-7
@@ -13,9 +13,11 @@ namespace Baya.Infrastructure.CrossCutting.Seams.Real;
|
||||
/// single JSON call: <c>POST /send_otp</c> and <c>POST /send</c>, authenticated with the shared
|
||||
/// <c>X-Api-Key</c> secret.
|
||||
///
|
||||
/// <para><b>Development only.</b> The relay broadcasts every message to a fixed list of Telegram chat ids — every
|
||||
/// recipient reads every code. It exists so manual testing beats reading OTPs out of the server log; it is not a
|
||||
/// gateway and must never be selected in a deployed environment (see <see cref="TelegramOptions"/>).</para>
|
||||
/// <para><b>Broadcast, not per-user routing.</b> The relay sends every message to a fixed list of Telegram chat
|
||||
/// ids — every recipient reads every code, whichever phone requested it. That makes it a shared-inbox channel for
|
||||
/// a small trusted group, not an SMS gateway: it is the deliberate OTP rail for the pre-launch demo deployment
|
||||
/// (no Iranian gateway contract yet), and must be replaced by <see cref="KavenegarSmsSender"/> before real
|
||||
/// customers sign up (see <see cref="TelegramOptions"/>).</para>
|
||||
///
|
||||
/// <para><b>The OTP is never logged</b> — only the phone tail and the relay's HTTP outcome, exactly like
|
||||
/// <see cref="KavenegarSmsSender"/>. A non-2xx (the relay answers <c>502</c> when <i>no</i> recipient got the
|
||||
@@ -27,7 +29,9 @@ public sealed class TelegramSmsSender(
|
||||
IOptions<SeamOptions> options,
|
||||
ILogger<TelegramSmsSender> logger) : ISmsSender
|
||||
{
|
||||
/// <summary>The repo's committed stand-in for a secret — treated as "not configured".</summary>
|
||||
/// <summary>The example key published in <c>telegram-otp-bot/.env.example</c> and its README. Anyone
|
||||
/// reading the repo knows it, so it is a documentation sample, not a secret — treated as "not configured"
|
||||
/// so a deployment can never quietly authenticate the OTP rail with a publicly-known value.</summary>
|
||||
private const string SecretPlaceholder = "ab8984974bc1fe5ce514d0fd74f71c8738b3aed92a7e4d86";
|
||||
|
||||
private readonly TelegramOptions _options = options.Value.Sms.Telegram;
|
||||
@@ -43,11 +47,12 @@ public sealed class TelegramSmsSender(
|
||||
// Fail with the config key rather than sending an unauthenticated request the relay answers with a bare
|
||||
// 401 — the cause of that 401 is invisible from this side.
|
||||
var apiKey = _options.ApiKey;
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
if (string.IsNullOrWhiteSpace(apiKey) || apiKey == SecretPlaceholder)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Seams:Sms:Telegram:ApiKey is not configured. Set it (user-secrets or environment) to the same " +
|
||||
"value as the relay's API_KEY, or select another Seams:Sms:Provider.");
|
||||
"Seams:Sms:Telegram:ApiKey is not configured (unset, or still the published example key). Set it " +
|
||||
"(appsettings or environment) to the same value as the relay's API_KEY, or select another " +
|
||||
"Seams:Sms:Provider.");
|
||||
}
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, path)
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||
|
||||
/// <summary>
|
||||
/// Options bound from the <c>Seams</c> configuration section. The mock seams read non-secret defaults
|
||||
/// from here; production keys/paths come from environment variables or user-secrets, never committed.
|
||||
/// from here; deployed keys/paths come from the environment-specific appsettings file or environment variables.
|
||||
///
|
||||
/// <para><b>Provider selection (refinement-phase-8).</b> Each vendor rail carries a <c>Provider</c> selector
|
||||
/// (default = the mock, so an unconfigured environment behaves exactly as before). Setting it to a real
|
||||
@@ -37,7 +37,7 @@ public sealed class SeamOptions
|
||||
/// (<c>IShahkarVerifier</c>), e-KYC (<c>IIdentityKycProvider</c>), and استعلام شبا
|
||||
/// (<c>IBankAccountOwnershipVerifier</c>). Each seam opts in with its own <c>Provider = finnotech</c> selector,
|
||||
/// but they authenticate against the same tenant, so the connection facts live here once. All values are
|
||||
/// secrets — user-secrets / environment, never committed.
|
||||
/// secrets — the environment-specific appsettings file or environment variables.
|
||||
/// </summary>
|
||||
public sealed class FinnotechOptions
|
||||
{
|
||||
@@ -63,8 +63,9 @@ public static class SeamProviders
|
||||
public const string SmsIr = "smsir";
|
||||
public const string Ghasedak = "ghasedak";
|
||||
|
||||
/// <summary><b>Development convenience channel, not an SMS gateway</b> — the local <c>telegram-otp-bot/</c>
|
||||
/// relay broadcasts every code to a fixed list of Telegram chat ids. Never select it in a real environment.</summary>
|
||||
/// <summary><b>Broadcast relay, not an SMS gateway</b> — the standalone <c>telegram-otp-bot/</c> service
|
||||
/// sends every code to a fixed list of Telegram chat ids. The pre-launch demo rail; replace with a real
|
||||
/// gateway before onboarding customers outside the trusted group.</summary>
|
||||
public const string Telegram = "telegram";
|
||||
|
||||
// Object storage
|
||||
@@ -105,7 +106,7 @@ public sealed class SmsOptions
|
||||
/// (Development only).</summary>
|
||||
public string Provider { get; set; } = SeamProviders.Mock;
|
||||
|
||||
/// <summary>Gateway API key / token (secret — user-secrets or environment, never committed).</summary>
|
||||
/// <summary>Gateway API key / token (secret — the environment-specific appsettings file or environment variables).</summary>
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The registered sender line (used by <c>SendAsync</c> free-form messages and non-template sends).</summary>
|
||||
@@ -122,13 +123,15 @@ public sealed class SmsOptions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <b>Development-only</b> Telegram OTP relay (the standalone <c>telegram-otp-bot/</c> Node service),
|
||||
/// selected by <c>Seams:Sms:Provider = telegram</c>. It replaces "read the OTP out of the server log" during
|
||||
/// manual testing — the tester gets the code on their phone without paying an Iranian SMS gateway.
|
||||
/// The Telegram OTP relay (the standalone <c>telegram-otp-bot/</c> Node service), selected by
|
||||
/// <c>Seams:Sms:Provider = telegram</c>. It replaces "read the OTP out of the server log" — the tester gets the
|
||||
/// code on their phone without an Iranian SMS gateway contract.
|
||||
///
|
||||
/// <para><b>It is not an SMS gateway.</b> There is no per-user routing: the relay <i>broadcasts</i> every code
|
||||
/// to a fixed list of Telegram chat ids, so every configured recipient reads every login code. That is fine for
|
||||
/// a test group and disqualifying for anything else — never point a deployed environment at it.</para>
|
||||
/// to a fixed list of Telegram chat ids, so every configured recipient reads every login code. That is workable
|
||||
/// for a trusted demo group — which is why the pre-launch <c>balinyaar.ir</c> deployment uses it — and
|
||||
/// disqualifying once anyone outside that group can request a code. Switch <c>Seams:Sms:Provider</c> to
|
||||
/// <c>kavenegar</c> at that point; nothing else changes.</para>
|
||||
/// </summary>
|
||||
public sealed class TelegramOptions
|
||||
{
|
||||
@@ -137,7 +140,7 @@ public sealed class TelegramOptions
|
||||
|
||||
/// <summary>The shared secret sent as the relay's <c>X-Api-Key</c> header — it must equal the relay's
|
||||
/// <c>API_KEY</c>. <b>Secret:</b> committed config carries an empty/placeholder value; the real one comes
|
||||
/// from user-secrets (<c>Seams:Sms:Telegram:ApiKey</c>) or the environment, never git.</summary>
|
||||
/// from <c>Seams:Sms:Telegram:ApiKey</c> in appsettings or the environment.</summary>
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Per-request timeout. The relay itself talks to Telegram (over a proxy in a filtered region), so
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ public static class ServiceCollectionExtension
|
||||
/// token swaps in the real HTTP adapter behind the same Application contract — <b>callers never change</b>. An
|
||||
/// unconfigured/typo'd provider falls closed to the mock. This makes a partial rollout the normal case (real SMS
|
||||
/// + real geocoder while payments stay mocked in a pre-launch environment). Real adapters read credentials from
|
||||
/// <c>Seams:*</c> (user-secrets/environment) and get an <see cref="System.Net.Http.HttpClient"/> from the
|
||||
/// <c>Seams:*</c> (appsettings/environment) and get an <see cref="System.Net.Http.HttpClient"/> from the
|
||||
/// <c>IHttpClientFactory</c>. (The real in-app <c>INotificationDispatcher</c> needs the database, so it is
|
||||
/// registered in the Persistence layer.)
|
||||
/// </summary>
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ public class SeedDataBase : ISeedDataBase
|
||||
}
|
||||
|
||||
// The bootstrap admin is config-driven, never a committed credential: it is created only when both
|
||||
// Seed:AdminUsername and Seed:AdminPassword are supplied (via user-secrets in Development, environment
|
||||
// Seed:AdminUsername and Seed:AdminPassword are supplied (via the environment-specific appsettings file, environment
|
||||
// variables in a deployment). With neither configured — the default for Testing and any fresh boot —
|
||||
// no admin account is created, so no well-known password ever lands in a real database. Day-to-day
|
||||
// admins reach the backoffice through the phone-OTP demo seeds (Development) or are provisioned
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
using Baya.Application.Contracts.Analytics;
|
||||
using Baya.Application.Contracts.Analytics;
|
||||
using Baya.Application.Contracts.Audit;
|
||||
using Baya.Application.Contracts.Common;
|
||||
using Baya.Application.Contracts.Configuration;
|
||||
@@ -129,7 +129,7 @@ public static class ServiceCollectionExtensions
|
||||
/// <summary>
|
||||
/// Idempotently seeds one active <c>standard</c> payment gateway so the b10 card rail has a selectable
|
||||
/// provider out of the box. <c>config_json</c> is encrypted at rest by the EF converter on save (so it
|
||||
/// must go through the DbContext, not <c>HasData</c>). Real merchant credentials come from user-secrets /
|
||||
/// must go through the DbContext, not <c>HasData</c>). Real merchant credentials come from appsettings /
|
||||
/// environment per deployment — this sandbox row is non-secret and only enables the local/dev flow.
|
||||
/// </summary>
|
||||
public static async Task SeedPaymentGatewaysAsync(this WebApplication app)
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed class BayaApiFactory : WebApplicationFactory<Program>
|
||||
_keepAlive = new SqliteConnection(_connectionString);
|
||||
_keepAlive.Open();
|
||||
|
||||
// The committed appsettings.json ships placeholder JWE keys (real ones come from user-secrets /
|
||||
// The committed appsettings.json ships placeholder JWE keys (real ones come from the environment-specific appsettings file /
|
||||
// env in Development / deploy). The Testing host has neither, so supply working test keys via
|
||||
// environment variables — they sit after appsettings.json in the default config chain, so they
|
||||
// reliably override the placeholders. The Encrypt key must be exactly 16 bytes for the AES-128
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
*.log
|
||||
|
||||
# Local secrets — the deployed values come from compose, and a copied .env would silently win on any
|
||||
# key the compose environment doesn't set.
|
||||
.env
|
||||
.env.local
|
||||
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
README.md
|
||||
INTEGRATION-PROMPT.md
|
||||
@@ -1,4 +1,6 @@
|
||||
# Copy to .env and fill in. Never commit .env.
|
||||
# Copy to .env and fill in — for LOCAL runs (`npm start`) only. Never commit .env.
|
||||
# The deployed stack sets every one of these in the root docker-compose.yml instead; nothing here is read
|
||||
# inside the container.
|
||||
|
||||
# From @BotFather — the full token, e.g. 1234567890:AAH....
|
||||
TELEGRAM_BOT_TOKEN=8968527151:AAFiCuNGkXjOiLZfT6urU8tkW8SCsWDM0ic
|
||||
@@ -12,7 +14,10 @@ TELEGRAM_CHAT_IDS=1277103616,110209855
|
||||
# REQUIRED. Shared secret the caller must send as the `X-Api-Key` header.
|
||||
# Minimum 16 chars; the process refuses to start without it.
|
||||
# Generate one: node -e "console.log(require('crypto').randomBytes(24).toString('hex'))"
|
||||
# The same value goes into the .NET side's Seams:Sms:Telegram:ApiKey (user-secrets, never committed).
|
||||
# The same value goes into the .NET side's Seams:Sms:Telegram:ApiKey (appsettings.Development.json).
|
||||
#
|
||||
# The value below is the PUBLISHED EXAMPLE — it is in git and in the README, so it is not a secret, and
|
||||
# TelegramSmsSender deliberately refuses to authenticate with it. Replace it in your own .env.
|
||||
API_KEY=ab8984974bc1fe5ce514d0fd74f71c8738b3aed92a7e4d86
|
||||
|
||||
# HTTP listener
|
||||
@@ -27,6 +32,7 @@ REDACT_CODE_IN_LOGS=false
|
||||
# this process tunnels its Telegram calls through it (HTTP CONNECT or SOCKS5, with optional
|
||||
# user:pass@ credentials). Leave it unset for a direct connection — nothing else changes.
|
||||
# local machine with a VPN client: http://127.0.0.1:10809 / socks5://127.0.0.1:10808
|
||||
# VPS with a proxy container: http://proxy:1080 (the container name on the shared docker network)
|
||||
# VPS with a proxy container: http://hysteria-client:8081 (the container name on caddy_net —
|
||||
# what the deployed stack uses, set in the root docker-compose.yml)
|
||||
# HTTPS_PROXY / ALL_PROXY are honoured as a fallback if TELEGRAM_PROXY_URL is unset.
|
||||
# TELEGRAM_PROXY_URL=http://127.0.0.1:10809
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Balinyaar Telegram OTP relay — build context is `telegram-otp-bot/`.
|
||||
#
|
||||
# Zero dependencies (Node built-ins only), so there is no install step and no build stage: the image is
|
||||
# the base runtime plus four source files.
|
||||
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
COPY src ./src
|
||||
|
||||
# The relay must accept connections from the API container, not just its own loopback. Everything else
|
||||
# (bot token, chat ids, API key, proxy URL) is supplied by compose — src/env.js lets real environment
|
||||
# variables win over any .env file, so nothing here depends on one existing.
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=5010
|
||||
|
||||
USER node
|
||||
EXPOSE 5010
|
||||
CMD ["node", "src/server.js"]
|
||||
@@ -1,15 +1,17 @@
|
||||
# balinyaar-telegram-otp-bot
|
||||
|
||||
A **standalone, dev-only** Telegram relay. It is not part of `client/` or `server/` — it is its own
|
||||
tiny Node project with **zero dependencies** (Node 18+ built-ins only: `node:http` + global `fetch`).
|
||||
A **standalone** Telegram relay. It is not part of `client/` or `server/` — it is its own tiny Node
|
||||
project with **zero dependencies** (Node 18+ built-ins only: `node:http` + global `fetch`).
|
||||
|
||||
Its whole job: expose an HTTP endpoint that the .NET API calls, and forward the message to a fixed
|
||||
list of Telegram chat ids. That replaces "read the OTP out of the server log" during manual testing —
|
||||
you get the code on your phone instead, without paying an Iranian SMS gateway.
|
||||
list of Telegram chat ids. That replaces "read the OTP out of the server log" — you get the code on
|
||||
your phone instead, without an Iranian SMS gateway contract.
|
||||
|
||||
> **Development only.** There is no per-user routing: *every* configured recipient receives *every*
|
||||
> OTP, regardless of which phone number requested it. That is fine for a test group; it is not an SMS
|
||||
> gateway. Do not point a real environment at this.
|
||||
> **Broadcast, not routing.** *Every* configured recipient receives *every* OTP, regardless of which
|
||||
> phone number requested it. That makes this a shared inbox for a small trusted group, not an SMS
|
||||
> gateway. It is the OTP rail for local development **and** for the pre-launch `balinyaar.ir` demo
|
||||
> deployment — it must be swapped for `Seams:Sms:Provider = kavenegar` before anyone outside that
|
||||
> trusted group can request a code.
|
||||
|
||||
---
|
||||
|
||||
@@ -93,7 +95,7 @@ The two POST routes mirror the server's `ISmsSender` (`SendOtpAsync` / `SendAsyn
|
||||
| `API_KEY` | — | **Required**, min 16 chars. Shared secret expected in `X-Api-Key`. Process exits without it. |
|
||||
| `TELEGRAM_CHAT_IDS` | — | Comma-separated recipient chat ids. Empty ⇒ every send returns `503`. |
|
||||
| `PORT` | `5010` | HTTP port. |
|
||||
| `HOST` | `127.0.0.1` | Bind address. Keep it loopback unless the API runs on another machine. |
|
||||
| `HOST` | `127.0.0.1` | Bind address. Loopback locally; the Dockerfile sets `0.0.0.0` so the API container can reach it. |
|
||||
| `REDACT_CODE_IN_LOGS` | `false` | Keep the code out of *this process's* stdout (still delivered). |
|
||||
| `TELEGRAM_PROXY_URL` | — | Optional outbound proxy for the Telegram hop — see below. |
|
||||
|
||||
@@ -101,7 +103,16 @@ The API key is the only access control — there is no IP allow-list and no TLS.
|
||||
loopback when the API runs on the same machine; if you must expose it, put it behind something that
|
||||
terminates TLS, or the key travels in clear text.
|
||||
|
||||
Values come from `.env` (git-ignored) or from real environment variables, which take precedence.
|
||||
Values come from `.env` (git-ignored) or from real environment variables, which take precedence. In the
|
||||
deployed stack there is no `.env` at all — the root `docker-compose.yml` supplies every variable directly
|
||||
(and `.dockerignore` keeps a local `.env` out of the image, so it can't silently win).
|
||||
|
||||
## Running in Docker
|
||||
|
||||
The [`Dockerfile`](Dockerfile) here is built by the root [`docker-compose.yml`](../docker-compose.yml) as
|
||||
the `otp-relay` service. Nothing is published to the host: the API reaches it as
|
||||
`http://balinyaar-otp-relay:5010` over the shared `caddy_net` network, and its own hop to Telegram goes
|
||||
through the proxy container on that same network. See [DEPLOY.md](../DEPLOY.md).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
Reference in New Issue
Block a user