Serve the editor from feuerwerk.nockal.com as well
The subdomain proxies to the same folder, but the build had the path /feuerwerk/ baked in. On feuerwerk.nockal.com the page loaded its assets, the API and the data URLs from /feuerwerk/... and got 404. The session cookie had Path=/feuerwerk/, so the browser would never have sent it to /api/ there either. - site.ts takes the base path from the folder of the open page: /feuerwerk/ on nockal.com, / on the subdomain. Release links point at the address you are working on. - vite.config.ts builds with a relative base. The copy plugin rewrites the pages to ./dist/assets/ and stops the build if it finds nothing to rewrite. The dev server now runs at http://localhost:5175/. - Auth.php sets the cookie without a Path, so it belongs to the API folder the browser sees. Sign-in and sign-out also clear the old /feuerwerk/ cookie, otherwise it would keep signing in after a logout. Existing sessions stay valid. - README: the new dev server address. Tests: - curl on both addresses: pages, JS, CSS and API 200 - sign-in, session, sign-out on both addresses - e2e 70 against both addresses: 13/13 each, no CSP violation - tsc, lint, prettier clean, site and releases tests 14/14 - PHP tests 7 of 8 files green, videos.test.php fails only because the web-php-fpm container has no ffmpeg Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b9d084dfe
commit
edef2b6323
5 files changed
+57
-17
No files matched your search
@@ -39,7 +39,7 @@ Node 22 is required.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # http://localhost:5175/feuerwerk/dist/; API calls are proxied to production
|
||||
npm run dev # http://localhost:5175/; API and data calls are proxied to production
|
||||
npm run check # typecheck + lint + format:check + unit tests; the gate CI runs too
|
||||
npm run build # writes dist/ and regenerates index.html; the folder is what nginx serves
|
||||
```
|
||||
|
||||
+14
-4
@@ -109,19 +109,29 @@ final class Auth
|
||||
return (string) ($_COOKIE[SessionStore::COOKIE] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* The cookie has no `Path`: the browser takes the folder of the API as it sees it, which is
|
||||
* `/feuerwerk/api` on nockal.com and `/api` on feuerwerk.nockal.com. nginx sees the second as
|
||||
* the first, so no path this side writes down would be right for both. The editor's pages and
|
||||
* the data never needed the cookie.
|
||||
*
|
||||
* Before that, the cookie was set for the whole site (`/feuerwerk/`). Such an old one is ended
|
||||
* with every new one, or it would still sign a browser in after signing out.
|
||||
*/
|
||||
private function setCookie(string $value, int $expires): void
|
||||
{
|
||||
setcookie(SessionStore::COOKIE, $value, [
|
||||
$options = [
|
||||
'expires' => $expires,
|
||||
'path' => self::basePath(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
'secure' => ($_SERVER['HTTPS'] ?? '') !== '',
|
||||
]);
|
||||
];
|
||||
setcookie(SessionStore::COOKIE, $value, $options);
|
||||
setcookie(SessionStore::COOKIE, '', ['expires' => time() - 3600, 'path' => self::basePath()] + $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* The path the editor is served under (`/feuerwerk/`), read off the script rather than
|
||||
* The path the old cookie was set for (`/feuerwerk/`), read off the script rather than
|
||||
* written down: the same checkout answers under a different prefix in CI.
|
||||
*/
|
||||
private static function basePath(): string
|
||||
|
||||
+12
-2
@@ -1,5 +1,15 @@
|
||||
/** The path the site is served under: the editor, the play page, the API and every data file. */
|
||||
export const SITE_PATH = '/feuerwerk/';
|
||||
/**
|
||||
* The path the site is served under: the editor, the play page, the API and every data file. It
|
||||
* is the folder of the open page, so one build answers at https://nockal.com/feuerwerk/ and at
|
||||
* https://feuerwerk.nockal.com/ alike. Away from a page (Node, the tests) it is the nginx path.
|
||||
*/
|
||||
export const SITE_PATH = sitePath(typeof location === 'undefined' ? undefined : location.pathname);
|
||||
|
||||
/** The folder of a page path: `/feuerwerk/play.html` and `/feuerwerk/` give `/feuerwerk/`. */
|
||||
export function sitePath(pagePath: string | undefined): string {
|
||||
if (pagePath === undefined) return '/feuerwerk/';
|
||||
return pagePath.slice(0, pagePath.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
/** The API's one entry point. */
|
||||
export const API_ENDPOINT = `${SITE_PATH}api/index.php`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { API_ENDPOINT, dataPath, moveOldStoredKeys } from '../../src/core/site';
|
||||
import { API_ENDPOINT, dataPath, moveOldStoredKeys, sitePath } from '../../src/core/site';
|
||||
|
||||
function memoryStore(initial: Record<string, string>) {
|
||||
const items = new Map(Object.entries(initial));
|
||||
@@ -21,6 +21,14 @@ describe('site', () => {
|
||||
expect(dataPath('textures')).toBe('/feuerwerk/data/textures');
|
||||
});
|
||||
|
||||
it('takes the path from the folder of the open page', () => {
|
||||
expect(sitePath('/feuerwerk/')).toBe('/feuerwerk/');
|
||||
expect(sitePath('/feuerwerk/index.html')).toBe('/feuerwerk/');
|
||||
expect(sitePath('/feuerwerk/play.html')).toBe('/feuerwerk/');
|
||||
expect(sitePath('/')).toBe('/');
|
||||
expect(sitePath('/play.html')).toBe('/');
|
||||
});
|
||||
|
||||
it('moves the keys stored under the old name and leaves the others alone', () => {
|
||||
const store = memoryStore({
|
||||
'game-maker.panel-sizes': '{"left":300}',
|
||||
|
||||
+21
-9
@@ -1,27 +1,38 @@
|
||||
import { copyFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { basename, dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig, type Plugin } from 'vite';
|
||||
import { publishPlugins } from './tools/lib/plugins.mjs';
|
||||
import { SITE_PATH } from './src/core/site';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* The workspace folder itself is what nginx serves under https://nockal.com/feuerwerk/.
|
||||
* The workspace folder itself is what nginx serves under https://nockal.com/feuerwerk/, and the
|
||||
* same folder answers at https://feuerwerk.nockal.com/. So the build knows no path: its base is
|
||||
* relative, and the pages find the API and the data from where they are (src/core/site.ts).
|
||||
* The Vite source pages live in `web/`: `index.html` is the editor, `play.html` a released game
|
||||
* (#266). The build writes hashed assets into `dist/` and this plugin copies the built pages to the
|
||||
* folder root, so that the site URLs load the production bundles while `api/` and `data/` sit next
|
||||
* to them.
|
||||
* to them. One folder up, their `./assets/` becomes `./dist/assets/`.
|
||||
*/
|
||||
const PAGES = ['index.html', 'play.html'];
|
||||
|
||||
function publishPages(): Plugin {
|
||||
let outDir = '';
|
||||
return {
|
||||
name: 'feuerwerk:publish-pages',
|
||||
apply: 'build',
|
||||
configResolved(config) {
|
||||
outDir = resolve(config.root, config.build.outDir);
|
||||
},
|
||||
closeBundle() {
|
||||
for (const page of PAGES) copyFileSync(resolve(__dirname, 'dist', page), resolve(__dirname, page));
|
||||
const folder = basename(outDir);
|
||||
for (const page of PAGES) {
|
||||
const html = readFileSync(resolve(outDir, page), 'utf8');
|
||||
const published = html.replaceAll('"./assets/', `"./${folder}/assets/`);
|
||||
if (published === html) throw new Error(`${page}: no ./assets/ link to move`);
|
||||
writeFileSync(resolve(outDir, '..', page), published);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -79,7 +90,7 @@ function shipPlugins(): Plugin {
|
||||
|
||||
export default defineConfig({
|
||||
root: 'web',
|
||||
base: `${SITE_PATH}dist/`,
|
||||
base: './',
|
||||
publicDir: false,
|
||||
plugins: [contentSecurityPolicy(), publishPages(), shipPlugins()],
|
||||
build: {
|
||||
@@ -95,11 +106,12 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 5175,
|
||||
// During development the PHP API is served by the production nginx; proxy it.
|
||||
// During development the PHP API and the data are served by the production nginx; proxy them.
|
||||
proxy: {
|
||||
[`${SITE_PATH}api`]: {
|
||||
'^/(api|data)/': {
|
||||
target: 'https://nockal.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => `/feuerwerk${path}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user