0.1.6Updated a month ago
import { randomUUID } from "node:crypto";
import * as cookie from "https://deno.land/std@0.224.0/http/cookie.ts";

import { RequestAnalytics } from "@infinity-beyond/modules/analytics/request_analytics.ts";
import { ResponseAnalytics } from "@infinity-beyond/modules/analytics/response_analytics.ts";
import { Entity } from "@infinity-beyond/classes/data_types/entity/mod.ts";
import State from "@infinity-beyond/modules/state.ts";
import { CustomEventEmitter } from "@infinity-beyond/classes/custom_event_emitter.ts";
import InfinityHeaders from "@infinity-beyond/modules/networking/infinity_headers.ts";
import { LoginRequest } from "@infinity-beyond/modules/security/draco/login_request.ts";
import { GetDracoUser } from "@infinity-beyond/modules/security/draco/get_draco_user.ts";
import { DefaultPermissions } from "@infinity-beyond/modules/security/default_permissions.ts";

import { freshHandler } from "@infinity-beyond/modules/networking/server.ts";

import type { ResolvedFreshConfig } from "$fresh/server.ts";

import { AnnouncePermissions } from "@infinity-beyond/modules/security/draco/announce_permissions.ts";
import { FetchInfinityClasses } from "@infinity-beyond/modules/delegation/entity_ref.ts";
import { CircleAlert } from "lucide-preact";
import type { ServeHandlerInfo } from "$fresh/src/server/types.ts";
import { prepareApplication } from "@infinity-beyond/modules/distribution/prepare_application.ts";
import { existsSync } from "jsr:@std/fs";
import { join } from "jsr:@std/path";

import { plural } from "https://deno.land/x/deno_plural@2.0.0/mod.ts";
import { create_fresh_context } from "@infinity-beyond/modules/networking/context.ts";

const COOKIE_NAME = `${(Deno.env.get('INFINITY_APP')?.toLowerCase().replace(/\s+/g, '-').replace(/_/g, '-') || 'Infinity')}-session`;

export class Infinity extends CustomEventEmitter<Infinity.Events> {
  readonly app_name: string

  constructor(app_name: string) {
    super();

    this.app_name = app_name;

    State.Application = this;
  }

  private fresh_handler!: ((req: Request, connInfo?: ServeHandlerInfo) => Promise<Response>);
  private ctx_config!: ResolvedFreshConfig;

  async listen({ port: PORT }: Infinity.ListenOptions = {}) {
    const port = Number(Deno.env.get('PORT')) || PORT || 9090;

    State.Entities = await FetchInfinityClasses();

    Infinity.announce();

    if(!existsSync(join(Deno.cwd(), 'app/islands/utils'))) await prepareApplication();

    const [fresh_handler, ctx_config] = await freshHandler({
      dev: Deno.args.includes('dev')
    });

    this.ctx_config = ctx_config;
    this.fresh_handler = fresh_handler;

    Deno.serve({
      port,
      onListen: () => {
        console.log(`♾️  Infinity Beyond listening at http://localhost:${port}/`);
      }
    }, this.handler.bind(this));
  }

  private async handler(request: Request, info: Deno.ServeHandlerInfo<Deno.NetAddr>) {
    request.state = {} as Infinity.Context;

    const { pathname } = new URL(request.url);

    switch(pathname) {
      case '/ping': return new Response('pong');
      case '/__worker__/handshake': return Infinity.handshake(request);
    }

    request.state.uuid = randomUUID();
    request.state.start = new Date();

    if(pathname.startsWith('/_frsh')) return this.fresh_handler(request, info);
    if(pathname.match(/\.(?:css|svg|png|jpg)$/)) return this.fresh_handler(request, info);

    const IS_EXTERNAL_API = /\/api\/v\d+\//.test(pathname);
    if(IS_EXTERNAL_API) {
      const slug = pathname.replace(/^.*\/api\/v\d+\/(\w+)\/?.*$/, '$1');
      const entity = State.Entities.values().find(e => plural(e.slug) == slug);

      const ctx = create_fresh_context(this.ctx_config, request, info);

      const handler = entity?.REST?.handler;

      if(handler) return handler(request, ctx as any) || Response.json({}, { status: 404 });
    };

    return this.fresh_handler(request, info);
  }

