# pafish Web

Dynamic website and store server for pafish.

This directory is separate from the installable blog runtime. It will host:

- official website pages
- public app store catalog
- developer accounts
- app submission and review
- orders, payments, licenses, downloads

The blog runtime may stay lightweight, while this service uses MySQL/InnoDB for multi-user platform data.

## Current Scaffold

- Public pages: `/`, `/store`, `/store/themes/{slug}`, `/store/extensions/{slug}`, `/docs`, `/changelog`, `/download`
- Store discovery: type, price, tag, keyword, latest/popular/name sorting
- SEO: per-page meta, canonical URLs, Open Graph tags, app detail JSON-LD, `/sitemap.xml`, `/robots.txt`
- Public APIs: `/api/catalog`, `/api/apps/{type}/{slug}`
- Developer APIs (token auth): `/api/developer/apps`
- Admin pages: `/admin`, `/admin/apps`
- Developer pages: `/admin/developer`, `/admin/developers`
- Version review pages: `/admin/versions`
- Commerce pages: `/admin/orders`, `/admin/licenses`

The admin uses MySQL when `pdo_mysql` and the configured database are available. If MySQL is not ready, it falls back to `resources/data/apps.json` so the website can still be previewed.

## Install

The fastest path is the web installer:

1. Create an empty MySQL database using `utf8mb4` (collation `utf8mb4_0900_ai_ci`).
2. Point your web server document root at `public/` and open `/setup` in a browser (`/install` is kept as an alias).
3. Fill in the database connection and the admin account. The installer will:
   - run all migrations in `database/migrations/` (idempotent),
   - create the admin user with a hashed password,
   - write connection settings to `storage/runtime.php`,
   - write `storage/installed.lock` to prevent re-installation.
4. After install completes you are redirected to `/admin/login`. Sign in with the admin account you just set.

Until the site is installed, every route except `/setup` and `/install` redirects there (API routes return `503`).

To re-run the installer, delete `storage/installed.lock` (and optionally `storage/runtime.php`).

### 部署包与重新安装

使用 `php scripts/build-release.php --tag=v0.1.0` 构建官网部署包。生成的 zip 不包含本地数据库配置、安装锁、备份和上传文件，解压到新的空目录后即可进入 `/setup`。

如果是覆盖已有站点目录，`storage/installed.lock` 会被保留，这是防止误触发重装的保护机制。只有在明确要重装时，才先备份并删除目标站点的 `storage/installed.lock`、`storage/runtime.php`，同时确认是否需要清空或更换数据库；生产站点不要直接删除这两个文件。

## 在线更新（系统更新）

后台「系统配置 → 系统更新」提供一键在线覆盖更新，无需操作服务器：
- 更新自动保留 `storage/`（数据库凭据 `runtime.php`、安装锁 `installed.lock`、限流计数、应用包）和
  `public/pafish-web/`（官网更新清单与安装包），
  因此**永远不会触发重新安装**；`config/app.php` 会被新版覆盖，旧文件备份在
  `storage/upgrade-bak-*/`，且旧 `site.url` / `site.blog_url` 会自动并入 `storage/runtime.php` 继续生效。
- 更新前整站自动备份，失败自动整体回滚；数据库迁移无需手动执行，
  下一次请求由 `Migrator` 自动补跑 `database/migrations/` 中未执行的迁移。
- 三种安装方式：
  1. **官方在线更新**：默认检查 `https://www.pafish.cn/pafish-web/pafish-web.json`，通过 HTTPS 下载 zip、校验 SHA-256 后安装。
  2. **上传更新包**：下载新版本 zip 后直接在页面里上传，点「立即更新」。
  3. **本地更新源**：服务器上放一个发布目录（如 `/www/wwwroot/pafish-web-releases`），
     写入 `config/app.php` 的 `upgrade.releases_dir`；配置后优先于官方在线更新，适合内网和测试环境。

发布新版本时：

1. 在 `config/app.php` 里把 `version` 改为新版本号。
2. 执行 `php scripts/build-release.php --notes="本次更新说明"`。脚本会生成部署包，并在
   `public/pafish-web/` 生成 `pafish-web.json`、对应 zip 和 SHA-256。
3. 发布官网时，将 `public/pafish-web/` 同步到 `www.pafish.cn`，使其可通过官方更新地址访问。
   部署包不携带该目录，避免把更新包递归打入自身。
4. 若本次改动含数据库变更，把新迁移文件加入 `database/migrations/` 并在
   `app/Support/Migrator.php` 的 `MIGRATIONS` 常量末尾追加名称（顺序执行）。

## 数据备份（后台「系统配置 → 数据备份」）

- 「立即备份」导出完整数据库到 `storage/backups/backup-*.sql`（mysqldump CLI 优先，
  `--single-transaction --routines --triggers`；不可用时纯 PHP 逐表导出兜底）。
