diff options
| author | Leitwolf11 | 2026-10-03 18:18:50 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-03 18:18:50 +0300 |
| commit | ba237e5ce9f50cd9e586697417c1d636e3cd129c (patch) | |
| tree | 7e67ce6c748af16b6c4ca03a51a9502fac5009f7 | |
| parent | e9eb2812a47b075c82a138db7f43cc08cccf072a (diff) | |
| download | progmaweb-ba237e5ce9f50cd9e586697417c1d636e3cd129c.tar.gz | |
Document the architecture, the account API, localization and limitationsdocs/api-and-web-reference
The repository had a README and two operations notes. This adds the
documents a contributor or operator needs and corrects what had gone stale.
New, under docs/:
- README.md: index of the documentation.
- ARCHITECTURE.md: the two processes, the three hosts, the host-aware
routing rules, the account flow and the trust boundaries.
- ACCOUNT-API.md: every endpoint with its request, responses and errors,
the field rules, the Google flow outcome by outcome, cookies, limits and
configuration. The responses were checked against a locally running API.
- LOCALIZATION.md: the four languages, how a page picks one, the cookie,
how Hebrew text is laid out, and how to add a language.
- DEVELOPMENT.md: requirements, local setup, checks, tests, what each CI
workflow enforces, the audit exception and the DocFX project.
- LIMITATIONS.md: what is missing or temporary, with the consequence of
each: the in-memory account store, unverified addresses, no mail, no
password change, the ignored returnTo parameter, the legacy ViGet status
route, and that the API answers 500 to everything while the Google
settings are empty.
Updated:
- README.md: languages, a documentation section, the local API command
with its port, and the DocFX check.
- ops/README.md: restarting the API deletes every account, how to release
a frontend-only change without that restart, and lessons from past
releases.
- ops/mail/README.md: a status note that the API does not send mail yet.
- The pull request template names all four languages.
No source file changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | .github/pull_request_template.md | 3 | ||||
| -rw-r--r-- | README.md | 26 | ||||
| -rw-r--r-- | docs/ACCOUNT-API.md | 151 | ||||
| -rw-r--r-- | docs/ARCHITECTURE.md | 115 | ||||
| -rw-r--r-- | docs/DEVELOPMENT.md | 136 | ||||
| -rw-r--r-- | docs/LIMITATIONS.md | 85 | ||||
| -rw-r--r-- | docs/LOCALIZATION.md | 106 | ||||
| -rw-r--r-- | docs/README.md | 20 | ||||
| -rw-r--r-- | ops/README.md | 25 | ||||
| -rw-r--r-- | ops/mail/README.md | 4 |
10 files changed, 667 insertions, 4 deletions
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f4d6812..3dd25c4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,7 +12,8 @@ is affected. - [ ] Next.js check, frontend tests, lint, and build were run where relevant. - [ ] ASP.NET Core build and API tests were run where relevant. -- [ ] en-US/de-DE, light/dark, accessibility, and small-screen behavior were considered for UI changes. +- [ ] All four languages (en-US, de-DE, ru-RU, he-IL with right-to-left text), light/dark, accessibility, and + small-screen behavior were considered for UI changes. - [ ] Auth, session, publisher identity, registry paths, and deployment compatibility were reviewed if affected. - [ ] No credentials, generated output, or unrelated files are included. @@ -23,6 +23,23 @@ duplicates. Email uniqueness is also evaluated case-insensitively by the API. ViGet does not maintain a separate publisher name. A package coordinate's `<Publisher>` segment is exactly the canonical Progmasoft `<Account>` name, with the same spelling and case. +## Languages + +Every page is published in English, German, Russian and Hebrew. A visitor who has not chosen a language gets the +language their browser prefers, and English when the browser prefers none of the four. A choice made in the language +menu is remembered across the three hosts. Hebrew text runs right to left while the page layout stays as it is. See +[Localization](docs/LOCALIZATION.md). + +## Documentation + +- [Architecture](docs/ARCHITECTURE.md): processes, hosts, routing and trust boundaries. +- [Account API](docs/ACCOUNT-API.md): endpoints, errors, cookies and limits. +- [Localization](docs/LOCALIZATION.md): language selection, text direction and adding a language. +- [Development](docs/DEVELOPMENT.md): local setup, checks and what CI enforces. +- [Known limitations](docs/LIMITATIONS.md): what is missing or temporary. Read this before operating the service; + accounts are currently kept in memory and do not survive a restart of the API. +- [Operations](ops/README.md) and the [mail boundary](ops/mail/README.md). + ## Architecture - `apps/web` is a Next.js App Router application. Host-aware routing keeps the public organization, account, and ViGet @@ -54,10 +71,10 @@ pnpm dev Start the account API in another terminal: ```text -dotnet run --project apps/api/Progmaweb.Api.csproj +dotnet run --project apps/api/Progmaweb.Api.csproj --urls http://127.0.0.1:5085 ``` -The frontend defaults to `http://localhost:5085` for server-side API requests. Set `PROGMAWEB_API_ORIGIN` when the API +The frontend defaults to `http://127.0.0.1:5085` for server-side API requests. Set `PROGMAWEB_API_ORIGIN` when the API uses another origin. Browser requests use the same-origin `/api` boundary so production can proxy them without exposing an internal address. @@ -77,13 +94,16 @@ pnpm build dotnet build apps/api/Progmaweb.Api.csproj --configuration Release dotnet test --project apps/api-tests/Progmaweb.Api.Tests.csproj --configuration Release dotnet test --project apps/api-tests/Progmaweb.Api.Tests.csproj --configuration Release --coverlet --coverlet-output-format cobertura +dotnet tool restore +dotnet docfx docs/api/docfx.json --warningsAsErrors ``` Frontend tests exercise the published locale contract and account/ViGet metadata without starting a production service. The frontend LCOV and API Cobertura reports are retained as one-day artifacts, then uploaded to Codecov by separate jobs. Only those upload jobs receive `id-token: write`; dependency installation, builds, and tests do not receive OIDC permissions, and fork-originated pull requests skip the upload jobs. CodeQL and Codacy provide static analysis; -Dependabot proposes dependency updates without merging them automatically. +Dependabot proposes dependency updates without merging them automatically. The DocFX command builds the API reference +and fails on a broken link or an unresolved reference. [Development](docs/DEVELOPMENT.md) explains each check. ## License diff --git a/docs/ACCOUNT-API.md b/docs/ACCOUNT-API.md new file mode 100644 index 0000000..022c958 --- /dev/null +++ b/docs/ACCOUNT-API.md @@ -0,0 +1,151 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 --> + +# Account API + +The account service is the ASP.NET Core application in `apps/api`. Browsers reach it through the same origin as the +page, under `/api/`; the Next.js server forwards those requests to the service on loopback port 5085. + +All bodies are JSON with camel-case property names. Errors use the problem-details format (`application/problem+json`). +Every response carries `Cache-Control: no-store`. + +## Summary + +| Method and path | Purpose | Rate limited | +| --- | --- | --- | +| `POST /api/v1/accounts/register` | create a password account and sign it in | yes | +| `POST /api/v1/accounts/login` | sign in with email address and password | yes | +| `POST /api/v1/accounts/logout` | end the current session | no | +| `GET /api/v1/accounts/me` | return the signed-in account | no | +| `GET /api/v1/accounts/oauth/google/start` | begin sign-in with Google | yes | +| `GET /api/v1/accounts/oauth/google/complete` | finish sign-in with Google | yes | +| `GET /api/v1/status` | report that the service is online | no | +| `GET /health` | liveness probe for the service manager | no | + +## Field rules + +| Field | Rule | +| --- | --- | +| `accountName` | 8 to 128 ASCII letters or digits, beginning with an upper-case letter. Unique without regard to case; the spelling the owner chose is kept. `Progmasoft` and `Leitwolf` are reserved in every case. | +| `email` | a single mailbox without a display name, at most 254 characters. Unique without regard to case. Ownership is not verified. | +| `password` | 12 to 256 characters, without a null character. No composition rule; long passphrases are welcome. | + +Surrounding white space of an account name or email address is ignored. + +## Register + +`POST /api/v1/accounts/register` + +```json +{ "accountName": "ExampleUser1", "email": "user@example.com", "password": "a long passphrase" } +``` + +**201 Created.** The session cookie is set, `Location` names the account, and the body is the account: + +```json +{ "accountName": "ExampleUser1", "email": "user@example.com", "createdAt": "2026-10-03T12:00:00+00:00" } +``` + +**400 Bad Request.** A validation problem whose `errors` object names each refused field (`accountName`, `email`, +`password`) with a message. A name or address that is already taken is reported the same way, on its field. + +## Sign in + +`POST /api/v1/accounts/login` + +```json +{ "email": "user@example.com", "password": "a long passphrase" } +``` + +**200 OK.** The session cookie is set and the body is the account, as above. + +**401 Unauthorized.** One message for every failure. The response does not say whether the address or the password +was wrong, and an unknown address costs the same password verification as a wrong password, so timing does not reveal +which addresses are registered. + +An account that was created through Google has no password and cannot sign in here. + +## Sign out + +`POST /api/v1/accounts/logout` + +**204 No Content.** The session is revoked and the cookie deleted. The response is the same when no session existed. + +## Current account + +`GET /api/v1/accounts/me` + +**200 OK** with the account, or **401 Unauthorized** when the request has no valid session. An expired session is +revoked when it is seen. + +## Sign-in with Google + +The flow is a server-side authorization-code flow. The browser is redirected; it never receives Google tokens. + +1. `GET /api/v1/accounts/oauth/google/start` sends the browser to Google. When a new account should be created, the + request carries `?accountName=<name>`. An invalid name redirects back to + `/register?google=invalid-account-name` without contacting Google. +2. Google returns to `/api/v1/accounts/oauth/google/callback`, which the authentication middleware handles. The + identity is kept for at most ten minutes in a separate cookie, `__Host-ProgmasoftExternal`. +3. `GET /api/v1/accounts/oauth/google/complete` decides the outcome and removes that cookie. + +| Situation | Result | +| --- | --- | +| the Google identity is already linked to an account | signed in; redirect to `/<Account>/dashboard` | +| the identity is new and a valid, free account name was supplied | account created and signed in | +| the identity is new and no account name was supplied | redirect to `/register?google=account-name-required` | +| the supplied account name is invalid | redirect to `/register?google=invalid-account-name` | +| the supplied account name is taken | redirect to `/register?google=account-name-unavailable` | +| the email address of the identity belongs to another account | redirect to `/login?google=failed` | +| Google reported no usable identity, or the callback failed | redirect to `/login?google=failed` | + +A Google identity is never linked to an existing account by email address. Password registrations do not verify the +address, so linking by address could give one person's account to another. + +The service needs a Google client identifier and secret to run at all; see [Known limitations](LIMITATIONS.md). + +## Status and health + +`GET /api/v1/status` returns `{ "service": "progmaweb-account", "status": "online", "version": "1.0.0" }`. +`GET /health` is the ASP.NET Core health check for the service manager and is not a public contract. + +## Sessions and cookies + +| Cookie | Purpose | Attributes | +| --- | --- | --- | +| `__Host-ProgmasoftSession` | the account session | `HttpOnly`, `Secure`, `SameSite=Lax`, `Path=/`, expires with the session | +| `__Host-ProgmasoftExternal` | carries the Google identity between callback and completion | `HttpOnly`, `Secure`, `SameSite=Lax`, ten minutes | + +A session token is 256 random bits. The service stores only its SHA-256 digest, so a copy of the store does not +contain a usable token. A session lasts 12 hours by default; `AccountSessions:LifetimeHours` changes that, within 1 to +168 hours. + +Passwords are hashed with the ASP.NET Core Identity password hasher and are never stored or logged in plain text. + +## Limits + +- **Rate limit.** The endpoints marked above share one limit per client address: 12 requests in a sliding window of + three minutes. A request over the limit gets **429 Too Many Requests**. +- **Body size.** A request body larger than 16 KiB gets **413 Content Too Large**. +- **Hosts.** The service answers only the host names listed in `AllowedHosts`; any other host gets **400**. + +The client address comes from `X-Forwarded-For`, which the service accepts from loopback proxies only. + +## Configuration + +| Setting | Meaning | Default | +| --- | --- | --- | +| `AccountSessions:LifetimeHours` | hours a session stays valid, clamped to 1–168 | `12` | +| `AccountSessions:CookieName` | name of the session cookie | `__Host-ProgmasoftSession` | +| `Authentication:Google:ClientId` | Google OAuth client identifier | none; required | +| `Authentication:Google:ClientSecret` | Google OAuth client secret | none; required | +| `AllowedHosts` | host names the service answers | see `appsettings.json` | +| `ASPNETCORE_URLS` | listener address | `http://127.0.0.1:5085` in production | + +Environment variables use a double underscore for the colon, for example `Authentication__Google__ClientId`. Secrets +belong in the process environment or a secret manager and are never committed. + +## What the API does not do + +It does not verify email addresses, send mail, reset passwords, list accounts or delete accounts. See +[Known limitations](LIMITATIONS.md). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..ef5a427 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,115 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 --> + +# Architecture + +Progmaweb is two processes behind one reverse proxy: + +- **`apps/web`**, a Next.js App Router application. It renders every page of three public hosts and forwards API + calls. +- **`apps/api`**, an ASP.NET Core minimal API. It owns accounts, passwords and sessions. + +Both listen on loopback only. Nginx is the only public listener and terminates TLS. + +```text +browser + | HTTPS + v +Nginx (progmasoft.com, www, account.progmasoft.com, viget.progmasoft.com) + | http://127.0.0.1:3010 + v +Next.js server (apps/web) + | /api/* -> http://127.0.0.1:5085 (rewrite, server side) + v +ASP.NET Core account API (apps/api) +``` + +## Hosts + +One build serves three hosts. The host of a request, not a separate deployment, decides which pages it sees. + +| Host | Serves | +| --- | --- | +| `progmasoft.com` | the organization homepage; the only host that is meant to be indexed as the organization | +| `www.progmasoft.com` | a permanent redirect to the apex host, issued by Nginx | +| `account.progmasoft.com` | the account landing page, sign-in, registration, recovery and the dashboard | +| `viget.progmasoft.com` | the ViGet package catalog and the Kotlin DSL plugin catalog | + +`*.localhost` names (`account.localhost`, `viget.localhost`) behave like the production hosts during development. + +## Host-aware routing + +`apps/web/proxy.ts` runs before every page request and applies these rules in order: + +1. **Language parameter.** `?lang=<locale>` with a published locale stores the language cookie and redirects to the + same URL without the parameter. See [Localization](LOCALIZATION.md). +2. **Account root.** `/` on the account host is rewritten to the internal `/account` page, so the account landing page + has the short URL. +3. **ViGet.** On the ViGet host `/` is rewritten to `/viget` and `/dslplugins` to `/viget/dslplugins`. `/login`, + `/register` and `/recover` redirect to the account host. +4. **Internal trees stay internal.** `/viget/...` on any other host redirects to the ViGet host, and `/account` on any + other host redirects to the account host. + +Internal rewrites use `http:` on purpose. TLS ends at Nginx, and Next.js would otherwise try TLS against its own +plain-HTTP loopback listener. + +The host is read from `X-Forwarded-Host` first and `Host` second, without the port and in lower case. + +## Pages + +| Route | Page | Indexed | +| --- | --- | --- | +| `/` (apex) | organization homepage | yes | +| `/account` (shown as `/` on the account host) | account landing page | no | +| `/login`, `/register`, `/recover` | authentication and recovery | no | +| `/<Account>/dashboard` | dashboard of the signed-in account | no | +| `/viget` (shown as `/` on the ViGet host) | package catalog | yes | +| `/viget/dslplugins` (shown as `/dslplugins`) | DSL plugin catalog | yes | + +`robots.txt` and `sitemap.xml` are generated per host: the ViGet host lists its two catalog pages, every other host +lists the apex homepage only. Nginx additionally sends `X-Robots-Tag: noindex` for the whole account host. + +Pages are rendered on the server for every request, because the language depends on the request. Two components run +in the browser: the authentication form and the dashboard. + +## Account flow + +1. The registration or sign-in form posts JSON to `/api/v1/accounts/register` or `/login` on the same origin. +2. Next.js rewrites `/api/*` to the account API on loopback. The browser never learns the internal address. +3. The API validates the input, stores or verifies the account, opens a session and sets the session cookie. +4. The browser is sent to `/<Account>/dashboard`. The dashboard calls `/api/v1/accounts/me`; without a valid session + it returns to `/login`, and with a session of another account it moves to that account's dashboard. + +Sign-in with Google is a server-side authorization-code flow that starts at `/api/v1/accounts/oauth/google/start` and +returns through the API; the browser never handles Google tokens. The [Account API](ACCOUNT-API.md) describes each +step. + +## Trust boundaries + +- **Browser to Nginx.** Everything from the browser is untrusted, including the language cookie, the `lang` + parameter and the `Accept-Language` header. Each is validated against the list of published locales. +- **Nginx and Next.js to the API.** The API trusts forwarded headers only from loopback and reads one proxy hop. A + caller that reached the API directly could otherwise forge the client address that the rate limiter uses. +- **API to its store.** The store keeps password hashes and session digests, never passwords or session tokens. +- **What the browser never receives.** Password hashes, session digests, deployment secrets and internal addresses. + +The API has no bulk-delete, reset-all or seed-password endpoint. Maintenance of production accounts is an operations +task, not a public route. + +## Response headers + +Nginx adds HSTS, `X-Content-Type-Options`, `X-Frame-Options: DENY`, a referrer policy and a permissions policy on +every host. Next.js sets the same content headers for the responses it produces. The API sets its own, stricter set: +a content security policy that allows nothing, `Cache-Control: no-store`, and no referrer. + +## Shared account identity + +An account name is the public identity of its owner on every Progmasoft service. ViGet does not keep a separate +publisher name: the `<Publisher>` segment of a package coordinate is exactly the canonical account name, with the same +spelling and case. + +## ViGet status route + +`viget.progmasoft.com/api/v1/status` is proxied by Nginx to a separate legacy process on loopback port 5080. It is not +part of this repository's API. Every other `/api/` path on the ViGet host is refused. See +[Known limitations](LIMITATIONS.md). diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..66822de --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,136 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 --> + +# Development + +## Requirements + +| Tool | Version | Used for | +| --- | --- | --- | +| Node.js | 24 or newer | the Next.js application and its tests | +| pnpm | 11 or newer; the repository pins 11.24.0 | the JavaScript workspace | +| .NET SDK | 10 | the account API, its tests and DocFX | + +`just` is optional. The `justfile` only names the commands below; it adds no logic of its own. + +## Repository layout + +```text +apps/web Next.js application: pages, components, localization, host routing +apps/api ASP.NET Core account API +apps/api-tests xUnit tests of the API, run against the application in memory +docs this documentation and the DocFX project in docs/api +ops Nginx, systemd and mail configuration with their runbooks +``` + +## Run it locally + +Install the JavaScript dependencies once, then start the frontend: + +```text +pnpm install --frozen-lockfile +pnpm dev +``` + +Start the account API in a second terminal on the port the frontend expects. The two Google settings must not be +empty, or the API answers every request with 500; placeholder values are enough for everything except sign-in with +Google itself. + +```text +Authentication__Google__ClientId=local-dev.apps.googleusercontent.com +Authentication__Google__ClientSecret=local-dev-placeholder +dotnet run --project apps/api/Progmaweb.Api.csproj --urls http://127.0.0.1:5085 +``` + +Set the two variables in the way your shell sets environment variables before running the last line. + +The frontend forwards `/api/*` to `PROGMAWEB_API_ORIGIN`, which defaults to `http://127.0.0.1:5085`. Set it when the +API listens elsewhere; `.env.example` shows the variable. + +Open the three surfaces through their development host names: + +| URL | Surface | +| --- | --- | +| `http://localhost:3000/` | organization homepage | +| `http://account.localhost:3000/` | account pages | +| `http://viget.localhost:3000/` | ViGet catalogs | + +Accounts created locally live in the memory of the API process and are gone when it stops. + +Real sign-in with Google needs the identifier and secret of a Google OAuth client in those two variables. Never commit +the downloaded Google client file. + +## Checks + +Run these before opening a pull request. They are the same commands CI runs. + +```text +pnpm check # route types and TypeScript +pnpm test # Node tests of localization and metadata +pnpm lint # ESLint, no warnings allowed +pnpm build # production build +pnpm format # Prettier (writes) + +dotnet build Progmaweb.slnx --configuration Release +dotnet test --project apps/api-tests/Progmaweb.Api.Tests.csproj --configuration Release +``` + +`just verify` runs the type check, the production build, the API build and the API tests in one go. + +The .NET projects treat every warning as an error and restore in locked mode in CI. After changing a NuGet reference, +restore without `--locked-mode` once and commit the updated `packages.lock.json`. + +A user-interface change is not verified by these commands alone. Look at it in a browser: all four languages, both +themes, a narrow window, and keyboard focus. + +## Tests + +- **Frontend** (`apps/web/tests`): the list of published locales, the shape of every message tree, language + negotiation, cookie attributes, text direction and page metadata. The tests import the TypeScript sources directly + and do not start a server. +- **API** (`apps/api-tests`): the endpoints through an in-memory application, including validation, duplicate names + and addresses, session cookies, the Google flow and security headers; and the account service on its own. The rate + limit has no automated test. + +## Continuous integration + +| Workflow | Runs on | Enforces | +| --- | --- | --- | +| Frontend | changes under `apps/web` and the workspace files | type check, tests with coverage, lint, build | +| Backend | changes under `apps/api`, `apps/api-tests`, `docs/api` and the tool manifest | locked restore, build, tests with coverage, the DocFX gate | +| Security | every push and pull request, and weekly | dependency review of the pull request, `pnpm audit` at high severity, NuGet vulnerability audit | +| CodeQL | pushes, pull requests, and weekly | static analysis of the workflows, TypeScript and C# | + +Coverage is uploaded to Codecov by separate jobs. Only those jobs receive an OIDC token; installation, build and test +jobs do not, and pull requests from forks skip the upload. Dependabot proposes weekly updates for GitHub Actions, npm +and NuGet and never merges them itself. + +`main` is protected: changes arrive through pull requests with green checks. + +### The dependency audit exception + +`pnpm-workspace.yaml` tells the audit to ignore one advisory, `GHSA-vfj7-8cjw-p6xm` (`braces` 3.0.3 and older). No +fixed release exists, and the package is reached only through the lint toolchain; it is not part of the built or +deployed application. Every other advisory of high severity still fails the check. Remove the entry as soon as a +release without the advisory is available, and do not add another entry without the same analysis written next to it. + +## API reference + +`docs/api` is a DocFX project that builds a reference from the C# sources. `.config/dotnet-tools.json` pins the DocFX +version, so a local run and CI use the same tool. + +```text +dotnet tool restore +dotnet docfx docs/api/docfx.json --warningsAsErrors +``` + +or `just api-docs`. The output goes to `docs/api/_site`, which is ignored by Git. A broken link or an unresolved +reference fails the build. The gate does not yet require documentation comments on the C# declarations; see +[Known limitations](LIMITATIONS.md). + +## Conventions + +- Public documents, comments, commit messages and pull request text are written in American English. +- Every file carries the SPDX header of the repository; `REUSE.toml` covers the formats that cannot hold a comment. +- A user-visible string is added to every locale in the same change. See [Localization](LOCALIZATION.md). +- Secrets, generated output and downloaded credentials are never committed. diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md new file mode 100644 index 0000000..107c48f --- /dev/null +++ b/docs/LIMITATIONS.md @@ -0,0 +1,85 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 --> + +# Known limitations + +This page lists what Progmaweb does not do yet, so that nobody has to discover it in production. Each entry says what +is missing, what follows from it, and what would close it. + +## Accounts are kept in memory + +The account API registers `InMemoryAccountStore` as its only store. Accounts and sessions live in the memory of the +API process. + +- **Consequence.** Every restart of the API service deletes all accounts and signs everyone out. A deployment that + restarts the API has this effect. +- **In operations.** Do not restart the API unless its code or configuration changed. A release that changes only the + frontend should restart only the frontend service; see [Operations](../ops/README.md). +- **What closes it.** A durable implementation of `IAccountStore`. The interface already states the uniqueness and + session rules such a store must keep. + +## The API does not run without Google settings + +When `Authentication:Google:ClientId` or `Authentication:Google:ClientSecret` is empty, the API answers every request +with 500, including registration, sign-in with a password and the status route. The validation of the Google options +fails inside the authentication middleware, before any endpoint runs. + +- **Consequence.** The API cannot be started for local work without setting both values, and the "Google sign-in is + unavailable" answer of the start endpoint is never reached in that state. +- **Workaround.** Set both values. Placeholders are enough when Google sign-in itself is not being tested; see + [Development](DEVELOPMENT.md). +- **What closes it.** Registering the Google scheme only when it is configured. + +## Email addresses are not verified + +Registration accepts any well-formed address. Nothing proves that the person who registered owns it. + +- **Consequence.** An address can be registered by someone other than its owner. For that reason a Google identity is + never linked to an existing account by address. +- **What closes it.** A verification message and a confirmed state on the account. + +## The API sends no mail + +The mail host described in the [mail boundary](../ops/mail/README.md) is prepared for verification and recovery +messages, but the API contains no code that sends mail. + +- **Consequence.** There is no automated password recovery. The recovery page asks the user to write to support from + the address registered to the account. + +## Passwords cannot be changed + +The dashboard shows a "Change password" button that does nothing yet, and the API has no endpoint to change a +password. + +## The sign-in page ignores `returnTo` + +The dashboard sends a signed-out visitor to `/login?returnTo=...`. The sign-in form does not read the parameter and +always continues to the dashboard of the account that signed in. + +## ViGet has no packages and no registry API here + +The catalog pages are real and intentionally empty: no package or plugin has been published. Publishing is closed. + +`viget.progmasoft.com/api/v1/status` is answered by a separate legacy process, not by this repository's API. Until +that route moves here, the legacy process has to keep running. + +## The rate limit is per process + +The authentication rate limit is counted in the memory of one API process, by client address. It is reset by a +restart and would not be shared between several instances. + +## Documentation comments are not required + +The DocFX gate fails on a broken link or an unresolved reference. It does not report a declaration without a +documentation comment, and most C# declarations have none, so the generated reference lists members without +descriptions. + +## The Hebrew text has not been reviewed by a native speaker + +The Hebrew translation was written and revised without a native reader. It may contain wording a native speaker would +not choose. + +## One audit advisory is ignored + +The dependency audit ignores a single advisory that has no fixed release and affects only the lint toolchain. The +entry and its removal condition are described in [Development](DEVELOPMENT.md). diff --git a/docs/LOCALIZATION.md b/docs/LOCALIZATION.md new file mode 100644 index 0000000..7ba409d --- /dev/null +++ b/docs/LOCALIZATION.md @@ -0,0 +1,106 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 --> + +# Localization + +Every page of Progmaweb is published in four languages. All of them live in `apps/web/lib/localization.ts`. + +| Locale | Language | Text direction | +| --- | --- | --- | +| `en-US` | English | left to right | +| `de-DE` | German | left to right | +| `ru-RU` | Russian | left to right | +| `he-IL` | Hebrew | right to left | + +A page has one URL in every language. There are no language-specific paths and no `hreflang` alternatives, because a +language is a preference of the visitor, not a second address of the page. + +## How a page picks its language + +For each request the server decides in this order (`apps/web/lib/locale.server.ts`): + +1. **The visitor's choice.** A valid `progmasoft_locale` cookie always wins. +2. **The browser.** Without that cookie the `Accept-Language` header is negotiated: the published language with the + highest quality wins, and the order of the header breaks ties. Only the primary language is compared, so `de-AT` + selects German. `iw`, the former code for Hebrew, is accepted. +3. **The fallback.** A browser that prefers no published language gets `en-US`. + +Examples of the negotiation: + +| `Accept-Language` | Page language | +| --- | --- | +| `he-IL,he;q=0.9,en;q=0.8` | Hebrew | +| `de-AT,de;q=0.9` | German | +| `tr-TR,tr;q=0.9,de;q=0.5` | German, the first preference that is published | +| `tr-TR,tr;q=0.9` | English, the fallback | +| `en;q=0.3,ru;q=0.9` | Russian; quality decides, not position | +| absent | English | + +The header comes from the browser and is not trusted: only its first 1024 characters are read, and a malformed or +zero quality never selects a language. + +## How a visitor changes the language + +The language menu in the header calls the server action `selectLocale` (`apps/web/lib/locale.actions.ts`). The action +stores the cookie, and Next.js renders the current route again and applies the result in place. Nothing is reloaded: +scroll position, theme and form input stay as they are. + +A link can carry the same choice as `?lang=<locale>`. The proxy stores the cookie and redirects to the URL without the +parameter, so a preference never becomes a second indexed address. The menu falls back to this link when the action +cannot reach the server. + +A value that is not a published locale is ignored in both paths. + +## The language cookie + +| Attribute | Value | +| --- | --- | +| Name | `progmasoft_locale` | +| Domain | `.progmasoft.com` on production hosts, so the choice follows the visitor to the account and ViGet hosts; host-only elsewhere | +| Lifetime | one year | +| Flags | `HttpOnly`, `SameSite=Lax`, and `Secure` over TLS | + +Both paths write the cookie through `localeCookieOptions`. Two writers with different attributes would leave two +cookies behind, one shadowing the other. + +## Hebrew and text direction + +Hebrew does not mirror the page. The layout, the navigation and code keep their left-to-right arrangement; only text +follows the language. + +- The root element carries `data-text-direction="rtl"`, not `dir="rtl"`. +- Blocks of prose (headings and paragraphs) run right to left and align to the right. +- Short pieces such as links, buttons and list items take the direction of their own first letter, so a product name + on its own still reads left to right. +- Code and form inputs stay left to right. +- The onward arrow comes from `forwardArrow(locale)` and points left in Hebrew. +- A left-to-right mark (``) follows `Visual X#` in Hebrew strings. Without it the `#` would jump to the other + side of the name inside right-to-left text. + +The rules are at the end of `apps/web/app/globals.css`. + +## Message tree + +`messages` holds one tree per locale with identical shape. Pages and components read it through +`getMessages(locale)`; there is no lookup by string key and no fallback from one language to another, so a missing +message is a type error or a failing test rather than an English sentence on a German page. + +The tests in `apps/web/tests/localization.test.mjs` enforce that + +- every locale has exactly the keys and array lengths of `en-US`, with no empty string; +- every locale has a name in its own language for the menu; +- negotiation, cookie attributes and text direction behave as described above. + +## Adding a language + +1. Add the locale to `supportedLocales`, its own name to `localeNames`, and its primary language to + `localeByLanguage`. +2. If the language is written right to left, extend `textDirection`. +3. Add a complete message tree. Copy the `en-US` tree and translate every string; keep product names and technical + identifiers such as `Visual.XSharp.kts` unchanged. +4. Extend the tests: the list of published locales and a negotiation case for the new language. +5. Run `pnpm check`, `pnpm test`, `pnpm lint` and `pnpm build`, then read every page in the new language in a + browser, in both themes and at |