99 * OF ANY KIND, either express or implied. See the License for the specific language
1010 * governing permissions and limitations under the License.
1111 */
12+ import crypto from 'crypto' ;
13+ import express from 'express' ;
1214import { promisify } from 'util' ;
1315import path from 'path' ;
1416import compression from 'compression' ;
1517import utils from './utils.js' ;
1618import RequestContext from './RequestContext.js' ;
1719import { asyncHandler , BaseServer } from './BaseServer.js' ;
1820import LiveReload from './LiveReload.js' ;
21+ import { saveSiteTokenToFile } from '../config/config-utils.js' ;
22+
23+ const LOGIN_ROUTE = '/.aem/cli/login' ;
24+ const LOGIN_ACK_ROUTE = '/.aem/cli/login/ack' ;
1925
2026export class HelixServer extends BaseServer {
2127 /**
@@ -27,6 +33,7 @@ export class HelixServer extends BaseServer {
2733 this . _liveReload = null ;
2834 this . _enableLiveReload = false ;
2935 this . _app . use ( compression ( ) ) ;
36+ this . _autoLogin = true ;
3037 }
3138
3239 withLiveReload ( value ) {
@@ -39,6 +46,91 @@ export class HelixServer extends BaseServer {
3946 return this ;
4047 }
4148
49+ async handleLogin ( req , res ) {
50+ // disable autologin if login was called at least once
51+ this . _autoLogin = false ;
52+ // clear any previous login errors
53+ delete this . loginError ;
54+
55+ if ( ! this . _project . siteLoginUrl ) {
56+ res . status ( 404 ) . send ( 'Login not supported. Could not extract site and org information.' ) ;
57+ return ;
58+ }
59+
60+ this . log . info ( `Starting login process for : ${ this . _project . org } /${ this . _project . site } . Redirecting...` ) ;
61+ this . _loginState = crypto . randomUUID ( ) ;
62+ const loginUrl = `${ this . _project . siteLoginUrl } &state=${ this . _loginState } ` ;
63+ res . status ( 302 ) . set ( 'location' , loginUrl ) . send ( '' ) ;
64+ }
65+
66+ async handleLoginAck ( req , res ) {
67+ const CACHE_CONTROL = 'no-store, private, must-revalidate' ;
68+ const CORS_HEADERS = {
69+ 'access-control-allow-methods' : 'POST, OPTIONS' ,
70+ 'access-control-allow-headers' : 'content-type' ,
71+ } ;
72+
73+ const { origin } = req . headers ;
74+ if ( [ 'https://admin.hlx.page' , 'https://admin-ci.hlx.page' ] . includes ( origin ) ) {
75+ CORS_HEADERS [ 'access-control-allow-origin' ] = origin ;
76+ }
77+
78+ if ( req . method === 'OPTIONS' ) {
79+ res . status ( 200 ) . set ( CORS_HEADERS ) . send ( '' ) ;
80+ return ;
81+ }
82+
83+ if ( req . method === 'POST' ) {
84+ const { state, siteToken } = req . body ;
85+ try {
86+ if ( ! this . _loginState || this . _loginState !== state ) {
87+ this . loginError = { message : 'Login Failed: We received an invalid state.' } ;
88+ this . log . warn ( 'State mismatch. Discarding site token.' ) ;
89+ res . status ( 400 )
90+ . set ( CORS_HEADERS )
91+ . set ( 'cache-control' , CACHE_CONTROL )
92+ . send ( 'Invalid state' ) ;
93+ return ;
94+ }
95+
96+ if ( ! siteToken ) {
97+ this . loginError = { message : 'Login Failed: Missing site token.' } ;
98+ res . status ( 400 )
99+ . set ( 'cache-control' , CACHE_CONTROL )
100+ . set ( CORS_HEADERS )
101+ . send ( 'Missing site token' ) ;
102+ return ;
103+ }
104+
105+ this . withSiteToken ( siteToken ) ;
106+ this . _project . headHtml . setSiteToken ( siteToken ) ;
107+ await saveSiteTokenToFile ( siteToken ) ;
108+ this . log . info ( 'Site token received and saved to file.' ) ;
109+
110+ res . status ( 200 )
111+ . set ( 'cache-control' , CACHE_CONTROL )
112+ . set ( CORS_HEADERS )
113+ . send ( 'Login successful.' ) ;
114+ return ;
115+ } finally {
116+ delete this . _loginState ;
117+ }
118+ }
119+
120+ if ( this . loginError ) {
121+ res . status ( 400 )
122+ . set ( 'cache-control' , CACHE_CONTROL )
123+ . send ( this . loginError . message ) ;
124+ delete this . loginError ;
125+ return ;
126+ }
127+
128+ res . status ( 302 )
129+ . set ( 'cache-control' , CACHE_CONTROL )
130+ . set ( 'location' , '/' )
131+ . send ( '' ) ;
132+ }
133+
42134 /**
43135 * Proxy Mode route handler
44136 * @param {Express.Request } req request
@@ -97,8 +189,8 @@ export class HelixServer extends BaseServer {
97189 }
98190 }
99191
100- // use proxy
101192 try {
193+ // use proxy
102194 const url = new URL ( ctx . url , proxyUrl ) ;
103195 for ( const [ key , value ] of proxyUrl . searchParams . entries ( ) ) {
104196 url . searchParams . append ( key , value ) ;
@@ -111,6 +203,8 @@ export class HelixServer extends BaseServer {
111203 cacheDirectory : this . _project . cacheDirectory ,
112204 file404html : this . _project . file404html ,
113205 siteToken : this . _siteToken ,
206+ loginPath : LOGIN_ROUTE ,
207+ autoLogin : this . _autoLogin ,
114208 } ) ;
115209 } catch ( err ) {
116210 log . error ( `${ pfx } failed to proxy AEM request ${ ctx . path } : ${ err . message } ` ) ;
@@ -126,6 +220,12 @@ export class HelixServer extends BaseServer {
126220 this . _liveReload = new LiveReload ( this . log ) ;
127221 await this . _liveReload . init ( this . app , this . _server ) ;
128222 }
223+
224+ this . app . get ( LOGIN_ROUTE , asyncHandler ( this . handleLogin . bind ( this ) ) ) ;
225+ this . app . get ( LOGIN_ACK_ROUTE , asyncHandler ( this . handleLoginAck . bind ( this ) ) ) ;
226+ this . app . post ( LOGIN_ACK_ROUTE , express . json ( ) , asyncHandler ( this . handleLoginAck . bind ( this ) ) ) ;
227+ this . app . options ( LOGIN_ACK_ROUTE , asyncHandler ( this . handleLoginAck . bind ( this ) ) ) ;
228+
129229 const handler = asyncHandler ( this . handleProxyModeRequest . bind ( this ) ) ;
130230 this . app . get ( '*' , handler ) ;
131231 this . app . post ( '*' , handler ) ;
0 commit comments