diff --git a/web-plugin/index.js b/web-plugin/index.js index 60726c8..47e71d3 100644 --- a/web-plugin/index.js +++ b/web-plugin/index.js @@ -120,7 +120,7 @@ function addButton() { const graph = app.graph; const snapshot = await fetch("/snapshot/get_current").then((x) => x.json()); - console.log(snapshot); + // console.log(snapshot); if (!snapshot) { showError( @@ -154,7 +154,7 @@ function addButton() { const deployMetaNode = deployMeta[0]; - console.log(deployMetaNode); + // console.log(deployMetaNode); const workflow_name = deployMetaNode.widgets[0].value; const workflow_id = deployMetaNode.widgets[1].value; @@ -168,20 +168,23 @@ function addButton() { // const endpoint = localStorage.getItem("endpoint") ?? ""; // const apiKey = localStorage.getItem("apiKey"); - const { endpoint, apiKey } = getData(); + const { endpoint, apiKey, displayName } = getData(); if (!endpoint || !apiKey || apiKey === "" || endpoint === "") { configDialog.show(); return; } - const ok = await confirmDialog.confirm("Confirm deployment", "A new version will be deployed, are you conform?") + const ok = await confirmDialog.confirm( + "Confirm deployment -> " + displayName, + "A new version will be deployed, are you conform?", + ); if (!ok) return; title.innerText = "Deploying..."; title.style.color = "orange"; - console.log(prompt); + // console.log(prompt); // TODO trim the ending / from endpoint is there is if (endpoint.endsWith("/")) { @@ -191,15 +194,17 @@ function addButton() { const apiRoute = endpoint + "/api/upload"; // const userId = apiKey try { + const body = { + workflow_name, + workflow_id, + workflow: prompt.workflow, + workflow_api: prompt.output, + snapshot: snapshot, + }; + console.log(body); let data = await fetch(apiRoute, { method: "POST", - body: JSON.stringify({ - workflow_name, - workflow_id, - workflow: prompt.workflow, - workflow_api: prompt.output, - snapshot: snapshot, - }), + body: JSON.stringify(body), headers: { "Content-Type": "application/json", Authorization: "Bearer " + apiKey, @@ -301,6 +306,17 @@ export class InfoDialog extends ComfyDialog { this.element.style.display = "flex"; this.element.style.zIndex = 1001; } + + showMessage(title, message) { + this.show(` +
+

${title}

+ +
+ `); + } } export class InputDialog extends InfoDialog { @@ -367,7 +383,6 @@ export class InputDialog extends InfoDialog { } } - export class ConfirmDialog extends InfoDialog { callback = undefined; @@ -456,6 +471,8 @@ function getData(environment) { export class ConfigDialog extends ComfyDialog { container = null; + poll = null; + timeout = null; constructor() { super(); @@ -498,17 +515,22 @@ export class ConfigDialog extends ComfyDialog { close() { this.element.style.display = "none"; + clearInterval(this.poll); + clearTimeout(this.timeout); } - save() { + save(api_key, displayName) { + if (!displayName) displayName = getData().displayName; + const deployOption = this.container.querySelector("#deployOption").value; localStorage.setItem("comfy_deploy_env", deployOption); const endpoint = this.container.querySelector("#endpoint").value; - const apiKey = this.container.querySelector("#apiKey").value; + const apiKey = api_key ?? this.container.querySelector("#apiKey").value; const data = { endpoint, apiKey, + displayName, }; localStorage.setItem( "comfy_deploy_env_data_" + deployOption, @@ -527,12 +549,8 @@ export class ConfigDialog extends ComfyDialog {

Comfy Deploy Config

`; + const button = this.container.querySelector("#loginButton"); + button.onclick = () => { + const uuid = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + window.open(data.endpoint + "/auth-request/" + uuid, "_blank"); + + this.timeout = setTimeout(() => { + clearInterval(poll); + infoDialog.showMessage( + "Timeout", + "Wait too long for the response, please try re-login", + ); + }, 30000); // Stop polling after 30 seconds + + this.poll = setInterval(() => { + fetch(data.endpoint + "/api/auth-response/" + uuid) + .then((response) => response.json()) + .then((json) => { + if (json.api_key) { + this.save(json.api_key, json.name); + this.container.querySelector("#apiKey").value = json.api_key; + infoDialog.show(); + clearInterval(this.poll); + clearTimeout(this.timeout); + infoDialog.showMessage( + "Authenticated", + "You will be able to upload workflow to " + json.name, + ); + } + }) + .catch((error) => { + console.error("Error:", error); + clearInterval(this.poll); + clearTimeout(this.timeout); + infoDialog.showMessage("Error", error); + }); + }, 2000); + }; + const apiKeyInput = this.container.querySelector("#apiKey"); - apiKeyInput.addEventListener("paste", function (e) { + apiKeyInput.addEventListener("paste", (e) => { e.stopPropagation(); }); diff --git a/web/bun.lockb b/web/bun.lockb index c5cc14e..650dd43 100755 Binary files a/web/bun.lockb and b/web/bun.lockb differ diff --git a/web/drizzle/0033_awesome_human_fly.sql b/web/drizzle/0033_awesome_human_fly.sql new file mode 100644 index 0000000..4eb44af --- /dev/null +++ b/web/drizzle/0033_awesome_human_fly.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS "comfyui_deploy"."auth_requests" ( + "request_id" text PRIMARY KEY NOT NULL, + "user_id" text, + "org_id" text, + "api_hash" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); diff --git a/web/drizzle/0034_even_lady_ursula.sql b/web/drizzle/0034_even_lady_ursula.sql new file mode 100644 index 0000000..6e48c4a --- /dev/null +++ b/web/drizzle/0034_even_lady_ursula.sql @@ -0,0 +1 @@ +ALTER TABLE "comfyui_deploy"."auth_requests" ADD COLUMN "expired_date" timestamp; \ No newline at end of file diff --git a/web/drizzle/meta/0033_snapshot.json b/web/drizzle/meta/0033_snapshot.json new file mode 100644 index 0000000..5e67823 --- /dev/null +++ b/web/drizzle/meta/0033_snapshot.json @@ -0,0 +1,824 @@ +{ + "id": "97662b25-3992-4859-9bdc-560e2a70daea", + "prevId": "1425ee00-66fb-4541-8da7-19b217944545", + "version": "5", + "dialect": "pg", + "tables": { + "api_keys": { + "name": "api_keys", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_unique": { + "name": "api_keys_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + } + }, + "auth_requests": { + "name": "auth_requests", + "schema": "comfyui_deploy", + "columns": { + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_hash": { + "name": "api_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "deployments": { + "name": "deployments", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_version_id": { + "name": "workflow_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "share_slug": { + "name": "share_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "showcase_media": { + "name": "showcase_media", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "deployment_environment", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "deployments_user_id_users_id_fk": { + "name": "deployments_user_id_users_id_fk", + "tableFrom": "deployments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployments_workflow_version_id_workflow_versions_id_fk": { + "name": "deployments_workflow_version_id_workflow_versions_id_fk", + "tableFrom": "deployments", + "tableTo": "workflow_versions", + "columnsFrom": [ + "workflow_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "deployments_workflow_id_workflows_id_fk": { + "name": "deployments_workflow_id_workflows_id_fk", + "tableFrom": "deployments", + "tableTo": "workflows", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployments_machine_id_machines_id_fk": { + "name": "deployments_machine_id_machines_id_fk", + "tableFrom": "deployments", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployments_share_slug_unique": { + "name": "deployments_share_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "share_slug" + ] + } + } + }, + "machines": { + "name": "machines", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auth_token": { + "name": "auth_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "machine_type", + "primaryKey": false, + "notNull": true, + "default": "'classic'" + }, + "status": { + "name": "status", + "type": "machine_status", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "models": { + "name": "models", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "gpu": { + "name": "gpu", + "type": "machine_gpu", + "primaryKey": false, + "notNull": false + }, + "build_machine_instance_id": { + "name": "build_machine_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_log": { + "name": "build_log", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "machines_user_id_users_id_fk": { + "name": "machines_user_id_users_id_fk", + "tableFrom": "machines", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workflow_run_outputs": { + "name": "workflow_run_outputs", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_outputs_run_id_workflow_runs_id_fk": { + "name": "workflow_run_outputs_run_id_workflow_runs_id_fk", + "tableFrom": "workflow_run_outputs", + "tableTo": "workflow_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workflow_runs": { + "name": "workflow_runs", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workflow_version_id": { + "name": "workflow_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workflow_inputs": { + "name": "workflow_inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "workflow_run_origin", + "primaryKey": false, + "notNull": true, + "default": "'api'" + }, + "status": { + "name": "status", + "type": "workflow_run_status", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_runs_workflow_version_id_workflow_versions_id_fk": { + "name": "workflow_runs_workflow_version_id_workflow_versions_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "workflow_versions", + "columnsFrom": [ + "workflow_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_runs_workflow_id_workflows_id_fk": { + "name": "workflow_runs_workflow_id_workflows_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "workflows", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_runs_machine_id_machines_id_fk": { + "name": "workflow_runs_machine_id_machines_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workflows": { + "name": "workflows", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflows_user_id_users_id_fk": { + "name": "workflows_user_id_users_id_fk", + "tableFrom": "workflows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workflow_versions": { + "name": "workflow_versions", + "schema": "comfyui_deploy", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workflow": { + "name": "workflow", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_api": { + "name": "workflow_api", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_versions_workflow_id_workflows_id_fk": { + "name": "workflow_versions_workflow_id_workflows_id_fk", + "tableFrom": "workflow_versions", + "tableTo": "workflows", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "deployment_environment": { + "name": "deployment_environment", + "values": { + "staging": "staging", + "production": "production", + "public-share": "public-share" + } + }, + "machine_gpu": { + "name": "machine_gpu", + "values": { + "T4": "T4", + "A10G": "A10G", + "A100": "A100" + } + }, + "machine_status": { + "name": "machine_status", + "values": { + "ready": "ready", + "building": "building", + "error": "error" + } + }, + "machine_type": { + "name": "machine_type", + "values": { + "classic": "classic", + "runpod-serverless": "runpod-serverless", + "modal-serverless": "modal-serverless", + "comfy-deploy-serverless": "comfy-deploy-serverless" + } + }, + "workflow_run_origin": { + "name": "workflow_run_origin", + "values": { + "manual": "manual", + "api": "api", + "public-share": "public-share" + } + }, + "workflow_run_status": { + "name": "workflow_run_status", + "values": { + "not-started": "not-started", + "running": "running", + "uploading": "uploading", + "success": "success", + "failed": "failed" + } + } + }, + "schemas": { + "comfyui_deploy": "comfyui_deploy" + }, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/web/drizzle/meta/0034_snapshot.json b/web/drizzle/meta/0034_snapshot.json new file mode 100644 index 0000000..2337aab --- /dev/null +++ b/web/drizzle/meta/0034_snapshot.json @@ -0,0 +1,830 @@ +{ + "id": "8d654f92-7f7e-420f-bbd3-73b6b27adf35", + "prevId": "97662b25-3992-4859-9bdc-560e2a70daea", + "version": "5", + "dialect": "pg", + "tables": { + "api_keys": { + "name": "api_keys", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_unique": { + "name": "api_keys_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + } + }, + "auth_requests": { + "name": "auth_requests", + "schema": "comfyui_deploy", + "columns": { + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_hash": { + "name": "api_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expired_date": { + "name": "expired_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "deployments": { + "name": "deployments", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_version_id": { + "name": "workflow_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "share_slug": { + "name": "share_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "showcase_media": { + "name": "showcase_media", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "deployment_environment", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "deployments_user_id_users_id_fk": { + "name": "deployments_user_id_users_id_fk", + "tableFrom": "deployments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployments_workflow_version_id_workflow_versions_id_fk": { + "name": "deployments_workflow_version_id_workflow_versions_id_fk", + "tableFrom": "deployments", + "tableTo": "workflow_versions", + "columnsFrom": [ + "workflow_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "deployments_workflow_id_workflows_id_fk": { + "name": "deployments_workflow_id_workflows_id_fk", + "tableFrom": "deployments", + "tableTo": "workflows", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployments_machine_id_machines_id_fk": { + "name": "deployments_machine_id_machines_id_fk", + "tableFrom": "deployments", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployments_share_slug_unique": { + "name": "deployments_share_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "share_slug" + ] + } + } + }, + "machines": { + "name": "machines", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auth_token": { + "name": "auth_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "machine_type", + "primaryKey": false, + "notNull": true, + "default": "'classic'" + }, + "status": { + "name": "status", + "type": "machine_status", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "models": { + "name": "models", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "gpu": { + "name": "gpu", + "type": "machine_gpu", + "primaryKey": false, + "notNull": false + }, + "build_machine_instance_id": { + "name": "build_machine_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_log": { + "name": "build_log", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "machines_user_id_users_id_fk": { + "name": "machines_user_id_users_id_fk", + "tableFrom": "machines", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workflow_run_outputs": { + "name": "workflow_run_outputs", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_outputs_run_id_workflow_runs_id_fk": { + "name": "workflow_run_outputs_run_id_workflow_runs_id_fk", + "tableFrom": "workflow_run_outputs", + "tableTo": "workflow_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workflow_runs": { + "name": "workflow_runs", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workflow_version_id": { + "name": "workflow_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workflow_inputs": { + "name": "workflow_inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "workflow_run_origin", + "primaryKey": false, + "notNull": true, + "default": "'api'" + }, + "status": { + "name": "status", + "type": "workflow_run_status", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_runs_workflow_version_id_workflow_versions_id_fk": { + "name": "workflow_runs_workflow_version_id_workflow_versions_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "workflow_versions", + "columnsFrom": [ + "workflow_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_runs_workflow_id_workflows_id_fk": { + "name": "workflow_runs_workflow_id_workflows_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "workflows", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_runs_machine_id_machines_id_fk": { + "name": "workflow_runs_machine_id_machines_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workflows": { + "name": "workflows", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflows_user_id_users_id_fk": { + "name": "workflows_user_id_users_id_fk", + "tableFrom": "workflows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workflow_versions": { + "name": "workflow_versions", + "schema": "comfyui_deploy", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workflow": { + "name": "workflow", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_api": { + "name": "workflow_api", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_versions_workflow_id_workflows_id_fk": { + "name": "workflow_versions_workflow_id_workflows_id_fk", + "tableFrom": "workflow_versions", + "tableTo": "workflows", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "deployment_environment": { + "name": "deployment_environment", + "values": { + "staging": "staging", + "production": "production", + "public-share": "public-share" + } + }, + "machine_gpu": { + "name": "machine_gpu", + "values": { + "T4": "T4", + "A10G": "A10G", + "A100": "A100" + } + }, + "machine_status": { + "name": "machine_status", + "values": { + "ready": "ready", + "building": "building", + "error": "error" + } + }, + "machine_type": { + "name": "machine_type", + "values": { + "classic": "classic", + "runpod-serverless": "runpod-serverless", + "modal-serverless": "modal-serverless", + "comfy-deploy-serverless": "comfy-deploy-serverless" + } + }, + "workflow_run_origin": { + "name": "workflow_run_origin", + "values": { + "manual": "manual", + "api": "api", + "public-share": "public-share" + } + }, + "workflow_run_status": { + "name": "workflow_run_status", + "values": { + "not-started": "not-started", + "running": "running", + "uploading": "uploading", + "success": "success", + "failed": "failed" + } + } + }, + "schemas": { + "comfyui_deploy": "comfyui_deploy" + }, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/web/drizzle/meta/_journal.json b/web/drizzle/meta/_journal.json index a262f43..3fbae3d 100644 --- a/web/drizzle/meta/_journal.json +++ b/web/drizzle/meta/_journal.json @@ -232,6 +232,20 @@ "when": 1705806921697, "tag": "0032_shallow_vermin", "breakpoints": true + }, + { + "idx": 33, + "version": "5", + "when": 1705853314500, + "tag": "0033_awesome_human_fly", + "breakpoints": true + }, + { + "idx": 34, + "version": "5", + "when": 1705902960991, + "tag": "0034_even_lady_ursula", + "breakpoints": true } ] } \ No newline at end of file diff --git a/web/migrate.mts b/web/migrate.mts index 5f4df12..972740d 100644 --- a/web/migrate.mts +++ b/web/migrate.mts @@ -14,8 +14,12 @@ if (sslMode === "false") sslMode = false; let connectionString = process.env.POSTGRES_URL!; -const isDevContainer = process.env.VSCODE_DEV_CONTAINER !== undefined; -if (isDevContainer) connectionString = connectionString.replace("localhost","host.docker.internal") +const isDevContainer = process.env.REMOTE_CONTAINERS !== undefined; +if (isDevContainer) + connectionString = connectionString.replace( + "localhost", + "host.docker.internal", + ); const sql = postgres(connectionString, { max: 1, ssl: sslMode as any }); const db = drizzle(sql, { @@ -23,16 +27,16 @@ const db = drizzle(sql, { }); let retries = 5; -while(retries) { +while (retries) { try { await sql`SELECT NOW()`; - console.log('Database is live'); + console.log("Database is live"); break; } catch (error) { - console.error('Database is not live yet', error); + console.error("Database is not live yet", error); retries -= 1; console.log(`Retries left: ${retries}`); - await new Promise(res => setTimeout(res, 1000)); + await new Promise((res) => setTimeout(res, 1000)); } } diff --git a/web/package.json b/web/package.json index 588cbb8..faf351d 100644 --- a/web/package.json +++ b/web/package.json @@ -72,6 +72,7 @@ "mdx-annotations": "^0.1.4", "million": "latest", "mitata": "^0.1.6", + "ms": "^2.1.3", "nanoid": "^5.0.4", "next": "14.1", "next-plausible": "^3.12.0", diff --git a/web/src/app/(app)/api/[[...routes]]/route.ts b/web/src/app/(app)/api/[[...routes]]/route.ts index ea42f63..e8c7cdd 100644 --- a/web/src/app/(app)/api/[[...routes]]/route.ts +++ b/web/src/app/(app)/api/[[...routes]]/route.ts @@ -1,4 +1,3 @@ -import { app } from "../../../../routes/app"; import { registerCreateRunRoute } from "@/routes/registerCreateRunRoute"; import { registerGetOutputRoute } from "@/routes/registerGetOutputRoute"; import { registerUploadRoute } from "@/routes/registerUploadRoute"; @@ -6,6 +5,9 @@ import { isKeyRevoked } from "@/server/curdApiKeys"; import { parseJWT } from "@/server/parseJWT"; import type { Context, Next } from "hono"; import { handle } from "hono/vercel"; +import { app } from "../../../../routes/app"; +import { registerWorkflowUploadRoute } from "@/routes/registerWorkflowUploadRoute"; +import { registerGetAuthResponse } from "@/routes/registerGetAuthResponse"; export const dynamic = "force-dynamic"; export const maxDuration = 300; // 5 minutes @@ -21,7 +23,10 @@ async function checkAuth(c: Context, next: Next) { const userData = token ? parseJWT(token) : undefined; if (!userData || token === undefined) { return c.text("Invalid or expired token", 401); - } else { + } + + // If the key has expiration, this is a temporary key and not in our db, so we can skip checking + if (userData.exp === undefined) { const revokedKey = await isKeyRevoked(token); if (revokedKey) return c.text("Revoked token", 401); } @@ -31,18 +36,20 @@ async function checkAuth(c: Context, next: Next) { await next(); } -app.use("/run", async (c, next) => { - return checkAuth(c, next); -}); - -app.use("/upload-url", async (c, next) => { - return checkAuth(c, next); -}); +app.use("/run", checkAuth); +app.use("/upload-url", checkAuth); +app.use("/upload-workflow", checkAuth); +// create run endpoint registerCreateRunRoute(app); registerGetOutputRoute(app); + +// file upload endpoint registerUploadRoute(app); +registerWorkflowUploadRoute(app); +registerGetAuthResponse(app); + // The OpenAPI documentation will be available at /doc app.doc("/doc", { openapi: "3.0.0", diff --git a/web/src/app/(app)/api/file-upload/route.ts b/web/src/app/(app)/api/file-upload/route.ts index 8f5ba7b..f132181 100644 --- a/web/src/app/(app)/api/file-upload/route.ts +++ b/web/src/app/(app)/api/file-upload/route.ts @@ -1,11 +1,12 @@ -import { parseDataSafe } from "../../../../lib/parseDataSafe"; import { handleResourceUpload } from "@/server/resource"; import { NextResponse } from "next/server"; import { z } from "zod"; +import { parseDataSafe } from "../../../../lib/parseDataSafe"; const Request = z.object({ file_name: z.string(), run_id: z.string(), + type: z.string(), }); @@ -29,7 +30,7 @@ export async function GET(request: Request) { { url: uploadUrl, }, - { status: 200 } + { status: 200 }, ); } catch (error: unknown) { const errorMessage = @@ -38,7 +39,7 @@ export async function GET(request: Request) { { error: errorMessage, }, - { status: 500 } + { status: 500 }, ); } } diff --git a/web/src/app/(app)/api/upload/route.ts b/web/src/app/(app)/api/upload/route.ts index 932169b..2a98b40 100644 --- a/web/src/app/(app)/api/upload/route.ts +++ b/web/src/app/(app)/api/upload/route.ts @@ -1,17 +1,14 @@ -import { createNewWorkflow } from "../../../../server/createNewWorkflow"; -import { parseJWT } from "../../../../server/parseJWT"; -import { db } from "@/db/db"; -import { - snapshotType, - workflowAPIType, - workflowTable, - workflowType, - workflowVersionTable, -} from "@/db/schema"; +import { snapshotType, workflowAPIType, workflowType } from "@/db/schema"; import { parseDataSafe } from "@/lib/parseDataSafe"; -import { eq, sql } from "drizzle-orm"; import { NextResponse } from "next/server"; import { z } from "zod"; +import { + createNewWorkflow, + createNewWorkflowVersion, +} from "../../../../server/createNewWorkflow"; +import { parseJWT } from "../../../../server/parseJWT"; + +// This is will be deprecated const corsHeaders = { "Access-Control-Allow-Origin": "*", @@ -55,7 +52,7 @@ export async function POST(request: Request) { const [data, error] = await parseDataSafe( UploadRequest, request, - corsHeaders + corsHeaders, ); if (!data || error) return error; @@ -75,7 +72,7 @@ export async function POST(request: Request) { // Case 1 new workflow try { - if ((!workflow_id || workflow_id.length == 0) && workflow_name) { + if ((!workflow_id || workflow_id.length === 0) && workflow_name) { // Create a new parent workflow const { workflow_id: _workflow_id, version: _version } = await createNewWorkflow({ @@ -91,56 +88,17 @@ export async function POST(request: Request) { workflow_id = _workflow_id; version = _version; - - // const workflow_parent = await db - // .insert(workflowTable) - // .values({ - // user_id, - // name: workflow_name, - // org_id: org_id, - // }) - // .returning(); - - // workflow_id = workflow_parent[0].id; - - // // Create a new version - // const data = await db - // .insert(workflowVersionTable) - // .values({ - // workflow_id: workflow_id, - // workflow, - // workflow_api, - // version: 1, - // snapshot: snapshot, - // }) - // .returning(); - // version = data[0].version; } else if (workflow_id) { // Case 2 update workflow - const data = await db - .insert(workflowVersionTable) - .values({ - workflow_id, - workflow: workflow, + const { version: _version } = await createNewWorkflowVersion({ + workflow_id: workflow_id, + workflowData: { + workflow, workflow_api, - // version: sql`${workflowVersionTable.version} + 1`, - snapshot: snapshot, - version: sql`( - SELECT COALESCE(MAX(version), 0) + 1 - FROM ${workflowVersionTable} - WHERE workflow_id = ${workflow_id} - )`, - }) - .returning(); - version = data[0].version; - - await db - .update(workflowTable) - .set({ - updated_at: new Date(), - }) - .where(eq(workflowTable.id, workflow_id)) - .returning(); + snapshot, + }, + }); + version = _version; } else { return NextResponse.json( { @@ -150,7 +108,7 @@ export async function POST(request: Request) { status: 500, statusText: "Invalid request", headers: corsHeaders, - } + }, ); } } catch (error: any) { @@ -162,7 +120,7 @@ export async function POST(request: Request) { status: 500, statusText: "Invalid request", headers: corsHeaders, - } + }, ); } @@ -174,6 +132,6 @@ export async function POST(request: Request) { { status: 200, headers: corsHeaders, - } + }, ); } diff --git a/web/src/app/(app)/auth-request/[request_id]/page.tsx b/web/src/app/(app)/auth-request/[request_id]/page.tsx new file mode 100644 index 0000000..a419b96 --- /dev/null +++ b/web/src/app/(app)/auth-request/[request_id]/page.tsx @@ -0,0 +1,54 @@ +import { ButtonAction } from "@/components/ButtonActionLoader"; +import { Button } from "@/components/ui/button"; +import { createAuthRequest } from "@/server/curdApiKeys"; +import { auth } from "@clerk/nextjs"; +import { redirect } from "next/navigation"; +import { getOrgOrUserDisplayName } from "../../../../server/getOrgOrUserDisplayName"; +import { db } from "@/db/db"; +import { eq } from "drizzle-orm"; +import { authRequestsTable } from "@/db/schema"; + +export default async function Home({ + params, +}: { + params: { request_id: string }; +}) { + const { userId, orgId } = await auth(); + + if (!userId) redirect("/"); + + if (!params.request_id) + return ( +
+ No valid request_id +
+ ); + + const existingResult = await db.query.authRequestsTable.findFirst({ + where: eq(authRequestsTable.request_id, params.request_id), + }); + + if (existingResult?.api_hash) { + return ( +
+ Request already consumed. +
+ ); + } + + const userName = await getOrgOrUserDisplayName(orgId, userId); + + return ( +
+
Grant API Access to {userName}
+ +
+ ); +} diff --git a/web/src/components/ButtonActionLoader.tsx b/web/src/components/ButtonActionLoader.tsx index 568e87d..5edb68e 100644 --- a/web/src/components/ButtonActionLoader.tsx +++ b/web/src/components/ButtonActionLoader.tsx @@ -4,10 +4,10 @@ import { LoadingIcon } from "@/components/LoadingIcon"; import { callServerPromise } from "@/components/callServerPromise"; import { Button } from "@/components/ui/button"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useAuth, useClerk } from "@clerk/nextjs"; import { MoreVertical } from "lucide-react"; @@ -15,80 +15,79 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; export function ButtonAction({ - action, - children, - routerAction = "back", - ...rest + action, + children, + routerAction = "back", + ...rest }: { - action: () => Promise; - routerAction?: "refresh" | "back"; - children: React.ReactNode; + action: () => Promise; + routerAction?: "refresh" | "back" | "do-nothing"; + children: React.ReactNode; }) { - const [pending, setPending] = useState(false); - const router = useRouter(); + const [pending, setPending] = useState(false); + const router = useRouter(); - return ( - - ); + } else if (routerAction === "refresh") router.refresh(); + }} + {...rest} + > + {children} {pending && } + + ); } export function ButtonActionMenu(props: { - title?: string; - actions: { - title: string; - action: () => Promise; - }[]; + title?: string; + actions: { + title: string; + action: () => Promise; + }[]; }) { - const user = useAuth(); - const [isLoading, setIsLoading] = useState(false); - const clerk = useClerk(); + const user = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const clerk = useClerk(); - return ( - - - - - - {props.actions.map((action) => ( - { - if (!user.isSignedIn) { - clerk.openSignIn({ - redirectUrl: window.location.href, - }); - return; - } + return ( + + + + + + {props.actions.map((action) => ( + { + if (!user.isSignedIn) { + clerk.openSignIn({ + redirectUrl: window.location.href, + }); + return; + } - setIsLoading(true); - await callServerPromise(action.action()); - setIsLoading(false); - }} - > - {action.title} - - ))} - - - ); + setIsLoading(true); + await callServerPromise(action.action()); + setIsLoading(false); + }} + > + {action.title} + + ))} + + + ); } diff --git a/web/src/db/db.ts b/web/src/db/db.ts index 33aa553..2cd8d5d 100644 --- a/web/src/db/db.ts +++ b/web/src/db/db.ts @@ -1,6 +1,6 @@ -import * as schema from "./schema"; -import { neonConfig, Pool } from "@neondatabase/serverless"; +import { Pool, neonConfig } from "@neondatabase/serverless"; import { drizzle as neonDrizzle } from "drizzle-orm/neon-serverless"; +import * as schema from "./schema"; const isDevContainer = process.env.REMOTE_CONTAINERS !== undefined; @@ -9,7 +9,7 @@ if (process.env.VERCEL_ENV !== "production") { // Set the WebSocket proxy to work with the local instance if (isDevContainer) { // Running inside a VS Code devcontainer - neonConfig.wsProxy = (host) => `host.docker.internal:5481/v1`; + neonConfig.wsProxy = (host) => "host.docker.internal:5481/v1"; } else { // Not running inside a VS Code devcontainer neonConfig.wsProxy = (host) => `${host}:5481/v1`; @@ -26,5 +26,5 @@ export const db = neonDrizzle( }), { schema, - } + }, ); diff --git a/web/src/db/schema.ts b/web/src/db/schema.ts index 20372ff..02bbedf 100644 --- a/web/src/db/schema.ts +++ b/web/src/db/schema.ts @@ -1,13 +1,13 @@ import { type InferSelectModel, relations } from "drizzle-orm"; import { - boolean, - integer, - jsonb, - pgEnum, - pgSchema, - text, - timestamp, - uuid, + boolean, + integer, + jsonb, + pgEnum, + pgSchema, + text, + timestamp, + uuid, } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; @@ -87,7 +87,7 @@ export const workflowVersionRelations = relations( fields: [workflowVersionTable.workflow_id], references: [workflowTable.id], }), - }) + }), ); export const workflowRunStatus = pgEnum("workflow_run_status", [ @@ -130,30 +130,30 @@ export const machinesStatus = pgEnum("machine_status", [ // We still want to keep the workflow run record. export const workflowRunsTable = dbSchema.table("workflow_runs", { - id: uuid("id").primaryKey().defaultRandom().notNull(), - // when workflow version deleted, still want to keep this record - workflow_version_id: uuid("workflow_version_id").references( - () => workflowVersionTable.id, - { - onDelete: "set null", - }, - ), - workflow_inputs: - jsonb("workflow_inputs").$type>(), - workflow_id: uuid("workflow_id") - .notNull() - .references(() => workflowTable.id, { - onDelete: "cascade", - }), - // when machine deleted, still want to keep this record - machine_id: uuid("machine_id").references(() => machinesTable.id, { - onDelete: "set null", - }), - origin: workflowRunOrigin("origin").notNull().default("api"), - status: workflowRunStatus("status").notNull().default("not-started"), - ended_at: timestamp("ended_at"), - created_at: timestamp("created_at").defaultNow().notNull(), - started_at: timestamp("started_at"), + id: uuid("id").primaryKey().defaultRandom().notNull(), + // when workflow version deleted, still want to keep this record + workflow_version_id: uuid("workflow_version_id").references( + () => workflowVersionTable.id, + { + onDelete: "set null", + }, + ), + workflow_inputs: + jsonb("workflow_inputs").$type>(), + workflow_id: uuid("workflow_id") + .notNull() + .references(() => workflowTable.id, { + onDelete: "cascade", + }), + // when machine deleted, still want to keep this record + machine_id: uuid("machine_id").references(() => machinesTable.id, { + onDelete: "set null", + }), + origin: workflowRunOrigin("origin").notNull().default("api"), + status: workflowRunStatus("status").notNull().default("not-started"), + ended_at: timestamp("ended_at"), + created_at: timestamp("created_at").defaultNow().notNull(), + started_at: timestamp("started_at"), }); export const workflowRunRelations = relations( @@ -172,7 +172,7 @@ export const workflowRunRelations = relations( fields: [workflowRunsTable.workflow_id], references: [workflowTable.id], }), - }) + }), ); // We still want to keep the workflow run record. @@ -196,7 +196,7 @@ export const workflowOutputRelations = relations( fields: [workflowRunOutputs.run_id], references: [workflowRunsTable.id], }), - }) + }), ); // when user delete, also delete all the workflow versions @@ -229,7 +229,7 @@ export const snapshotType = z.object({ z.object({ hash: z.string(), disabled: z.boolean(), - }) + }), ), file_custom_nodes: z.array(z.any()), }); @@ -244,7 +244,7 @@ export const showcaseMedia = z.array( z.object({ url: z.string(), isCover: z.boolean().default(false), - }) + }), ); export const showcaseMediaNullable = z @@ -252,36 +252,36 @@ export const showcaseMediaNullable = z z.object({ url: z.string(), isCover: z.boolean().default(false), - }) + }), ) .nullable(); export const deploymentsTable = dbSchema.table("deployments", { - id: uuid("id").primaryKey().defaultRandom().notNull(), - user_id: text("user_id") - .references(() => usersTable.id, { - onDelete: "cascade", - }) - .notNull(), - org_id: text("org_id"), - workflow_version_id: uuid("workflow_version_id") - .notNull() - .references(() => workflowVersionTable.id), - workflow_id: uuid("workflow_id") - .notNull() - .references(() => workflowTable.id, { - onDelete: "cascade", - }), - machine_id: uuid("machine_id") - .notNull() - .references(() => machinesTable.id), - share_slug: text("share_slug").unique(), - description: text("description"), - showcase_media: - jsonb("showcase_media").$type>(), - environment: deploymentEnvironment("environment").notNull(), - created_at: timestamp("created_at").defaultNow().notNull(), - updated_at: timestamp("updated_at").defaultNow().notNull(), + id: uuid("id").primaryKey().defaultRandom().notNull(), + user_id: text("user_id") + .references(() => usersTable.id, { + onDelete: "cascade", + }) + .notNull(), + org_id: text("org_id"), + workflow_version_id: uuid("workflow_version_id") + .notNull() + .references(() => workflowVersionTable.id), + workflow_id: uuid("workflow_id") + .notNull() + .references(() => workflowTable.id, { + onDelete: "cascade", + }), + machine_id: uuid("machine_id") + .notNull() + .references(() => machinesTable.id), + share_slug: text("share_slug").unique(), + description: text("description"), + showcase_media: + jsonb("showcase_media").$type>(), + environment: deploymentEnvironment("environment").notNull(), + created_at: timestamp("created_at").defaultNow().notNull(), + updated_at: timestamp("updated_at").defaultNow().notNull(), }); export const publicShareDeployment = z.object({ @@ -331,6 +331,16 @@ export const apiKeyTable = dbSchema.table("api_keys", { updated_at: timestamp("updated_at").defaultNow().notNull(), }); +export const authRequestsTable = dbSchema.table("auth_requests", { + request_id: text("request_id").primaryKey().notNull(), + user_id: text("user_id"), + org_id: text("org_id"), + api_hash: text("api_hash"), + created_at: timestamp("created_at").defaultNow().notNull(), + expired_date: timestamp("expired_date"), + updated_at: timestamp("updated_at").defaultNow().notNull(), +}); + export type UserType = InferSelectModel; export type WorkflowType = InferSelectModel; export type MachineType = InferSelectModel; diff --git a/web/src/routes/newId.ts b/web/src/routes/newId.ts new file mode 100644 index 0000000..6c3d144 --- /dev/null +++ b/web/src/routes/newId.ts @@ -0,0 +1,13 @@ +import { customAlphabet } from "nanoid"; + +export const nanoid = customAlphabet( + "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", +); +const prefixes = { + img: "img", + vid: "vid", +} as const; + +export function newId(prefix: keyof typeof prefixes): string { + return [prefixes[prefix], nanoid(16)].join("_"); +} diff --git a/web/src/routes/registerCreateRunRoute.ts b/web/src/routes/registerCreateRunRoute.ts index 376c8d4..98143eb 100644 --- a/web/src/routes/registerCreateRunRoute.ts +++ b/web/src/routes/registerCreateRunRoute.ts @@ -1,10 +1,10 @@ -import { createRun } from "../server/createRun"; import { db } from "@/db/db"; import { deploymentsTable } from "@/db/schema"; import type { App } from "@/routes/app"; import { authError } from "@/routes/authError"; -import { z, createRoute } from "@hono/zod-openapi"; +import { createRoute, z } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; +import { createRun } from "../server/createRun"; const createRunRoute = createRoute({ method: "post", @@ -99,7 +99,7 @@ export const registerCreateRunRoute = (app: App) => { }, { status: 500, - } + }, ); } }); diff --git a/web/src/routes/registerGetAuthResponse.ts b/web/src/routes/registerGetAuthResponse.ts new file mode 100644 index 0000000..196dfbb --- /dev/null +++ b/web/src/routes/registerGetAuthResponse.ts @@ -0,0 +1,150 @@ +import { db } from "@/db/db"; +import { authRequestsTable } from "@/db/schema"; +import type { App } from "@/routes/app"; +import { authError } from "@/routes/authError"; +import { z, createRoute } from "@hono/zod-openapi"; +import { eq } from "drizzle-orm"; +import jwt from "jsonwebtoken"; +import crypto from "crypto"; +import { getOrgOrUserDisplayName } from "@/server/getOrgOrUserDisplayName"; +import ms from "ms"; + +const route = createRoute({ + method: "get", + path: "/auth-response/:request_id", + tags: ["comfyui"], + summary: "Get an API Key with code", + description: + "This endpoints is specifically built for ComfyUI workflow upload.", + request: { + params: z.object({ + request_id: z.string(), + }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + api_key: z.string(), + name: z.string(), + }), + }, + }, + description: "The returned API Key", + }, + 201: { + content: { + "application/json": { + schema: z.object({ + message: z.string(), + }), + }, + }, + description: "The API key is not yet ready", + }, + 500: { + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + description: "Error when fetching the API Key with code", + }, + ...authError, + }, +}); + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", +}; + +export const registerGetAuthResponse = (app: App) => { + return app.openapi(route, async (c) => { + const { request_id } = c.req.valid("param"); + + try { + const result = await db.query.authRequestsTable.findFirst({ + where: eq(authRequestsTable.request_id, request_id), + }); + + if (result?.api_hash) { + return c.json( + { + message: "Already used.", + }, + { + status: 201, + headers: corsHeaders, + }, + ); + } + + if (result && result.user_id) { + const expireTime = "1w"; + const token = jwt.sign( + { user_id: result.user_id, org_id: result.org_id }, + process.env.JWT_SECRET!, + { + expiresIn: expireTime, + }, + ); + + const hash = crypto.createHash("sha256").update(token).digest("hex"); + + const now = new Date(); + const expiryDate = new Date(now.getTime() + ms(expireTime)); + + await db + .update(authRequestsTable) + .set({ + api_hash: hash, + expired_date: expiryDate, + }) + .where(eq(authRequestsTable.request_id, request_id)); + + const userName = await getOrgOrUserDisplayName( + result.org_id, + result.user_id, + ); + + return c.json( + { + api_key: token, + name: userName, + }, + { + status: 200, + headers: corsHeaders, + }, + ); + } + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return c.json( + { + error: errorMessage, + }, + { + statusText: "Invalid request", + status: 500, + headers: corsHeaders, + }, + ); + } + return c.json( + { + message: "Not ready yet.", + }, + { + status: 201, + headers: corsHeaders, + }, + ); + }); +}; diff --git a/web/src/routes/registerUploadRoute.ts b/web/src/routes/registerUploadRoute.ts index b58d76b..d318b66 100644 --- a/web/src/routes/registerUploadRoute.ts +++ b/web/src/routes/registerUploadRoute.ts @@ -3,20 +3,7 @@ import { authError } from "@/routes/authError"; import { getFileDownloadUrl } from "@/server/getFileDownloadUrl"; import { handleResourceUpload } from "@/server/resource"; import { z, createRoute } from "@hono/zod-openapi"; -import { customAlphabet } from "nanoid"; - -export const nanoid = customAlphabet( - "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" -); - -const prefixes = { - img: "img", - vid: "vid", -} as const; - -export function newId(prefix: keyof typeof prefixes): string { - return [prefixes[prefix], nanoid(16)].join("_"); -} +import { newId } from "./newId"; const uploadUrlRoute = createRoute({ method: "get", @@ -96,7 +83,7 @@ export const registerUploadRoute = (app: App) => { file_id: id, download_url: await getFileDownloadUrl(filePath), }, - 200 + 200, ); } catch (error: unknown) { const errorMessage = @@ -107,7 +94,7 @@ export const registerUploadRoute = (app: App) => { }, { status: 500, - } + }, ); } }); diff --git a/web/src/routes/registerWorkflowUploadRoute.ts b/web/src/routes/registerWorkflowUploadRoute.ts new file mode 100644 index 0000000..b6fe96d --- /dev/null +++ b/web/src/routes/registerWorkflowUploadRoute.ts @@ -0,0 +1,164 @@ +import { snapshotType, workflowAPIType, workflowType } from "@/db/schema"; +import type { App } from "@/routes/app"; +import { authError } from "@/routes/authError"; +import { + createNewWorkflow, + createNewWorkflowVersion, +} from "@/server/createNewWorkflow"; +import { z, createRoute } from "@hono/zod-openapi"; + +const route = createRoute({ + method: "post", + path: "/upload-workflow", + tags: ["comfyui"], + summary: "Upload workflow from ComfyUI", + description: + "This endpoints is specifically built for ComfyUI workflow upload.", + request: { + body: { + content: { + "application/json": { + schema: z.object({ + workflow_id: z.string().optional(), + workflow_name: z.string().min(1).optional(), + workflow: workflowType, + workflow_api: workflowAPIType, + snapshot: snapshotType, + }), + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + workflow_id: z.string(), + version: z.string(), + }), + }, + }, + description: "Retrieve the output", + }, + 500: { + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + description: "Error when uploading the workflow", + }, + ...authError, + }, +}); + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", +}; + +export const registerWorkflowUploadRoute = (app: App) => { + app.openapi(route, async (c) => { + const { + // user_id, + workflow, + workflow_api, + workflow_id: _workflow_id, + workflow_name, + snapshot, + } = c.req.valid("json"); + const { org_id, user_id } = c.get("apiKeyTokenData")!; + + if (!user_id) + return c.json( + { + error: "Invalid user_id", + }, + { + headers: corsHeaders, + status: 500, + }, + ); + + let workflow_id = _workflow_id; + + let version = -1; + + try { + if ((!workflow_id || workflow_id.length === 0) && workflow_name) { + // Create a new parent workflow + const { workflow_id: _workflow_id, version: _version } = + await createNewWorkflow({ + user_id: user_id, + org_id: org_id, + workflow_name: workflow_name, + workflowData: { + workflow, + workflow_api, + snapshot, + }, + }); + + workflow_id = _workflow_id; + version = _version; + } else if (workflow_id) { + // Case 2 update workflow + const { version: _version } = await createNewWorkflowVersion({ + workflow_id: workflow_id, + workflowData: { + workflow, + workflow_api, + snapshot, + }, + }); + version = _version; + } else { + return c.json( + { + error: "Invalid request, missing either workflow_id or name", + }, + { + status: 500, + statusText: "Invalid request", + headers: corsHeaders, + }, + ); + } + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return c.json( + { + error: errorMessage, + }, + { + statusText: "Invalid request", + status: 500, + headers: corsHeaders, + }, + ); + } + + return c.json( + { + workflow_id: workflow_id, + version: version, + }, + { + status: 200, + headers: corsHeaders, + }, + ); + }); + + app.route("/upload-workflow").options(async (c) => { + return new Response(null, { + status: 204, + headers: corsHeaders, + }); + }); +}; diff --git a/web/src/server/APIKeyBodyRequest.ts b/web/src/server/APIKeyBodyRequest.ts index e7b92bc..3f71a00 100644 --- a/web/src/server/APIKeyBodyRequest.ts +++ b/web/src/server/APIKeyBodyRequest.ts @@ -1,9 +1,10 @@ import { z } from "zod"; export const APIKeyBodyRequest = z.object({ - user_id: z.string().optional(), - org_id: z.string().optional(), + user_id: z.string().optional().nullable(), + org_id: z.string().optional().nullable(), iat: z.number(), + exp: z.number().optional(), }); export type APIKeyUserType = z.infer; diff --git a/web/src/server/createNewWorkflow.ts b/web/src/server/createNewWorkflow.ts index 3636ff9..faa00d7 100644 --- a/web/src/server/createNewWorkflow.ts +++ b/web/src/server/createNewWorkflow.ts @@ -1,6 +1,46 @@ import { db } from "@/db/db"; import type { WorkflowVersionType } from "@/db/schema"; import { workflowTable, workflowVersionTable } from "@/db/schema"; +import { eq, sql } from "drizzle-orm"; + +export async function createNewWorkflowVersion({ + workflow_id, + workflowData, +}: { + workflow_id: string; + workflowData: Pick< + WorkflowVersionType, + "workflow" | "workflow_api" | "snapshot" + >; +}) { + // Add a new version + const data = await db + .insert(workflowVersionTable) + .values({ + workflow_id, + ...workflowData, + version: sql`( + SELECT COALESCE(MAX(version), 0) + 1 + FROM ${workflowVersionTable} + WHERE workflow_id = ${workflow_id} + )`, + }) + .returning(); + const version = data[0].version; + + // Touch up the last updated time + await db + .update(workflowTable) + .set({ + updated_at: new Date(), + }) + .where(eq(workflowTable.id, workflow_id)) + .returning(); + + return { + version, + }; +} export async function createNewWorkflow({ workflow_name, @@ -10,7 +50,7 @@ export async function createNewWorkflow({ }: { workflow_name: string; user_id: string; - org_id?: string; + org_id?: string | null; workflowData: Pick< WorkflowVersionType, "workflow" | "workflow_api" | "snapshot" diff --git a/web/src/server/curdApiKeys.ts b/web/src/server/curdApiKeys.ts index 252bddd..b15b625 100644 --- a/web/src/server/curdApiKeys.ts +++ b/web/src/server/curdApiKeys.ts @@ -1,23 +1,28 @@ "use server"; import { db } from "@/db/db"; -import { apiKeyTable } from "@/db/schema"; +import { apiKeyTable, authRequestsTable } from "@/db/schema"; +import { withServerPromise } from "@/server/withServerPromise"; import { auth } from "@clerk/nextjs"; import { and, desc, eq, isNull } from "drizzle-orm"; import jwt from "jsonwebtoken"; import { revalidatePath } from "next/cache"; -// export const nanoid = customAlphabet( -// "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" -// ); +export const createAuthRequest = withServerPromise( + async (request_id: string) => { + const { userId, orgId } = auth(); -// const prefixes = { -// cd: "cd", -// } as const; + const result = await db.insert(authRequestsTable).values({ + request_id: request_id, + user_id: userId, + org_id: orgId, + }); -// function newId(prefix: keyof typeof prefixes): string { -// return [prefixes[prefix], nanoid(16)].join("_"); -// } + return { + message: "Auth request created, you may now return to your application.", + }; + }, +); export async function addNewAPIKey(name: string) { const { userId, orgId } = auth(); @@ -29,7 +34,7 @@ export async function addNewAPIKey(name: string) { if (orgId) { token = jwt.sign( { user_id: userId, org_id: orgId }, - process.env.JWT_SECRET! + process.env.JWT_SECRET!, ); } else { token = jwt.sign({ user_id: userId }, process.env.JWT_SECRET!); @@ -93,7 +98,7 @@ export async function getAPIKeys() { where: and( eq(apiKeyTable.user_id, userId), isNull(apiKeyTable.org_id), - eq(apiKeyTable.revoked, false) + eq(apiKeyTable.revoked, false), ), orderBy: desc(apiKeyTable.created_at), }); diff --git a/web/src/server/getOrgOrUserDisplayName.tsx b/web/src/server/getOrgOrUserDisplayName.tsx new file mode 100644 index 0000000..562b2c5 --- /dev/null +++ b/web/src/server/getOrgOrUserDisplayName.tsx @@ -0,0 +1,18 @@ +import { db } from "@/db/db"; +import { usersTable } from "@/db/schema"; +import { clerkClient } from "@clerk/nextjs"; +import { eq } from "drizzle-orm"; + +export async function getOrgOrUserDisplayName( + orgId: string | undefined | null, + userId: string, +) { + return orgId + ? await clerkClient.organizations + .getOrganization({ + organizationId: orgId, + }) + .then((x) => x.name) + : (await db.select().from(usersTable).where(eq(usersTable.id, userId)))[0] + .name; +}