Tài liệu

Framework SSR

Framework SSR Kết nối app Nuxt, Next.js, Angular, SvelteKit hoặc Remix với Enfyra bằng REST cùng origin, OAuth cookie, refresh-token support và Socket.IO. Mô hình tích hợp Trình duyệt -> origin của ứng dụng SSR -> tiền tố proxy Enfyra cục bộ -> cầu nối /api và /ws của ứng dụng En

Framework SSR

Kết nối app Nuxt, Next.js, Angular, SvelteKit hoặc Remix với Enfyra bằng REST cùng origin, OAuth cookie, refresh-token support và Socket.IO.

Mô hình tích hợp

Trình duyệt
  -> origin của ứng dụng SSR
  -> tiền tố proxy Enfyra cục bộ
  -> cầu nối /api và /ws của ứng dụng Enfyra
  -> máy chủ Enfyra

Không gọi raw Enfyra server trong browser code. Browser chỉ gọi origin của chính app bạn.

Proxy cần có

Expose hai path từ app:

Local path Chuyển tiếp đến Mục đích
/enfyra/** https://demo.enfyra.io/api/** REST, auth, OAuth, file, GraphQL
/socket.io/** https://demo.enfyra.io/ws/socket.io/** Socket.IO transport

Bạn có thể chọn REST prefix khác nhưng phải giữ ổn định. Nếu là /enfyra, OAuth bắt buộc truyền cookieBridgePrefix=/enfyra.

Browser code dùng chung

Khi proxy đã có, browser call giống nhau trong mọi SSR framework.

Đăng nhập password:

await fetch("/enfyra/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  credentials: "include",
  body: JSON.stringify({ email, password, remember: true }),
})

Lấy current user:

const me = await fetch("/enfyra/me", {
  credentials: "include",
})

Bắt đầu OAuth ở Enfyra app URL rồi quay về qua app proxy:

const redirect = new URL("/chat", window.location.origin)
const url = new URL("/api/auth/google", "https://demo.enfyra.io")

url.searchParams.set("redirect", redirect.toString())
url.searchParams.set("cookieBridgePrefix", "/enfyra")

window.location.href = url.toString()

Socket.IO:

import { io } from "socket.io-client"

const socket = io("/chat", {
  path: "/socket.io",
  withCredentials: true,
  reconnection: false,
  transports: ["polling"],
  upgrade: false,
})

/chat là Enfyra websocket namespace; /socket.io là transport path SSR app proxy đến Enfyra websocket bridge. Demo chat app dùng polling-only vì hoạt động ổn định qua rewrite/reverse proxy đơn giản; khi deployment proxy hỗ trợ WebSocket upgrade tốt, bạn có thể bật websocket transport.

Luồng OAuth

Người dùng bấm đăng nhập Google
  -> https://demo.enfyra.io/api/auth/google?redirect=...&cookieBridgePrefix=/enfyra
  -> Enfyra chuyển hướng đến Google
  -> Google quay về callback của Enfyra
  -> Enfyra chuyển hướng qua {yourAppOrigin}/enfyra/auth/set-cookies
  -> phản hồi proxy đặt cookie trên origin của ứng dụng
  -> trình duyệt quay lại URL redirect

Người dùng không tự xử lý token.

Nuxt

Dùng routeRules:

export default defineNuxtConfig({
  routeRules: {
    "/enfyra/**": {
      proxy: {
        to: "https://demo.enfyra.io/api/**",
        fetchOptions: { redirect: "manual" },
      },
    },
    "/socket.io/**": {
      proxy: {
        to: "https://demo.enfyra.io/ws/socket.io/**",
      },
    },
  },
})

redirect: "manual" giữ OAuth và set-cookie redirect dưới app origin.

Next.js

Dùng rewrites():

const nextConfig = {
  async rewrites() {
    return [
      {
        source: "/enfyra/:path*",
        destination: "https://demo.enfyra.io/api/:path*",
      },
      {
        source: "/socket.io/",
        destination: "https://demo.enfyra.io/ws/socket.io/",
      },
    ]
  },
}

export default nextConfig

Với server component, forward incoming cookie khi fetch qua URL app:

import { cookies } from "next/headers"

const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/enfyra/me`, {
  headers: { cookie: (await cookies()).toString() },
  cache: "no-store",
})

Angular

Dùng Angular dev-server proxy ở local và cấu hình route tương tự trong production reverse proxy:

{
  "/enfyra/**": {
    "target": "https://demo.enfyra.io/api",
    "secure": true,
    "changeOrigin": true,
    "pathRewrite": {
      "^/enfyra": ""
    }
  },
  "/socket.io/**": {
    "target": "https://demo.enfyra.io/ws",
    "secure": true,
    "changeOrigin": true,
    "ws": true
  }
}

Gắn proxy file vào Angular serve target:

{
  "projects": {
    "your-app": {
      "architect": {
        "serve": {
          "options": {
            "proxyConfig": "src/proxy.conf.json"
          }
        }
      }
    }
  }
}

Dùng HTTP interceptor để mọi Enfyra request gửi cookie:

import { ApplicationConfig, inject } from "@angular/core"
import { HttpInterceptorFn, provideHttpClient, withInterceptors } from "@angular/common/http"
import { CanActivateFn, provideRouter, Router } from "@angular/router"
import { catchError, map, of } from "rxjs"

import { routes } from "./app.routes"
import { EnfyraAuthService } from "./enfyra-auth.service"

export const enfyraCredentialsInterceptor: HttpInterceptorFn = (req, next) => {
  if (!req.url.startsWith("/enfyra/")) return next(req)
  return next(req.clone({ withCredentials: true }))
}

export const requireUserGuard: CanActivateFn = () => {
  const auth = inject(EnfyraAuthService)
  const router = inject(Router)

  return auth.loadMe().pipe(
    map(user => user ? true : router.createUrlTree(["/login"])),
    catchError(() => of(router.createUrlTree(["/login"]))),
  )
}

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([enfyraCredentialsInterceptor])),
    provideRouter(routes),
  ],
}

Tạo auth service nhỏ xoay quanh /enfyra/login, /enfyra/me, /enfyra/logout; sau khi auth đã biết, tạo một Socket.IO connection trong app-level service. Route guard chỉ bảo vệ browser navigation; Enfyra route permission, guard và owner check vẫn phải nằm ở server.

Ví dụ khởi tạo OAuth trong Angular service:

import { Injectable, signal } from "@angular/core"
import { HttpClient } from "@angular/common/http"
import { Observable, switchMap, tap } from "rxjs"

type EnfyraUser = {
  id: string | number
  email?: string
}

@Injectable({ providedIn: "root" })
export class EnfyraAuthService {
  readonly user = signal<EnfyraUser | null>(null)

  constructor(private readonly http: HttpClient) {}

  login(email: string, password: string): Observable<EnfyraUser | null> {
    return this.http.post("/enfyra/login", { email, password, remember: true }).pipe(
      switchMap(() => this.loadMe()),
    )
  }

  loadMe(): Observable<EnfyraUser | null> {
    return this.http.get<EnfyraUser | null>("/enfyra/me").pipe(
      tap(user => this.user.set(user)),
    )
  }

  logout(): Observable<unknown> {
    return this.http.post("/enfyra/logout", {}).pipe(
      tap(() => this.user.set(null)),
    )
  }

  startGoogleOAuth(returnPath = "/") {
    const redirect = new URL(returnPath, window.location.origin)
    const url = new URL("/api/auth/google", "https://demo.enfyra.io")
    url.searchParams.set("redirect", redirect.toString())
    url.searchParams.set("cookieBridgePrefix", "/enfyra")
    window.location.href = url.toString()
  }
}
import { computed, effect, Injectable, signal } from "@angular/core"
import { io, type Socket } from "socket.io-client"

import { EnfyraAuthService } from "./enfyra-auth.service"

@Injectable({ providedIn: "root" })
export class EnfyraRealtimeService {
  private socket: Socket | null = null
  private readonly connected = signal(false)
  readonly isConnected = computed(() => this.connected())

  constructor(private readonly auth: EnfyraAuthService) {
    effect(() => {
      const user = this.auth.user()
      if (user) this.connect()
      else this.disconnect()
    })
  }

  connect() {
    if (this.socket) return this.socket

    this.socket = io("/chat", {
      path: "/socket.io",
      withCredentials: true,
      reconnection: true,
      reconnectionAttempts: Infinity,
      reconnectionDelay: 2000,
      reconnectionDelayMax: 30000,
      transports: ["polling"],
      upgrade: false,
    })

    this.socket.on("connect", () => this.connected.set(true))
    this.socket.on("disconnect", () => this.connected.set(false))
    return this.socket
  }

  disconnect() {
    this.socket?.disconnect()
    this.socket = null
    this.connected.set(false)
  }

  onMessage(handler: (event: unknown) => void) {
    const activeSocket = this.connect()
    activeSocket.on("chat:message", handler)
    return () => activeSocket.off("chat:message", handler)
  }
}

SvelteKit

Dùng server hook cho REST/auth prefix:

import type { Handle } from "@sveltejs/kit"

const ENFYRA_API = "https://demo.enfyra.io/api"

export const handle: Handle = async ({ event, resolve }) => {
  if (!event.url.pathname.startsWith("/enfyra/")) {
    return resolve(event)
  }

  const upstream = new URL(
    event.url.pathname.replace(/^\/enfyra/, ""),
    ENFYRA_API,
  )
  upstream.search = event.url.search

  return fetch(upstream, {
    method: event.request.method,
    headers: event.request.headers,
    body:
      event.request.method === "GET" || event.request.method === "HEAD"
        ? undefined
        : event.request.body,
    redirect: "manual",
  })
}

Cấu hình deployment proxy để chuyển /socket.io/** tới https://demo.enfyra.io/ws/socket.io/**.

Remix

Tạo catch-all resource route cho /enfyra/*:

import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"

const ENFYRA_API = "https://demo.enfyra.io/api"

async function proxy(request: Request, params: { "*": string }) {
  const url = new URL(request.url)
  const upstream = new URL(`/${params["*"] ?? ""}`, ENFYRA_API)
  upstream.search = url.search

  return fetch(upstream, {
    method: request.method,
    headers: request.headers,
    body:
      request.method === "GET" || request.method === "HEAD"
        ? undefined
        : request.body,
    redirect: "manual",
  })
}

export const loader = ({ request, params }: LoaderFunctionArgs) =>
  proxy(request, params)

export const action = ({ request, params }: ActionFunctionArgs) =>
  proxy(request, params)

Cấu hình hosting proxy để chuyển /socket.io/** tới https://demo.enfyra.io/ws/socket.io/**.

Lỗi thường gặp

Thiếu cookieBridgePrefix

Proxy prefix /enfyra thì OAuth phải có:

cookieBridgePrefix=/enfyra

Thiếu nó, Enfyra dùng cookie bridge /api mặc định, đúng cho Enfyra app nhưng sai với third-party app dùng /enfyra.

Bắt đầu OAuth sai origin

Demo chat app production bắt đầu OAuth trên Enfyra app URL:

new URL("/api/auth/google", "https://demo.enfyra.io")

Không bắt đầu Google OAuth tại /enfyra/auth/google. Local /enfyra/** proxy vẫn cần cho bridge /enfyra/auth/set-cookies về sau.

Browser code gọi backend host

Tránh:

fetch("https://raw-server.example.com/project_task")

Dùng:

fetch("/enfyra/project_task", { credentials: "include" })

Kết nối /ws/chat

Đừng coi Enfyra realtime là raw WebSocket path. Dùng Socket.IO namespace và transport path:

io("/chat", { path: "/socket.io", withCredentials: true })

Bước tiếp theo

Sau khi tích hợp framework, xây feature thực với Ví dụ third-party chat app.