Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/docs/ARCHITECTURE.md
blob: ef5a427fb21a87042dc67e53da2e96f2fa9772bb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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).