ngx-cookie-service-ssr
Version:
Angular SSR cookie service
349 lines (344 loc) • 13.1 kB
JavaScript
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