  static async Cookie(request: Request, ctx: InfinityContext) {
    ctx.state ||= request.state;

    const cookie_uuid = cookie.getCookies(request.headers)[COOKIE_NAME] as string | undefined;
    if(cookie_uuid) {
      request.state.cookie_uuid = ctx.state.cookie_uuid = cookie_uuid;
      request.state.cookie_existed = ctx.state.cookie_existed = true;
    } else {
      request.state.cookie_uuid = ctx.state.cookie_uuid = randomUUID();
      request.state.cookie_existed = ctx.state.cookie_existed = false;
    }
  }

  static async Middleware(request: Request, ctx: InfinityContext) {
    const { pathname } = new URL(request.url);

    ctx.state = request.state;

    this.Cookie(request, ctx);

    await Infinity.draco_setup(ctx);
    await Infinity.notification_setup(ctx.state);

    // Special requests
    switch(pathname) {
      case '/logout':
        return Infinity.logout();
      case '/login': {
        if(!request.state.user) return Infinity.login(request);
        return new Response(undefined, {
          status: 307,
          headers: {
            Location: '/'
          }
        })
      }
    }

    await RequestAnalytics.Process(request);

    const response = await ctx.next();

    await ResponseAnalytics.Process(request, response);

    InfinityHeaders.apply(response.headers, {
      'x-request-id': request.state.uuid,
      'x-time-taken': `${Date.now() - request.state.start.getTime()}`,
    }, InfinityHeaders.Default);

    if(!request.state.cookie_existed) {
      cookie.setCookie(response.headers, {
        name: COOKIE_NAME,
        value: request.state.cookie_uuid,
        maxAge: 60 * 60 * 24 * 7, // 7 days
        secure: new URL(request.url).protocol == 'https:',
      });
    }

    return response;
  }

  private static handshake(request: Request) {
    const signature = request.headers.get('sec-websocket-protocol')?.replace(/^SIGNATURE, (.+$)$/, '$1');

    if(!signature || signature !== State.SIGNATURE) {
      return new Response('Invalid signature', { status: 403 });
    }

    const { socket, response } = Deno.upgradeWebSocket(request);

    const query: Record<string, string> = new URL(request.url).search
      ?.replace(/^\?/, '')
      .split('&')
      .reduce((p, query) => {
        const [key, value] = query.split('=');
        p[key] = value;
        return p;
      }, {} as Record<string, string>) ?? {};

    const { uuid } = query;
    if(!uuid) {
      socket.close(1, 'No provided uuid');
      return new Response('', { status: 403 });
    }

    socket.onopen = () => {
      Entity.__AddWorker(uuid, socket);
    }

    return response;
  }

  private static async draco_setup({ state }: { state: Infinity.Context }) {
    state.user = await GetDracoUser(state.cookie_uuid);
  }

  private static async notification_setup(state: Infinity.Context) {
    state.notifications = [{
      id: 1,
      title: "Testing",
      body: "This is a test notification",
      read: false,
    }];
  }

  private static async login(request: Request) {
    return await LoginRequest(request.state.cookie_uuid);
  }

  private static async logout() {
    const response = new Response(undefined, {
      status: 307,
      headers: {
        Location: '/',
      }
    });

    cookie.deleteCookie(response.headers, COOKIE_NAME);

    return response;
  }

  static get Permissions() {
    return DefaultPermissions;
  }

  private static HAS_ANNOUNCED = false;
  private static announce() {
    if(this.HAS_ANNOUNCED) return;
    this.HAS_ANNOUNCED = true;

    if(State.IS_BUILDING) return;

    const EntityPermissions: string[] = [];

    Object.values(State.Entities).forEach(entity => {
      if(entity instanceof Entity) {
        EntityPermissions.push(...Object.keys((entity as any).__permissions));
      }
    });

    AnnouncePermissions(EntityPermissions);
  }
}