feat(auth): created logs, user_environment, and user_settings tables and associated routes

This commit is contained in:
Waleed Latif
2025-02-17 10:50:35 -08:00
parent 78e210f98d
commit c4149b9241
7 changed files with 853 additions and 1 deletions

View File

@@ -0,0 +1,88 @@
import { NextResponse } from 'next/server'
import { eq } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { z } from 'zod'
import { hashSecret } from '@/lib/utils'
import { EnvironmentVariable } from '@/stores/settings/environment/types'
import { db } from '@/db'
import { userEnvironment } from '@/db/schema'
const EnvironmentSchema = z.object({
userId: z.string(),
data: z.string(), // A JSON stringified object of envvars
})
export async function POST(request: Request) {
try {
const body = await request.json()
const { userId, data } = EnvironmentSchema.parse(body)
// Parse the incoming JSON string
const parsedData = JSON.parse(data) as Record<string, EnvironmentVariable>
// Hash all environment variables with unique salts
const securedData = await Promise.all(
Object.entries(parsedData).map(async ([key, value]) => {
const { hash, salt } = await hashSecret(value.value)
return [key, { key, value: hash, salt }]
})
)
// Store the hashed values
await db
.insert(userEnvironment)
.values({
id: nanoid(),
userId,
variables: JSON.stringify(Object.fromEntries(securedData)),
updatedAt: new Date(),
})
.onConflictDoUpdate({
target: [userEnvironment.userId],
set: {
variables: JSON.stringify(Object.fromEntries(securedData)),
updatedAt: new Date(),
},
})
return NextResponse.json({ success: true }, { status: 200 })
} catch (error: any) {
console.error('Environment update error:', error)
return NextResponse.json({ error: error.message }, { status: 500 })
}
}
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const userId = searchParams.get('userId')
if (!userId) {
return NextResponse.json({ error: 'userId is required' }, { status: 400 })
}
const result = await db
.select()
.from(userEnvironment)
.where(eq(userEnvironment.userId, userId))
.limit(1)
if (!result.length) {
return NextResponse.json({ data: {} }, { status: 200 })
}
// Parse the variables and return just the structure without the hashed values
const variables = JSON.parse(result[0].variables)
const sanitizedVariables = Object.fromEntries(
Object.entries(variables).map(([key, value]: [string, any]) => [
key,
{ key, value: '••••••••' }, // Hide the actual value
])
)
return NextResponse.json({ data: sanitizedVariables }, { status: 200 })
} catch (error: any) {
console.error('Environment fetch error:', error)
return NextResponse.json({ error: error.message }, { status: 500 })
}
}

View File

@@ -0,0 +1,80 @@
import { NextResponse } from 'next/server'
import { eq } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { z } from 'zod'
import { db } from '@/db'
import { userSettings } from '@/db/schema'
const SettingsSchema = z.object({
userId: z.string(),
isAutoConnectEnabled: z.boolean().default(true),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const { userId, isAutoConnectEnabled } = SettingsSchema.parse(body)
// Store the settings
await db
.insert(userSettings)
.values({
id: nanoid(),
userId,
isAutoConnectEnabled,
updatedAt: new Date(),
})
.onConflictDoUpdate({
target: [userSettings.userId],
set: {
isAutoConnectEnabled,
updatedAt: new Date(),
},
})
return NextResponse.json({ success: true }, { status: 200 })
} catch (error: any) {
console.error('Settings update error:', error)
return NextResponse.json({ error: error.message }, { status: 500 })
}
}
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const userId = searchParams.get('userId')
if (!userId) {
return NextResponse.json({ error: 'userId is required' }, { status: 400 })
}
const result = await db
.select()
.from(userSettings)
.where(eq(userSettings.userId, userId))
.limit(1)
if (!result.length) {
return NextResponse.json(
{
data: {
isAutoConnectEnabled: true, // Return default values
},
},
{ status: 200 }
)
}
return NextResponse.json(
{
data: {
isAutoConnectEnabled: result[0].isAutoConnectEnabled,
},
},
{ status: 200 }
)
} catch (error: any) {
console.error('Settings fetch error:', error)
return NextResponse.json({ error: error.message }, { status: 500 })
}
}

View File

