0.1.3Updated 10 days ago
import { v1 } from 'jsr:@std/uuid';
import { deleteCookie, getCookies, setCookie } from "jsr:@std/http";

import { MemCache } from "https://viapak.xyz/@utils/memcache";
import { logger } from "https://viapak.xyz/@utils/logger";

import { RequestManager } from "../modules/request_manager.ts";
import { User } from "./user.ts";

export class Draco {
  private static Cache = new MemCache<Draco.User>();

  private static _HOST: string
  static get HOST() { return this._HOST }

  private static _API_KEY: string
  static get API_KEY() { return this._API_KEY }

  private static _PERMISSIONS: Record<string, any>
  static get Permissions() { return this._PERMISSIONS }

  private static _COOKIE_NAME: string
  static get COOKIE_NAME() { return this._COOKIE_NAME }

  private static _initialized = false

  static readonly RequestManager = new RequestManager(this);

  static SendRequest<T = any>(...params: Parameters<RequestManager['Send']>) {
    if(!this._initialized) throw new Error('Draco not configured. [Draco.Configure()]');

    return this.RequestManager.Send<T>(...params);
  }

  static async Configure(options: Draco.Options) {
    this.log("Configured");

    this._HOST = options.host || Deno.env.get('DRACO_HOST') || Deno.env.get('DRACO_URL')!;
    this._API_KEY = options.api_key || Deno.env.get('DRACO_API_KEY')!;
    this._PERMISSIONS = Object.freeze(options.permissions);
    this._COOKIE_NAME = options.cookie_name || Deno.env.get("DRACO_COOKIE") || "draco_uuid";

    if(!this.HOST) throw new Error('Draco host not set');
    if(!this.API_KEY) throw new Error('Draco api key not set');

    this._initialized = true;

    await this.AnnouncePermissions();
  }

  static async AnnouncePermissions() {
    const ok = (await this.SendRequest("announce-permissions", {
      permissions: Object.values(this._PERMISSIONS)
    })) !== null;

    if(ok) {
      this.log("%cConnected", 'color: green');
      this.log(`Permissions Synchronized`);
    }

    return ok;
  }

  static LogoutRedirect() {
    const headers = new Headers({
      Location: "/"
    });

    deleteCookie(headers, this.COOKIE_NAME);
    setCookie(headers, {
      name: this.COOKIE_NAME,
      value: v1.generate()
    })

    return new Response(null, {
      headers,
      status: 307
    })
  }

  static async LoginRedirect(cookie_uuid: Draco.Login.Request['cookie_uuid'], redirect_url: Draco.Login.Request['redirect_url']) {
    const response = await this.SendRequest<Draco.Login.Response>("login-request", { cookie_uuid, redirect_url });

    const user_redirect_url = response?.redirect_url;
    return new Response(null, {
      headers: {
        Location: user_redirect_url || '/'
      },
      status: 307
    })
  }

  static GetCookie(request: Request) {
    return getCookies(request.headers)[this.COOKIE_NAME] || false;
  }

  /**
   * Returns a `Response` to initialize a new Draco cookie.
   * 
   * **WARNING**: Only use if `GetCookie` returns false!
   */
  static CookieRedirect(request: Request) {
    const url = new URL(request.url);

    const cookie = getCookies(request.headers)[this.COOKIE_NAME];

    const headers = new Headers({
      Location: request.url
    });

    if(!cookie) {
      setCookie(headers, {
        name: this.COOKIE_NAME,
        value: v1.generate(),
        path: "/",
        domain: url.hostname,
        expires: Date.now() + TWO_WEEKS,
        httpOnly: true,
        secure: /^https/.test(url.protocol),
      })
    }

    return new Response(null, {
      headers,
      status: 307
    })
  }

  static async GetUser(cookie_uuid?: string, force?: boolean): Promise<Draco.User.Class | null>
  static async GetUser(request: Request, force?: boolean): Promise<Draco.User.Class | null>
  static async GetUser(cookie_or_request: string | undefined | Request, force?: boolean) {
    let cookie_uuid: string | undefined;
    if(cookie_or_request instanceof Request) {
      cookie_uuid = this.GetCookie(cookie_or_request) || undefined;
    } else cookie_uuid = cookie_or_request;

    if(!cookie_uuid) return null;

    if(this.Cache.has(cookie_uuid) && !force) return new User(this.Cache.get(cookie_uuid)!);

    const { user } = (await this.SendRequest<{ user: Draco.User }>('refresh-token', { cookie_uuid })) ?? {};
    if(!user) return null;

    if(user?.image_path?.indexOf('/') == 0) {
      user.image_path = this.HOST.replace(/\/+$/, '') + user.image_path;
    }

    this.Cache.set(cookie_uuid, user);

    return new User(user);
  }

  static async LogEvent(cookie: string | undefined, event: Draco.Event): Promise<boolean>
  static async LogEvent(request: Request, event: Draco.Event): Promise<boolean>
  static async LogEvent(cookie_or_request: string | undefined | Request, event: Draco.Event): Promise<boolean> {
    let cookie_uuid: string | undefined;
    if(cookie_or_request instanceof Request) {
      cookie_uuid = this.GetCookie(cookie_or_request) || undefined;
    } else cookie_uuid = cookie_or_request;

    if(!cookie_uuid) return false;

    const success = (await this.SendRequest("log-event", {
      cookie_uuid,
      event: event.type,
      information: event.information,
      ip_address: event.ip_address,
    } satisfies Draco.Event.Request)) !== null;

    return success;
  }

  static log = logger({
    glyph: () => this._initialized ? '' : '',
    glyph_color: "#00aaff",
    name: "Draco",
    name_color: "#00aaff",
  })
}

const TWO_WEEKS = 1000 * 60 * 60 * 24 * 14;