UNPKG

ngx-cookie-service-ssr

Version:
349 lines (344 loc) 13.1 kB
import { isPlatformBrowser } from '@angular/common'; import * as i0 from '@angular/core'; import { inject, DOCUMENT, PLATFORM_ID, REQUEST, RESPONSE_INIT, Injectable } from '@angular/core'; class SsrCookieService { constructor() { this.document = inject(DOCUMENT); this.platformId = inject(PLATFORM_ID); this.request = inject(REQUEST, { optional: true }); this.responseInit = inject(RESPONSE_INIT, { optional: true }); this.documentIsAccessible = isPlatformBrowser(this.platformId); } /** * Get cookie Regular Expression * * @param name Cookie name * @returns property RegExp * * @author: Stepan Suvorov * @since: 1.0.0 */ static getCookieRegExp(name) { const escapedName = name.replace(/([[\]{}()|=;+?,.*^$\\])/gi, '\\$1'); return new RegExp('(?:^' + escapedName + '|;\\s*' + escapedName + ')=(.*?)(?:;|$)'); } /** * Gets the unencoded version of an encoded component of a Uniform Resource Identifier (URI). * * @param encodedURIComponent A value representing an encoded URI component. * * @returns The unencoded version of an encoded component of a Uniform Resource Identifier (URI). * * @author: Stepan Suvorov * @since: 1.0.0 */ static safeDecodeURIComponent(encodedURIComponent) { try { return decodeURIComponent(encodedURIComponent); } catch { // probably it is not uri encoded. return as is return encodedURIComponent; } } /** * Return `true` if {@link Document} is accessible, otherwise return `false` * * @param name Cookie name * @returns boolean - whether cookie with specified name exists * * @author: Stepan Suvorov * @since: 1.0.0 */ check(name) { name = encodeURIComponent(name); const regExp = SsrCookieService.getCookieRegExp(name); return regExp.test((this.documentIsAccessible ? this.document.cookie : this.getRequestCookies()) ?? ''); } /** * Get cookies by name * * @param name Cookie name * @returns property value * * @author: Stepan Suvorov * @since: 1.0.0 */ get(name) { if (this.check(name)) { name = encodeURIComponent(name); const regExp = SsrCookieService.getCookieRegExp(name); const result = regExp.exec((this.documentIsAccessible ? this.document.cookie : this.getRequestCookies()) ?? ''); return result?.[1] ? SsrCookieService.safeDecodeURIComponent(result[1]) : ''; } return ''; } /** * Get all cookies in JSON format * * @returns all the cookies in json * * @author: Stepan Suvorov * @since: 1.0.0 */ getAll() { const cookies = {}; const cookieString = this.documentIsAccessible ? this.document?.cookie : this.getRequestCookies(); if (cookieString && cookieString !== '') { cookieString.split(';').forEach((currentCookie) => { const trimmedCookie = currentCookie.trim(); if (!trimmedCookie) { return; } // Find the first '=' to handle cookie values that contain '=' const equalIndex = trimmedCookie.indexOf('='); if (equalIndex === -1) { // Cookie without value (invalid, but handle gracefully) return; } const cookieName = trimmedCookie.substring(0, equalIndex); const cookieValue = trimmedCookie.substring(equalIndex + 1); cookies[SsrCookieService.safeDecodeURIComponent(cookieName)] = SsrCookieService.safeDecodeURIComponent(cookieValue); }); } return cookies; } set(name, value, expiresOrOptions, path, domain, secure, sameSite, partitioned) { const options = this.normalizeOptions(expiresOrOptions, path, domain, secure, sameSite, partitioned); if (!this.documentIsAccessible) { if (this.responseInit) { this.setServerCookie(name, value, options); } return; } // Warn if SameSite=None without Secure on browser if (options.sameSite === 'None' && !options.secure) { options.secure = true; console.warn(`[ngx-cookie-service] Cookie ${name} was forced with secure flag because SameSite=None.` + `More details : https://github.com/stevermeister/ngx-cookie-service/issues/86#issuecomment-597720130`); } const cookieString = this.buildCookieString(name, value, options); this.document.cookie = cookieString; } /** * Delete cookie by name at given path and domain. If not path is not specified, cookie at '/' path will be deleted. * * @param name Cookie name * @param path Cookie path * @param domain Cookie domain * @param secure Cookie secure flag * @param sameSite Cookie sameSite flag - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite * * @author: Stepan Suvorov * @since: 1.0.0 */ delete(name, path, domain, secure, sameSite = 'Lax') { if (!this.documentIsAccessible && !this.responseInit) { return; } const expiresDate = new Date('Thu, 01 Jan 1970 00:00:01 GMT'); this.set(name, '', { expires: expiresDate, path, domain, secure, sameSite }); } /** * Delete all cookies at given path and domain. If not path is not specified, all cookies at '/' path will be deleted. * * @param path Cookie path * @param domain Cookie domain * @param secure Is the Cookie secure * @param sameSite Is the cookie same site * * @author: Stepan Suvorov * @since: 1.0.0 */ deleteAll(path, domain, secure, sameSite = 'Lax') { if (!this.documentIsAccessible && !this.responseInit) { return; } const cookies = this.getAll(); for (const cookieName in cookies) { if (cookies.hasOwnProperty(cookieName)) { this.delete(cookieName, path, domain, secure, sameSite); } } } /** * Helper method to safely get cookies from request object * Handles both Angular's REQUEST interface (Web API Request) and Express's req interface */ getRequestCookies() { if (!this.request) { return null; } // Handle Web API Request object (Angular 19+ with AngularNodeAppEngine) // The REQUEST token provides a standard Web API Request object if (this.request.headers && typeof this.request.headers.get === 'function') { return this.request.headers.get('cookie'); } // Handle Express request object (for backward compatibility or custom setups) const reqAny = this.request; if (typeof reqAny.get === 'function') { const cookie = reqAny.get('cookie') || reqAny.get('Cookie'); if (cookie) { return cookie; } } // Handle direct headers object access (Express typically uses lowercase 'cookie') const headers = this.request.headers; if (headers && typeof headers === 'object') { // Check both lowercase and capitalized versions if (headers['cookie']) { return headers['cookie']; } if (headers['Cookie']) { return headers['Cookie']; } } return null; } /** * Helper method to set cookies on the server response * * @param name Cookie name * @param value Cookie value * @param options Cookie options * * @author: Pavankumar Jadda * @since: 21.2.0 */ setServerCookie(name, value, options = {}) { if (!this.responseInit) { return; } // SameSite=None requires Secure if (options.sameSite === 'None' && !options.secure) { options.secure = true; } const cookieString = this.buildCookieString(name, value, options); this.appendSetCookieHeader(cookieString); } /** * Normalizes cookie options from either an options object or individual parameters * * @param expiresOrOptions Either a CookieOptions object, expiration (number of days or Date), or undefined * @param path Cookie path * @param domain Cookie domain * @param secure Cookie secure flag * @param sameSite Cookie sameSite flag * @param partitioned Cookie partitioned flag * @returns Normalized CookieOptions object * * @author: Pavankumar Jadda * @since: 21.2.0 */ normalizeOptions(expiresOrOptions, path, domain, secure, sameSite, partitioned) { // If it's an object but not a Date, treat it as CookieOptions if (typeof expiresOrOptions === 'object' && !(expiresOrOptions instanceof Date)) { return { ...expiresOrOptions }; } // Otherwise, it's either a number, Date, or undefined - build options from individual params return { expires: expiresOrOptions, path, domain, secure, sameSite, partitioned, }; } /** * Builds a cookie string from name, value, and options * * @param name Cookie name * @param value Cookie value * @param options Cookie options * @returns Formatted cookie string * * @author: Pavankumar Jadda * @since: 21.2.0 */ buildCookieString(name, value, options = {}) { let cookieString = encodeURIComponent(name) + '=' + encodeURIComponent(value) + ';'; if (options.expires) { if (typeof options.expires === 'number') { const dateExpires = new Date(Date.now() + options.expires * 1000 * 60 * 60 * 24); cookieString += 'Expires=' + dateExpires.toUTCString() + ';'; } else { cookieString += 'Expires=' + options.expires.toUTCString() + ';'; } } if (options.maxAge !== undefined) { cookieString += 'Max-Age=' + options.maxAge + ';'; } if (options.path) { cookieString += 'Path=' + options.path + ';'; } if (options.domain) { cookieString += 'Domain=' + options.domain + ';'; } if (options.secure) { cookieString += 'Secure;'; } cookieString += 'SameSite=' + (options.sameSite ?? 'Lax') + ';'; if (options.partitioned) { cookieString += 'Partitioned;'; } if (options.httpOnly) { cookieString += 'HttpOnly;'; } return cookieString; } /** * Appends a Set-Cookie header to the response * Handles different header formats (Headers object, array, or plain object) * * @param cookieString The formatted cookie string to append * * @author: Pavankumar Jadda * @since: 21.2.0 */ appendSetCookieHeader(cookieString) { if (!this.responseInit) { return; } // Initialize headers if missing if (!this.responseInit.headers) { this.responseInit.headers = new Headers(); } const headers = this.responseInit.headers; if (headers instanceof Headers) { headers.append('Set-Cookie', cookieString); } else if (Array.isArray(headers)) { // If it's an array (sequence of sequences), push new header headers.push(['Set-Cookie', cookieString]); } else { // Handle plain object format const headersObj = headers; const existing = headersObj['Set-Cookie']; if (existing) { headersObj['Set-Cookie'] = Array.isArray(existing) ? [...existing, cookieString] : [existing, cookieString]; } else { headersObj['Set-Cookie'] = cookieString; } } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SsrCookieService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SsrCookieService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SsrCookieService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); /* * Public API Surface of ngx-cookie-service-ssr */ /** * Generated bundle index. Do not edit. */ export { SsrCookieService }; //# sourceMappingURL=ngx-cookie-service-ssr.mjs.map