---
name: login-config
description: >
  Customize the login / authentication experience of a generated SmartStack
  client app, through two complementary levers. (1) configure-login WRITES
  CONFIGURATION the package reads: connection providers (Local password,
  Microsoft / Google OAuth, Entra ID SSO), the initial admin, and the email
  provider — into secret-safe appsettings. (2) scaffold-login-page SCAFFOLDS an
  editable login page OVERRIDE via the package's `extensions.pages[PAGE_KEYS.LOGIN]`
  seam (branding + UI-side masking of providers and the sign-up link). Neither
  edits the package; the default login UI ships in @atlashub/smartstack.
  Conversational, then invokes the colocated CLIs.
argument-hint: "[describe the login you want, e.g. 'local + Microsoft, no signup, branded Acme']"
allowed-tools: Read, Grep, Glob, Bash
---

# login-config — Customize the login / auth experience

The default login page, the `/api/auth/*` endpoints and the registration flow
live in the **SmartStack package** the client consumes (`@atlashub/smartstack`
npm + `SmartStack.*` NuGet). Customizing `/login` therefore means two things,
and this skill ships a CLI for each:

| Lever | CLI | What it does |
|------|-----|--------------|
| **Configure** | `configure-login` | Writes backend `appsettings` (providers, initial admin, email) — secret-safe. |
| **Customize the page** | `scaffold-login-page` | Emits an editable login page override (`extensions.pages[PAGE_KEYS.LOGIN]`) — branding + which providers / sign-up link are shown. |

Use them together: `configure-login` enables a provider **server-side**;
`scaffold-login-page` decides whether its button is **shown** and how the page
**looks**.

## Ground truth (verified against SmartStack.app, package 3.55.0)

- The package ships `LoginPage` / `RegisterPage`; the default login **always**
  shows the Microsoft + Google buttons and the "create account" link, and shows
  the Entra button only when `VITE_MSAL_CLIENT_ID` is set at build time.
- There is **no** `GET /api/auth/config` endpoint, and `SmartStackConfig` does
  **not** accept an `auth` key — so dynamic, config-driven masking from the
  backend is NOT available. The way to mask providers / hide sign-up today is to
  **scaffold an override** (you own the JSX).
- There is **no** self-registration flag. `POST /api/auth/register` is always
  open, limited only by the license seat quota. Nothing in config closes it.
- The override seam IS real and public: `extensions.pages[PAGE_KEYS.LOGIN]`
  replaces `/login`; `useLoginForm()` (the real login flow) and `PageProps` are
  exported by `@atlashub/smartstack >= 3.55.0`.

## What takes effect when (be honest with the user)

| Capability | How |
|---|---|
| Enable MS / Google / Entra **backend** | ✅ `configure-login` → appsettings (buttons already call these) |
| Initial admin + email (confirmation / reset) | ✅ `configure-login` → appsettings (provider block required, see below) |
| **Brand** the login, **hide** non-chosen provider buttons, **hide** "create account" | ✅ `scaffold-login-page` (you render only what you ask for) |
| **Disable** account creation server-side | ❌ Not supported — the backend has no flag; restrict at the network layer if truly required |
| Frontend `config.auth` injection (`frontendAuth:true`) | ❌ `SmartStackConfig` has no `auth` key yet — keep OFF (injecting it breaks the client TS build) |

---

## CLI 1 — configure-login (backend configuration)

Writes (deep-merged, idempotent) the configuration the framework reads:

```bash
npx --prefer-offline tsx skills/login-config/cli/configure-login/index.ts \
  --spec '{
    "projectPath": "/abs/project",
    "appCode": "demo",
    "connectionTypes": {
      "local": true,
      "microsoft": { "clientId": "…", "clientSecret": "…" },
      "entra": { "clientId": "…", "allowedTenantIds": ["…"] }
    },
    "allowRegistration": false,
    "branding": { "appName": "Acme Portal" },
    "initialAdmin": { "email": "admin@acme.io", "password": "…", "requirePasswordChange": true },
    "email": {
      "provider": "Smtp",
      "fromEmail": "no-reply@acme.io",
      "fromName": "Acme",
      "smtp": { "host": "smtp.office365.com", "port": 587, "username": "no-reply@acme.io", "password": "…", "useSsl": true }
    },
    "secretsMode": "local-file",
    "frontendAuth": false
  }'
```

Add `--dry_run` to preview target files without writing.

### Spec fields