- 列表支持下载 / 还原 / 删除；还原前自动把当前库导出为安全备份（`backup-*`），
  误操作可随时用安全备份还原回来；还原需输入备份文件名二次确认。
- 上传 `.sql` 文件可跨站点还原（保存为 `upload-*`，仅 .sql、≤200MB、头部须含
  `CREATE TABLE|INSERT INTO|mysqldump`）。
- 备份目录在 `storage/` 内，一键在线更新不会触碰；已加入 `.gitignore`。

### Nginx + PHP-FPM

Use `pafish-web/public` as the site root and forward missing files to `index.php`:

```nginx
root /www/wwwroot/pafish-web/public;
index index.php index.html;

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass 127.0.0.1:9000;
}
```

Make sure the PHP-FPM user can write to `pafish-web/storage`.

### Manual / environment-variable setup (optional)

You can skip the installer by setting connection values in `config/app.php` or via environment variables, then importing migrations manually:

- `PAFISH_DB_HOST`, `PAFISH_DB_PORT`, `PAFISH_DB_DATABASE`, `PAFISH_DB_USERNAME`, `PAFISH_DB_PASSWORD`
- Import each file in `database/migrations/` in filename order.
- Insert an admin user with a `password_hash()` value in `users.password_hash` (see `database/schema/seed_admin.sql` as a template).

`storage/runtime.php` (written by the installer) takes precedence over `config/app.php`; environment variables take precedence over both.

Generate a password hash manually:

```bash
php -r "echo password_hash('your-password', PASSWORD_DEFAULT), PHP_EOL;"
```

## Security Baseline

- SQL uses PDO prepared statements.
- Admin write actions require CSRF tokens.
- Admin/developer login authenticates against the `users` table only; there is no hardcoded fallback password.
- Session cookies are `HttpOnly` and `SameSite=Lax`; HTTPS requests also use `Secure`.
- Successful login regenerates the session id.
- Developer API tokens are stored as SHA-256 hashes and sent via `Authorization: Bearer`.
- App forms validate type, slug, version, date, URLs, and text length before saving.
- Admin and developer roles are separated through ability checks.
- Developer app ownership isolation requires MySQL because JSON mode is only a local preview fallback.
- App version submission, ZIP package review, and package downloads require MySQL and `ZipArchive`.
- Published packages are served through `/downloads/apps/{versionId}` after review.
- Paid package downloads require a valid license query parameter: `/downloads/apps/{versionId}?license=...`.
- License checks are available at `POST /api/licenses/check`.
- License activation is available at `POST /api/licenses/activate`.
- Pending order creation is available at `POST /api/orders`.
- Responses include basic hardening headers such as `X-Frame-Options` and `X-Content-Type-Options`.
- `storage/runtime.php` holds DB credentials and is git-ignored alongside `storage/installed.lock` and `storage/packages/`.

## Structure

- `public/` - web entry
- `app/Http/` - routing/controllers
- `app/Store/` - catalog and store domain services
- `app/Support/` - database/config helpers, installer, migrator
- `database/migrations/` - versioned MySQL migrations (applied by the installer)
- `database/schema/` - schema reference drafts
- `config/` - service configuration

## Developer API

Generate a token in the admin developer center (`/admin/developer` → 生成 / 重置令牌), then:

```bash
# List your apps
curl -H "Authorization: Bearer <token>" https://example.com/api/developer/apps

# Create an app
curl -H "Authorization: Bearer <token>" \
  -d "type=theme&slug=my-theme&name=My Theme&version=1.0.0&author=me" \
  https://example.com/api/developer/apps

# Update an app
curl -H "Authorization: Bearer <token>" \
  -d "type=theme&slug=my-theme&name=My Theme&version=1.1.0&author=me" \
  https://example.com/api/developer/apps/theme/my-theme

# Upload a new version (multipart; version/requires_version/changelog + package zip)
curl -H "Authorization: Bearer <token>" \
  -F "version=1.2.0" -F "requires_version=0.1.6" -F "changelog=修复若干问题" \
  -F "package=@my-theme-1.2.0.zip" \
  https://example.com/api/developer/apps/theme/my-theme/versions
```

## API Endpoints

- `GET /api/runtime-store/v1/catalog?kind=theme|plugin`
- `GET /api/apps/{type}/{slug}`
- `GET /api/developer/apps` (token)
- `POST /api/developer/apps` (token)
- `POST /api/developer/apps/{type}/{slug}` (token)
- `POST /api/developer/apps/{type}/{slug}/versions` (token, multipart)
- `POST /api/orders`
- `POST /api/licenses/activate`
- `POST /api/licenses/check`
- `POST /api/licenses/deactivate`
- `POST /api/licenses/activations`