@@ -0,0 +1,28 @@
CREATE TABLE "logs" (
"id" text PRIMARY KEY NOT NULL,
"workflow_id" text NOT NULL,
"execution_id" text,
"level" text NOT NULL,
"message" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user_environment" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"variables" text NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "user_environment_user_id_unique" UNIQUE("user_id")
);
--> statement-breakpoint
CREATE TABLE "user_settings" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"is_auto_connect_enabled" boolean DEFAULT true NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "user_settings_user_id_unique" UNIQUE("user_id")
);
--> statement-breakpoint
ALTER TABLE "logs" ADD CONSTRAINT "logs_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_environment" ADD CONSTRAINT "user_environment_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,599 @@
{
"id": "3ee25aee-2e62-4bf0-bb64-dc01bf8cad70",
"prevId": "80542c3c-48a3-41e5-b911-fe51152efccc",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.logs": {
"name": "logs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"execution_id": {
"name": "execution_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"level": {
"name": "level",
"type": "text",
"primaryKey": false,
"notNull": true
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"logs_workflow_id_workflow_id_fk": {
"name": "logs_workflow_id_workflow_id_fk",
"tableFrom": "logs",
"tableTo": "workflow",
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": ["token"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user_environment": {
"name": "user_environment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"variables": {
"name": "variables",
"type": "text",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"user_environment_user_id_user_id_fk": {
"name": "user_environment_user_id_user_id_fk",
"tableFrom": "user_environment",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_environment_user_id_unique": {
"name": "user_environment_user_id_unique",
"nullsNotDistinct": false,
"columns": ["user_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user_settings": {
"name": "user_settings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"is_auto_connect_enabled": {
"name": "is_auto_connect_enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"user_settings_user_id_user_id_fk": {
"name": "user_settings_user_id_user_id_fk",
"tableFrom": "user_settings",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_settings_user_id_unique": {
"name": "user_settings_user_id_unique",
"nullsNotDistinct": false,
"columns": ["user_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.waitlist": {
"name": "waitlist",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"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": {
"waitlist_email_unique": {
"name": "waitlist_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow": {
"name": "workflow",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_synced": {
"name": "last_synced",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"workflow_user_id_user_id_fk": {
"name": "workflow_user_id_user_id_fk",
"tableFrom": "workflow",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -22,6 +22,13 @@
"when": 1739776634326,
"tag": "0002_previous_xavin",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1739818168546,
"tag": "0003_smiling_hammerhead",
"breakpoints": true
}
]
}

View File

@@ -1,4 +1,4 @@
import { boolean, integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
import { boolean, integer, pgTable, text, timestamp, unique } from 'drizzle-orm/pg-core'
export const user = pgTable('user', {
id: text('id').primaryKey(),
@@ -70,3 +70,34 @@ export const waitlist = pgTable('waitlist', {
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
})
export const consoleLog = pgTable('logs', {
id: text('id').primaryKey(),
workflowId: text('workflow_id')
.notNull()
.references(() => workflow.id, { onDelete: 'cascade' }),
executionId: text('execution_id'),
level: text('level').notNull(), // e.g. "info", "error", etc.
message: text('message').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
})
export const userEnvironment = pgTable('user_environment', {
id: text('id').primaryKey(), // Use the user id as the key
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' })
.unique(), // One environment per user
variables: text('variables').notNull(), // JSON stringified {key: hashedValue}
updatedAt: timestamp('updated_at').notNull().defaultNow(),
})
export const userSettings = pgTable('user_settings', {
id: text('id').primaryKey(), // Use the user id as the key
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' })
.unique(), // One settings record per user
isAutoConnectEnabled: boolean('is_auto_connect_enabled').notNull().default(true),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
})

View File

@@ -1,6 +1,25 @@
import { type ClassValue, clsx } from 'clsx'
import { createHash } from 'crypto'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**
* Hashes a secret using SHA-256 with a salt
* @param secret - The secret to hash
* @param salt - Optional salt to use for hashing. If not provided, a random salt will be generated
* @returns A promise that resolves to an object containing the hashed secret and salt
*/
export async function hashSecret(
secret: string,
salt?: string
): Promise<{ hash: string; salt: string }> {
const useSalt =
salt || createHash('sha256').update(crypto.randomUUID()).digest('hex').slice(0, 16)
const hash = createHash('sha256')
.update(secret + useSalt)
.digest('hex')
return { hash, salt: useSalt }
}