stevermeister/ngx-cookie-service

ETA On SSR

martijnmelchers opened this issue · 8 comments

Is your feature request related to a problem? Please describe

A working version of SSR

Describe the solution you'd like

A working SSR solution

Describe alternatives you've considered

We're currently using ngx-cookie however this is not properly working as of 6.0.0 with Angular 13

Additional context

No response

i tried to implement the ssr support in the same cookies service class. Here is my solution which i am using currently:


import {Inject, Injectable, Optional, PLATFORM_ID} from '@angular/core';
import {DOCUMENT, isPlatformBrowser} from '@angular/common';
import {REQUEST} from "@nguniversal/express-engine/tokens";
import {Request} from "express";

export type SameSite = 'Lax' | 'None' | 'Strict';

export interface CookieOptions {
expires?: number | Date;
path?: string;
domain?: string;
secure?: boolean;
sameSite?: SameSite;
}

@Injectable({
providedIn: 'root',
})
export class CookieService {
private readonly documentIsAccessible: boolean;

constructor(
    @Inject(DOCUMENT) private document: Document,
    // Get the `PLATFORM_ID` so we can check if we're in a browser.
    @Inject(PLATFORM_ID) private platformId: string,
    @Optional() @Inject(REQUEST) private request: Request
) {
    this.documentIsAccessible = isPlatformBrowser(this.platformId);
}

/**
 * Get cookie Regular Expression
 *
 * @param name Cookie name
 * @returns property RegExp
 *
 * @author: Stepan Suvorov
 * @since: 1.0.0
 */
private static getCookieRegExp(name: string): RegExp {
    const escapedName: string = name.replace(/([\[\]\{\}\(\)\|\=\;\+\?\,\.\*\^\$])/gi, '\\$1');

    return new RegExp('(?:^' + escapedName + '|;\\s*' + escapedName + ')=(.*?)(?:;|$)', 'g');
}

/**
 * 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
 */
private static safeDecodeURIComponent(encodedURIComponent: string): string {
    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: string): boolean {
    name = encodeURIComponent(name);
    const regExp: RegExp = CookieService.getCookieRegExp(name);
    return regExp.test(this.getPlatformCookies());
}

/**
 * Get cookies by name
 *
 * @param name Cookie name
 * @returns property value
 *
 * @author: Stepan Suvorov
 * @since: 1.0.0
 */
get(name: string): string {
    if (this.check(name)) {
        name = encodeURIComponent(name);

        const regExp: RegExp = CookieService.getCookieRegExp(name);
        const result: RegExpExecArray | null = regExp.exec(this.getPlatformCookies());

        return result && result[1] ? CookieService.safeDecodeURIComponent(result[1]) : '';
    } else {
        return '';
    }
}

/**
 * Get all cookies in JSON format
 *
 * @returns all the cookies in json
 *
 * @author: Stepan Suvorov
 * @since: 1.0.0
 */
getAll(): { [key: string]: string } {

    const cookies: { [key: string]: string } = {};
    const cookiesString: string = this.getPlatformCookies();

    if (cookiesString) {
        cookiesString.split(';').forEach((currentCookie: string) => {
            const [cookieName, cookieValue] = currentCookie.split('=');
            cookies[CookieService.safeDecodeURIComponent(cookieName.replace(/^ /, ''))] = CookieService.safeDecodeURIComponent(cookieValue);
        });
    }

    return cookies;
}

/**
 * Set cookie based on provided information
 *
 * @param name     Cookie name
 * @param value    Cookie value
 * @param expires  Number of days until the cookies expires or an actual `Date`
 * @param path     Cookie path
 * @param domain   Cookie domain
 * @param secure   Secure flag
 * @param sameSite OWASP samesite token `Lax`, `None`, or `Strict`. Defaults to `Lax`
 *
 * @author: Stepan Suvorov
 * @since: 1.0.0
 */
set(
    name: string,
    value: string,
    expires?: CookieOptions['expires'],
    path?: CookieOptions['path'],
    domain?: CookieOptions['domain'],
    secure?: CookieOptions['secure'],
    sameSite?: SameSite
): void;

/**
 * Set cookie based on provided information
 *
 * Cookie's parameters:
 * <pre>
 * expires  Number of days until the cookies expires or an actual `Date`
 * path     Cookie path
 * domain   Cookie domain
 * secure   Secure flag
 * sameSite OWASP samesite token `Lax`, `None`, or `Strict`. Defaults to `Lax`
 * </pre>
 *
 * @param name     Cookie name
 * @param value    Cookie value
 * @param options  Body with cookie's params
 *
 * @author: Stepan Suvorov
 * @since: 1.0.0
 */
set(name: string, value: string, options?: CookieOptions): void;

set(
    name: string,
    value: string,
    expiresOrOptions?: CookieOptions['expires'] | CookieOptions,
    path?: CookieOptions['path'],
    domain?: CookieOptions['domain'],
    secure?: CookieOptions['secure'],
    sameSite?: SameSite
): void {
    if (!this.documentIsAccessible) {
        return;
    }

    if (typeof expiresOrOptions === 'number' || expiresOrOptions instanceof Date || path || domain || secure || sameSite) {
        const optionsBody = {
            expires: expiresOrOptions as CookieOptions['expires'],
            path,
            domain,
            secure,
            sameSite: sameSite ? sameSite : 'Lax',
        };

        this.set(name, value, optionsBody);
        return;
    }

    let cookieString: string = encodeURIComponent(name) + '=' + encodeURIComponent(value) + ';';

    const options = expiresOrOptions ? expiresOrOptions : {};

    if (options.expires) {
        if (typeof options.expires === 'number') {
            const dateExpires: Date = new Date(new Date().getTime() + options.expires * 1000 * 60 * 60 * 24);

            cookieString += 'expires=' + dateExpires.toUTCString() + ';';
        } else {
            cookieString += 'expires=' + options.expires.toUTCString() + ';';
        }
    }

    if (options.path) {
        cookieString += 'path=' + options.path + ';';
    }

    if (options.domain) {
        cookieString += 'domain=' + options.domain + ';';
    }

    if (options.secure === false && options.sameSite === 'None') {
        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`
        );
    }
    if (options.secure) {
        cookieString += 'secure;';
    }

    if (!options.sameSite) {
        options.sameSite = 'Lax';
    }

    cookieString += 'sameSite=' + options.sameSite + ';';

    // this.document.cookie = cookieString
    this.setPlatformCookies(cookieString);
}

/**
 * Delete cookie by name
 *
 * @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: string, path?: CookieOptions['path'], domain?: CookieOptions['domain'], secure?: CookieOptions['secure'], sameSite: SameSite = 'Lax'): void {
    if (!this.documentIsAccessible) {
        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
 *
 * @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?: CookieOptions['path'], domain?: CookieOptions['domain'], secure?: CookieOptions['secure'], sameSite: SameSite = 'Lax'): void {
    if (!this.documentIsAccessible) {
        return;
    }

    const cookies: any = this.getAll();

    for (const cookieName in cookies) {
        if (cookies.hasOwnProperty(cookieName)) {
            this.delete(cookieName, path, domain, secure, sameSite);
        }
    }
}

getPlatformCookies(): string {
    let cookies = '';
    if (this.documentIsAccessible) {
        cookies = this.document.cookie;
    } else {
        cookies =  this.request.headers.cookie != undefined ? this.request.headers.cookie : '';
    }
    return cookies;
}

setPlatformCookies(cookieString: string) {
    if (this.documentIsAccessible) {
        this.document.cookie = cookieString;
    } else {
        this.request.headers.cookie = cookieString;
    }
}

}

@martijnmelchers for some reason I didn't get notification for this issue. First, what error you see?

Second, we currently have 2 solutions (#214 and #206) for this and one of it will make it to production. I will keep the issue open and let you know the progress.

is there a solution yet?

is there a solution yet?

Haven't got a chance to work on this. But current version 14.0.0 should work with SSR and shows login page for brief period of time and we are working to fix this.

is there a solution yet?

Haven't got a chance to work on this. But current version 14.0.0 should work with SSR and shows login page for brief period of time and we are working to fix this.

You should check out other libraries doing the same. We've been using ngx-cookie for a while now and it's been working fine. However 6.0 broke because of a parsing error. Our only hold back to switching to this library is a stable working SSR version

is there a solution yet?

Haven't got a chance to work on this. But current version 14.0.0 should work with SSR and shows login page for brief period of time and we are working to fix this.

You should check out other libraries doing the same. We've been using ngx-cookie for a while now and it's been working fine. However 6.0 broke because of a parsing error. Our only hold back to switching to this library is a stable working SSR version

Understood. BTW SSR always worked in this library v12, v13 etc... We are in the process of adding dedicated SSR support

@martijnmelchers @viniciusverasdossantos @bahag-mersinlioglue Just now published version 14.0.0-beta.0 to NPM. Could you guys try it and let me know how it goes.

I am closing since I have not heard from anyone. If anyone finds this comment, you can install SSR version of the library and usage guide here