| Field | Required | Notes |
|---|---|---|
| `projectPath`, `appCode` | ✅ | generated app root + app code |
| `connectionTypes` | ✅ | `{ local, microsoft?, google?, entra? }`; ≥ 1 enabled. Written as `Authentication:Microsoft/Google` + `Authentication:EntraSso` (the Entra tenant whitelist is written to `Authentication:Microsoft:AllowedTenants`, shared with social Microsoft login). |
| `allowRegistration` | ✅ | **UI signal only** — NOT written to appsettings (no backend flag exists). Used by `scaffold-login-page` + the gated front literal. `false` emits a warning. |
| `branding` | — | `appName`, `logoUrl`, `heroTitle`, `heroSubtitle` (front only; takes effect via `scaffold-login-page` or once the package reads it) |
| `initialAdmin` | — | `{ email, password, requirePasswordChange? }`. **Strongly recommended:** the API fail-fasts on first boot until `Security:InitialAdmin:Password` is set. |
| `email` | — | `{ enabled?, provider, fromEmail, fromName, smtp?, sendGrid?, azureAcs? }`. `provider` ∈ `Development\|Smtp\|SendGrid\|AzureAcs`. **The matching provider block is required** (`smtp.host` / `sendGrid.apiKey` / `azureAcs.connectionString`) or no mail is sent. |
| `secretsMode` | — | `local-file` (default) \| `placeholders`. Secrets (OAuth secrets, admin password, SMTP password, SendGrid key, ACS connection string) → gitignored `appsettings.Local.json`. |
| `frontendAuth` | — | default **false** — keep OFF (see ground truth). |
| `writeEnvExample` | — | default false; append `# VITE_MSAL_*` (Entra only). |
| `force` | — | rewrite files even when unchanged |

### Conversational flow

1. **Locate the project** — confirm `projectPath` + `appCode`; the CLI auto-detects the API folder + `web/{appCode}-web`.
2. **Connection types** — Local / Microsoft / Google / Entra (≥ 1). MS/Google need `clientId` + `clientSecret`; Entra needs `clientId` (+ optional `authority`, `allowedTenantIds`).
3. **Initial admin** — collect `initialAdmin` (recommend it; warn about the first-boot fail-fast).
4. **Email** — pick the provider AND collect its block (SMTP host/port/user/pwd, or SendGrid key, or ACS connection string), else mail won't send.
5. **Registration** — set `allowRegistration`; be explicit that it cannot be disabled server-side (network layer only). It drives the sign-up link in a scaffolded page.
6. **Secrets** — default `local-file`; if `.gitignore` doesn't exclude `appsettings.Local.json`, the CLI downgrades to placeholders and warns.
7. **Theme** — delegate to `/scaffold-theme` (never write `index.css` here).
8. **Invoke** `configure-login`; relay `warnings` + `nextSteps`.

---

## CLI 2 — scaffold-login-page (editable login override)

Emits an editable login page the developer OWNS and wires it through the
override seam — branding + only the providers / sign-up link you ask for:

```bash
npx --prefer-offline tsx skills/login-config/cli/scaffold-login-page/index.ts \
  --spec '{
    "projectPath": "/abs/project",
    "appCode": "demo",
    "componentName": "CustomLoginPage",
    "providers": { "local": true, "microsoft": true, "entra": false },
    "allowRegistration": false,
    "branding": { "appName": "Acme Portal", "subtitle": "Connectez-vous", "logoUrl": "/logo.svg" }
  }'
```

Emits (relative to `web/{appCode}-web/src/`):
- `auth/{ComponentName}.tsx` — the editable page (`skip-if-exists`; uses `useLoginForm()` + `PageProps`). Email/password is wired through `useLoginForm().handleSubmit` (tokens land in httpOnly cookies, redirects + onboarding handled by the hook — you only own presentation). OAuth → `/api/auth/{microsoft,google}`, Entra SSO → `/sso`.
- `extensions/login.generated.ts` — `loginExtensions = { pages: { [PAGE_KEYS.LOGIN]: … } }` (`overwrite`).
- wires `main.tsx`: imports the module + spreads `...loginExtensions` into `extensions: {}` (idempotent; **coexists** with `...vitrineExtensions` from `/site-vitrine`).

### Spec fields

| Field | Required | Notes |
|---|---|---|
| `projectPath`, `appCode` | ✅ | generated app root + app code |
| `providers` | ✅ | `{ local, microsoft?, google?, entra? }` — which affordances to RENDER (≥ 1). `local` ⇒ email/password form. |
| `componentName` | — | PascalCase, default `CustomLoginPage` |
| `allowRegistration` | — | default false; shows the "create account" link (UI affordance only) |
| `branding` | — | `appName`, `logoUrl`, `title`, `subtitle` |
| `registerHref` / `forgotPasswordHref` | — | default `/register` / `/forgot-password` |
| `showForgotPassword` | — | default true |
| `force` | — | overwrite the page even if it exists (loses dev edits) |

Non-blocking version guard: the seam needs `@atlashub/smartstack >= 3.55.0`
(warns, never refuses). Entra button needs `VITE_MSAL_CLIENT_ID` at build time.

## Idempotency

- **appsettings** are **deep-merged** (configure-login) — only owned keys change; secrets partitioned to `appsettings.Local.json`.
- **page component** is `skip-if-exists` — re-running preserves dev edits (use `force` to regenerate).
- **wiring module** is regenerated; **main.tsx** injection is marker-aware (`// @customised` skips it) and idempotent.

## When NOT to use

- Restyle the app theme → `/scaffold-theme` (the login inherits it).
- Permission gating inside pages → `scaffold-frontend-auth`.
- Change the OAuth button rendering at the framework level, the registration
  endpoint behavior, or add a server-side registration switch → that lives in
  SmartStack.app (the package), not in a generated client app.
- Public pre-auth pages (home, presentation) → `/site-vitrine`.
