From ddf5d8cff17e1b030c78048ce0ad47f7ae65db20 Mon Sep 17 00:00:00 2001 From: Andrei Tuicu Date: Sun, 22 Dec 2024 11:54:06 +0200 Subject: [PATCH] feat: Allow AEM CLI to obtain site token --- src/server/HelixServer.js | 46 +++++--- test/server.test.js | 229 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+), 14 deletions(-) diff --git a/src/server/HelixServer.js b/src/server/HelixServer.js index 4c5407f0..5fde062f 100644 --- a/src/server/HelixServer.js +++ b/src/server/HelixServer.js @@ -34,6 +34,7 @@ export class HelixServer extends BaseServer { this._enableLiveReload = false; this._app.use(compression()); this._autoLogin = true; + this._saveSiteTokenToDotEnv = true; } withLiveReload(value) { @@ -64,6 +65,7 @@ export class HelixServer extends BaseServer { } async handleLoginAck(req, res) { + const CACHE_CONTROL = 'no-store, private, must-revalidate'; const CORS_HEADERS = { 'access-control-allow-methods': 'POST, OPTIONS', 'access-control-allow-headers': 'content-type', @@ -85,35 +87,51 @@ export class HelixServer extends BaseServer { if (!this._loginState || this._loginState !== state) { this.loginError = { message: 'Login Failed: We received an invalid state.' }; this.log.warn('State mismatch. Discarding site token.'); - res.status(400).set(CORS_HEADERS).send('Invalid state'); + res.status(400) + .set(CORS_HEADERS) + .set('cache-control', CACHE_CONTROL) + .send('Invalid state'); return; } if (!siteToken) { - this.loginError = { message: 'Login Failed: We received an invalid state.' }; - res.status(400).set(CORS_HEADERS).send('Missing site token'); + this.loginError = { message: 'Login Failed: Missing site token.' }; + res.status(400) + .set('cache-control', CACHE_CONTROL) + .set(CORS_HEADERS) + .send('Missing site token'); return; } this.withSiteToken(siteToken); this._project.headHtml.setSiteToken(siteToken); - await writeSiteTokenToEnv(siteToken); + if (this._saveSiteTokenToDotEnv) { + await writeSiteTokenToEnv(siteToken); + } this.log.info('Site token received and saved to .env file.'); - res.status(200).set(CORS_HEADERS).send('Login successful.'); + res.status(200) + .set('cache-control', CACHE_CONTROL) + .set(CORS_HEADERS) + .send('Login successful.'); return; } finally { - this._loginState = null; + delete this._loginState; } } if (this.loginError) { - res.status(400).send(this.loginError.message); + res.status(400) + .set('cache-control', CACHE_CONTROL) + .send(this.loginError.message); delete this.loginError; return; } - res.status(302).set('location', '/').send(''); + res.status(302) + .set('cache-control', CACHE_CONTROL) + .set('location', '/') + .send(''); } /** @@ -174,13 +192,13 @@ export class HelixServer extends BaseServer { } } - // use proxy - const url = new URL(ctx.url, proxyUrl); - for (const [key, value] of proxyUrl.searchParams.entries()) { - url.searchParams.append(key, value); - } - try { + // use proxy + const url = new URL(ctx.url, proxyUrl); + for (const [key, value] of proxyUrl.searchParams.entries()) { + url.searchParams.append(key, value); + } + await utils.proxyRequest(ctx, url.href, req, res, { injectLiveReload: this._project.liveReload, headHtml: this._project.headHtml, diff --git a/test/server.test.js b/test/server.test.js index e734b803..4c2a35c0 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -440,4 +440,233 @@ describe('Helix Server', () => { await project.stop(); } }); + + it('starts login', async () => { + const cwd = await setupProject(path.join(__rootdir, 'test', 'fixtures', 'project'), testRoot); + const project = new HelixProject() + .withCwd(cwd) + .withHttpPort(3000) + .withProxyUrl('http://main--foo--bar.aem.page') + .withSiteLoginUrl('https://admin.hlx.page/login/bar/foo/main?client_id=aem-cli&response_type=site_token&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F.aem%2Fcli%2Flogin%2Fack'); + + await project.init(); + project.log.level = 'silly'; + + try { + await project.start(); + const resp = await getFetch()(`http://127.0.0.1:${project.server.port}/.aem/cli/login`, { + cache: 'no-store', + redirect: 'manual', + }); + assert.strictEqual(resp.status, 302); + assert.ok( + resp.headers.get('location').startsWith('https://admin.hlx.page/login/bar/foo/main?client_id=aem-cli&response_type=site_token&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F.aem%2Fcli%2Flogin%2Fack&state='), + ); + } finally { + await project.stop(); + } + }); + + it('starts auto login when receiving 401 during navigation', async () => { + const cwd = await setupProject(path.join(__rootdir, 'test', 'fixtures', 'project'), testRoot); + const project = new HelixProject() + .withCwd(cwd) + .withHttpPort(3000) + .withProxyUrl('http://main--foo--bar.aem.page') + .withSiteLoginUrl('https://admin.hlx.page/login/bar/foo/main?client_id=aem-cli&response_type=site_token&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F.aem%2Fcli%2Flogin%2Fack'); + + await project.init(); + project.log.level = 'silly'; + + nock('http://main--foo--bar.aem.page').get('/').reply(401, 'Unauthorized'); + + try { + await project.start(); + const resp = await getFetch()(`http://127.0.0.1:${project.server.port}/`, { + cache: 'no-store', + redirect: 'manual', + // emulate browser navigation + headers: { + 'sec-fetch-mode': 'navigate', + 'sec-fetch-dest': 'document', + }, + }); + assert.strictEqual(resp.status, 302); + assert.strictEqual(resp.headers.get('location'), '/.aem/cli/login'); + } finally { + await project.stop(); + } + }); + + it('receives site token, saves it and uses it', async () => { + const cwd = await setupProject(path.join(__rootdir, 'test', 'fixtures', 'project'), testRoot); + const project = new HelixProject() + .withCwd(cwd) + .withHttpPort(3000) + .withProxyUrl('http://main--foo--bar.aem.page') + .withSiteLoginUrl('https://admin.hlx.page/login/bar/foo/main?client_id=aem-cli&response_type=site_token&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F.aem%2Fcli%2Flogin%2Fack'); + + await project.init(); + project.log.level = 'silly'; + + nock('http://main--foo--bar.aem.page') + .get('/') + .reply(function fn() { + assert.strictEqual(this.req.headers.authorization, 'token test-site-token'); + return [200, 'hello', { 'content-type': 'text/html' }]; + }) + .get('/head.html') + .reply(function fn() { + assert.strictEqual(this.req.headers.authorization, 'token test-site-token'); + return [200, '