From 274d83a402905a564d29044939c5551a2f213937 Mon Sep 17 00:00:00 2001 From: WcaleNieWolny Date: Tue, 18 Nov 2025 18:46:50 +0100 Subject: [PATCH 1/3] fix: delta updload --- supabase/functions/_backend/private/files.ts | 190 +++++------ .../_backend/utils/hono_middleware.ts | 147 ++++++++- supabase/functions/_backend/utils/pg_files.ts | 295 ++++++++++++++++++ .../_backend/utils/postgres_schema.ts | 35 +++ 4 files changed, 572 insertions(+), 95 deletions(-) create mode 100644 supabase/functions/_backend/utils/pg_files.ts diff --git a/supabase/functions/_backend/private/files.ts b/supabase/functions/_backend/private/files.ts index a51aa7b996..6ad369ef94 100644 --- a/supabase/functions/_backend/private/files.ts +++ b/supabase/functions/_backend/private/files.ts @@ -11,8 +11,10 @@ import { ALLOWED_HEADERS, ALLOWED_METHODS, EXPOSED_HEADERS, MAX_UPLOAD_LENGTH_BY import { simpleError } from '../utils/hono.ts' import { middlewareKey } from '../utils/hono_middleware.ts' import { cloudlog } from '../utils/logging.ts' +import { closeClient, getDrizzleClient, getPgClient } from '../utils/pg.ts' +import { getAppByAppIdPg, getUserIdFromApikey, hasAppRightApikeyPg } from '../utils/pg_files.ts' import { createStatsBandwidth } from '../utils/stats.ts' -import { hasAppRightApikey, supabaseAdmin } from '../utils/supabase.ts' +import { supabaseAdmin } from '../utils/supabase.ts' import { backgroundTask } from '../utils/utils.ts' import { app as download_link } from './download_link.ts' import { app as files_config } from './files_config.ts' @@ -335,129 +337,133 @@ async function checkWriteAppAccess(c: Context, next: Next) { userId: apikey.user_id, }) - const { data: userId, error: _errorUserId } = await supabaseAdmin(c) - .rpc('get_user_id', { apikey: capgkey, app_id }) + // Use Postgres instead of Supabase SDK + const pgClient = getPgClient(c, true) // read-only query + const drizzleClient = getDrizzleClient(pgClient) - cloudlog({ - requestId: c.get('requestId'), - message: 'checkWriteAppAccess - get_user_id result', - userId, - hasError: !!_errorUserId, - error: _errorUserId, - userIdIsNull: userId === null, - }) + try { + // Get user_id from apikey using Postgres + const userId = await getUserIdFromApikey(c, capgkey, drizzleClient) - if (_errorUserId || userId === null) { cloudlog({ requestId: c.get('requestId'), - message: 'checkWriteAppAccess - user lookup failed', - error: _errorUserId, + message: 'checkWriteAppAccess - get_user_id result', userId, - app_id, - capgkeyPrefix: capgkey ? capgkey.substring(0, 15) : 'missing', - }) - throw new HTTPException(400, { - res: c.json({ - error: 'user_not_found', - message: 'User not found for the provided API key', - moreInfo: { app_id, hasApiKey: !!capgkey, apiKeyLength: capgkey?.length ?? 0, requestId: c.get('requestId') }, - }), + userIdIsNull: userId === null, }) - } - - cloudlog({ - requestId: c.get('requestId'), - message: 'checkWriteAppAccess - checking app permissions via hasAppRightApikey', - userId, - app_id, - }) - // Use the hasAppRightApikey function which checks org membership, roles, and permissions - const hasPermission = await hasAppRightApikey(c, app_id, userId, 'read', capgkey) - - cloudlog({ - requestId: c.get('requestId'), - message: 'checkWriteAppAccess - hasAppRightApikey result', - hasPermission, - }) + if (userId === null) { + cloudlog({ + requestId: c.get('requestId'), + message: 'checkWriteAppAccess - user lookup failed', + userId, + app_id, + capgkeyPrefix: capgkey ? capgkey.substring(0, 15) : 'missing', + }) + throw new HTTPException(400, { + res: c.json({ + error: 'user_not_found', + message: 'User not found for the provided API key', + moreInfo: { app_id, hasApiKey: !!capgkey, apiKeyLength: capgkey?.length ?? 0, requestId: c.get('requestId') }, + }), + }) + } - if (!hasPermission) { cloudlog({ requestId: c.get('requestId'), - message: 'checkWriteAppAccess - insufficient permissions', + message: 'checkWriteAppAccess - checking app permissions via hasAppRightApikeyPg', userId, app_id, }) - throw new HTTPException(403, { - res: c.json({ - error: 'insufficient_permissions', - message: 'You don\'t have permission to access this app', - moreInfo: { app_id, requestId: c.get('requestId') }, - }), - }) - } - const { data: app, error: errorApp } = await supabaseAdmin(c) - .from('apps') - .select('app_id, owner_org') - .eq('app_id', app_id) - .single() + // Use the Postgres version of hasAppRightApikey + const requiredRight: Database['public']['Enums']['user_min_right'] = 'read' + const hasPermission = await hasAppRightApikeyPg(c, app_id, requiredRight, userId, capgkey, drizzleClient) - if (errorApp) { cloudlog({ requestId: c.get('requestId'), - message: 'checkWriteAppAccess - app not found', - error: errorApp, - app_id, - }) - throw new HTTPException(404, { - res: c.json({ - error: 'app_not_found', - message: 'App not found', - moreInfo: { app_id, requestId: c.get('requestId') }, - }), + message: 'checkWriteAppAccess - hasAppRightApikeyPg result', + hasPermission, }) - } - if (app.owner_org !== owner_org) { + if (!hasPermission) { + cloudlog({ + requestId: c.get('requestId'), + message: 'checkWriteAppAccess - insufficient permissions', + userId, + app_id, + }) + throw new HTTPException(403, { + res: c.json({ + error: 'insufficient_permissions', + message: 'You don\'t have permission to access this app', + moreInfo: { app_id, requestId: c.get('requestId') }, + }), + }) + } + + // Get app using Postgres + const app = await getAppByAppIdPg(c, app_id, drizzleClient) + + if (!app) { + cloudlog({ + requestId: c.get('requestId'), + message: 'checkWriteAppAccess - app not found', + app_id, + }) + throw new HTTPException(404, { + res: c.json({ + error: 'app_not_found', + message: 'App not found', + moreInfo: { app_id, requestId: c.get('requestId') }, + }), + }) + } + + if (app.owner_org !== owner_org) { + cloudlog({ + requestId: c.get('requestId'), + message: 'checkWriteAppAccess - owner org mismatch', + filePathOwnerOrg: owner_org, + actualOwnerOrg: app.owner_org, + app_id, + }) + throw new HTTPException(403, { + res: c.json({ + error: 'owner_org_mismatch', + message: 'The owner organization in the file path does not match the app\'s owner organization', + moreInfo: { + app_id, + filePathOwnerOrg: owner_org, + actualOwnerOrg: app.owner_org, + requestId: c.get('requestId'), + }, + }), + }) + } + cloudlog({ requestId: c.get('requestId'), - message: 'checkWriteAppAccess - owner org mismatch', - filePathOwnerOrg: owner_org, - actualOwnerOrg: app.owner_org, + message: 'checkWriteAppAccess - access granted', app_id, - }) - throw new HTTPException(403, { - res: c.json({ - error: 'owner_org_mismatch', - message: 'The owner organization in the file path does not match the app\'s owner organization', - moreInfo: { - app_id, - filePathOwnerOrg: owner_org, - actualOwnerOrg: app.owner_org, - requestId: c.get('requestId'), - }, - }), + owner_org, }) } - - cloudlog({ - requestId: c.get('requestId'), - message: 'checkWriteAppAccess - access granted', - app_id, - owner_org, - }) + finally { + // Always close the connection + await backgroundTask(c, closeClient(c, pgClient)) + } await next() } app.options(`/upload/${ATTACHMENT_PREFIX}`, optionsHandler) -app.post(`/upload/${ATTACHMENT_PREFIX}`, middlewareKey(['all', 'write', 'upload']), setKeyFromMetadata, checkWriteAppAccess, uploadHandler) +app.post(`/upload/${ATTACHMENT_PREFIX}`, middlewareKey(['all', 'write', 'upload'], true), setKeyFromMetadata, checkWriteAppAccess, uploadHandler) app.options(`/upload/${ATTACHMENT_PREFIX}/:id{.+}`, optionsHandler) -app.get(`/upload/${ATTACHMENT_PREFIX}/:id{.+}`, middlewareKey(['all', 'write', 'upload']), setKeyFromIdParam, checkWriteAppAccess, getHandler) +app.get(`/upload/${ATTACHMENT_PREFIX}/:id{.+}`, middlewareKey(['all', 'write', 'upload'], true), setKeyFromIdParam, checkWriteAppAccess, getHandler) app.get(`/read/${ATTACHMENT_PREFIX}/:id{.+}`, setKeyFromIdParam, getHandler) -app.patch(`/upload/${ATTACHMENT_PREFIX}/:id{.+}`, middlewareKey(['all', 'write', 'upload']), setKeyFromIdParam, checkWriteAppAccess, uploadHandler) +app.patch(`/upload/${ATTACHMENT_PREFIX}/:id{.+}`, middlewareKey(['all', 'write', 'upload'], true), setKeyFromIdParam, checkWriteAppAccess, uploadHandler) app.route('/config', files_config) app.route('/download_link', download_link) diff --git a/supabase/functions/_backend/utils/hono_middleware.ts b/supabase/functions/_backend/utils/hono_middleware.ts index 255be10bcb..c644951887 100644 --- a/supabase/functions/_backend/utils/hono_middleware.ts +++ b/supabase/functions/_backend/utils/hono_middleware.ts @@ -1,8 +1,11 @@ import type { Context } from 'hono' import type { AuthInfo } from './hono.ts' import type { Database } from './supabase.types.ts' +import { and, eq, inArray } from 'drizzle-orm' import { honoFactory, quickError } from './hono.ts' import { cloudlog } from './logging.ts' +import { getDrizzleClient, getPgClient, logPgError } from './pg.ts' +import * as schema from './postgres_schema.ts' import { checkKey, checkKeyById, supabaseAdmin, supabaseClient } from './supabase.ts' // TODO: make universal middleware who @@ -23,6 +26,92 @@ function isUUID(str: string) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str) } +/** + * Check API key using Postgres/Drizzle instead of Supabase SDK + */ +async function checkKeyPg( + c: Context, + keyString: string, + rights: Database['public']['Enums']['key_mode'][], + drizzleClient: ReturnType, +): Promise { + try { + const result = await drizzleClient + .select() + .from(schema.apikeys) + .where(and( + eq(schema.apikeys.key, keyString), + inArray(schema.apikeys.mode, rights), + )) + .limit(1) + .then(data => data[0]) + + if (!result) { + return null + } + + // Convert to the expected format, ensuring arrays are properly handled + return { + id: result.id, + created_at: result.created_at?.toISOString() || null, + user_id: result.user_id, + key: result.key, + mode: result.mode, + updated_at: result.updated_at?.toISOString() || null, + name: result.name, + limited_to_orgs: result.limited_to_orgs || [], + limited_to_apps: result.limited_to_apps || [], + } as Database['public']['Tables']['apikeys']['Row'] + } + catch (e: unknown) { + logPgError(c, 'checkKeyPg', e) + return null + } +} + +/** + * Check API key by ID using Postgres/Drizzle instead of Supabase SDK + */ +async function checkKeyByIdPg( + c: Context, + id: number, + rights: Database['public']['Enums']['key_mode'][], + drizzleClient: ReturnType, +): Promise { + try { + const result = await drizzleClient + .select() + .from(schema.apikeys) + .where(and( + eq(schema.apikeys.id, id), + inArray(schema.apikeys.mode, rights), + )) + .limit(1) + .then(data => data[0]) + + if (!result) { + return null + } + + // Convert to the expected format, ensuring arrays are properly handled + return { + id: result.id, + created_at: result.created_at?.toISOString() || null, + user_id: result.user_id, + key: result.key, + mode: result.mode, + updated_at: result.updated_at?.toISOString() || null, + name: result.name, + limited_to_orgs: result.limited_to_orgs || [], + limited_to_apps: result.limited_to_apps || [], + } as Database['public']['Tables']['apikeys']['Row'] + } + catch (e: unknown) { + logPgError(c, 'checkKeyByIdPg', e) + return null + } +} + async function foundAPIKey(c: Context, capgkeyString: string, rights: Database['public']['Enums']['key_mode'][]) { const subkey_id = c.req.header('x-limited-key-id') ? Number(c.req.header('x-limited-key-id')) : null @@ -106,12 +195,13 @@ export function middlewareV2(rights: Database['public']['Enums']['key_mode'][]) }) } -export function middlewareKey(rights: Database['public']['Enums']['key_mode'][]) { +export function middlewareKey(rights: Database['public']['Enums']['key_mode'][], usePostgres = false) { const subMiddlewareKey = honoFactory.createMiddleware(async (c, next) => { const capgkey_string = c.req.header('capgkey') const apikey_string = c.req.header('authorization') const subkey_id = c.req.header('x-limited-key-id') ? Number(c.req.header('x-limited-key-id')) : null const key = capgkey_string ?? apikey_string + cloudlog({ requestId: c.get('requestId'), message: 'middlewareKey - checking authorization', @@ -120,20 +210,71 @@ export function middlewareKey(rights: Database['public']['Enums']['key_mode'][]) hasCapgkey: !!capgkey_string, hasAuthorization: !!apikey_string, hasKey: !!key, + usePostgres, }) if (!key) { cloudlog({ requestId: c.get('requestId'), message: 'No key provided', method: c.req.method, url: c.req.url }) return quickError(401, 'no_key_provided', 'No key provided') } - const apikey: Database['public']['Tables']['apikeys']['Row'] | null = await checkKey(c, key, supabaseAdmin(c), rights) + + let apikey: Database['public']['Tables']['apikeys']['Row'] | null = null + let pgClient: ReturnType | null = null + + if (usePostgres) { + try { + pgClient = getPgClient(c, true) // read-only query + const drizzleClient = getDrizzleClient(pgClient) + apikey = await checkKeyPg(c, key, rights, drizzleClient) + } + finally { + if (pgClient) { + pgClient.end().catch((err) => { + cloudlog({ + requestId: c.get('requestId'), + message: 'middlewareKey - PG connection close error', + error: err instanceof Error ? err.message : String(err), + }) + }) + } + } + } + else { + apikey = await checkKey(c, key, supabaseAdmin(c), rights) + } + if (!apikey) { cloudlog({ requestId: c.get('requestId'), message: 'Invalid apikey', key, method: c.req.method, url: c.req.url }) return quickError(401, 'invalid_apikey', 'Invalid apikey') } c.set('apikey', apikey) c.set('capgkey', key) + if (subkey_id) { - const subkey: Database['public']['Tables']['apikeys']['Row'] | null = await checkKeyById(c, subkey_id, supabaseAdmin(c), rights) + let subkey: Database['public']['Tables']['apikeys']['Row'] | null = null + let subkeyPgClient: ReturnType | null = null + + if (usePostgres) { + try { + subkeyPgClient = getPgClient(c, true) + const drizzleClient = getDrizzleClient(subkeyPgClient) + subkey = await checkKeyByIdPg(c, subkey_id, rights, drizzleClient) + } + finally { + if (subkeyPgClient) { + subkeyPgClient.end().catch((err) => { + cloudlog({ + requestId: c.get('requestId'), + message: 'middlewareKey - Subkey PG connection close error', + error: err instanceof Error ? err.message : String(err), + }) + }) + } + } + } + else { + subkey = await checkKeyById(c, subkey_id, supabaseAdmin(c), rights) + } + if (!subkey) { cloudlog({ requestId: c.get('requestId'), message: 'Invalid subkey', subkey_id }) return quickError(401, 'invalid_subkey', 'Invalid subkey') diff --git a/supabase/functions/_backend/utils/pg_files.ts b/supabase/functions/_backend/utils/pg_files.ts new file mode 100644 index 0000000000..47a5f9d18f --- /dev/null +++ b/supabase/functions/_backend/utils/pg_files.ts @@ -0,0 +1,295 @@ +import type { Context } from 'hono' +import type { Database } from './supabase.types.ts' +import { and, eq } from 'drizzle-orm' +import { cloudlog } from './logging.ts' +import { getDrizzleClient, logPgError } from './pg.ts' +import * as schema from './postgres_schema.ts' + +/** + * Get user_id from apikey using Postgres/Drizzle + * Equivalent to the get_user_id(apikey) RPC function + */ +export async function getUserIdFromApikey( + c: Context, + apikey: string, + drizzleClient: ReturnType, +): Promise { + try { + cloudlog({ + requestId: c.get('requestId'), + message: 'getUserIdFromApikey - querying', + apikeyPrefix: apikey?.substring(0, 15), + }) + + const result = await drizzleClient + .select({ user_id: schema.apikeys.user_id }) + .from(schema.apikeys) + .where(eq(schema.apikeys.key, apikey)) + .limit(1) + .then(data => data[0]) + + cloudlog({ + requestId: c.get('requestId'), + message: 'getUserIdFromApikey - result', + userId: result?.user_id ?? null, + }) + + return result?.user_id ?? null + } + catch (e: unknown) { + logPgError(c, 'getUserIdFromApikey', e) + return null + } +} + +/** + * Get owner_org from app_id using Postgres/Drizzle + * Equivalent to get_user_main_org_id_by_app_id(app_id) RPC function + */ +export async function getOwnerOrgByAppId( + c: Context, + appId: string, + drizzleClient: ReturnType, +): Promise { + try { + const result = await drizzleClient + .select({ owner_org: schema.apps.owner_org }) + .from(schema.apps) + .where(eq(schema.apps.app_id, appId)) + .limit(1) + .then(data => data[0]) + + return result?.owner_org ?? null + } + catch (e: unknown) { + logPgError(c, 'getOwnerOrgByAppId', e) + return null + } +} + +/** + * Check minimum rights for a user + * Equivalent to check_min_rights(min_right, user_id, org_id, app_id, channel_id) RPC function + */ +export async function checkMinRightsPg( + c: Context, + minRight: Database['public']['Enums']['user_min_right'], + userId: string, + orgId: string, + appId: string | null, + channelId: number | null, + drizzleClient: ReturnType, +): Promise { + try { + if (!userId) { + cloudlog({ + requestId: c.get('requestId'), + message: 'checkMinRightsPg - userId is null', + }) + return false + } + + // Get all user rights for this org and user + const userRights = await drizzleClient + .select({ + user_right: schema.org_users.user_right, + app_id: schema.org_users.app_id, + channel_id: schema.org_users.channel_id, + }) + .from(schema.org_users) + .where(and( + eq(schema.org_users.org_id, orgId), + eq(schema.org_users.user_id, userId), + )) + + // Define the right hierarchy + const rightHierarchy: Record = { + 'invite_read': 0, + 'invite_upload': 1, + 'invite_write': 2, + 'invite_admin': 3, + 'invite_super_admin': 3.5, + 'read': 4, + 'upload': 5, + 'write': 6, + 'admin': 7, + 'super_admin': 8, + } + + const minRightLevel = rightHierarchy[minRight] + + // Check if any of the user's rights meet the minimum requirement + for (const userRight of userRights) { + if (!userRight.user_right) + continue + + const userRightLevel = rightHierarchy[userRight.user_right] + + // Check conditions as in the SQL function + const hasOrgWideRight = userRightLevel >= minRightLevel + && userRight.app_id === null + && userRight.channel_id === null + + const hasAppRight = userRightLevel >= minRightLevel + && userRight.app_id === appId + && userRight.channel_id === null + + const hasChannelRight = userRightLevel >= minRightLevel + && userRight.app_id === appId + && userRight.channel_id === channelId + + if (hasOrgWideRight || hasAppRight || hasChannelRight) { + cloudlog({ + requestId: c.get('requestId'), + message: 'checkMinRightsPg - permission granted', + minRight, + userRight: userRight.user_right, + hasOrgWideRight, + hasAppRight, + hasChannelRight, + }) + return true + } + } + + cloudlog({ + requestId: c.get('requestId'), + message: 'checkMinRightsPg - permission denied', + minRight, + userId, + orgId, + appId, + channelId, + userRightsCount: userRights.length, + }) + + return false + } + catch (e: unknown) { + logPgError(c, 'checkMinRightsPg', e) + return false + } +} + +/** + * Check if an API key has the right access to an app + * Equivalent to has_app_right_apikey(appid, right, userid, apikey) RPC function + */ +export async function hasAppRightApikeyPg( + c: Context, + appId: string, + right: Database['public']['Enums']['user_min_right'], + userId: string, + apikey: string, + drizzleClient: ReturnType, +): Promise { + try { + cloudlog({ + requestId: c.get('requestId'), + message: 'hasAppRightApikeyPg - start', + appId, + right, + userId, + apikeyPrefix: apikey?.substring(0, 15), + }) + + // Get owner_org for the app + const orgId = await getOwnerOrgByAppId(c, appId, drizzleClient) + if (!orgId) { + cloudlog({ + requestId: c.get('requestId'), + message: 'hasAppRightApikeyPg - org not found', + appId, + }) + return false + } + + // Get the apikey record + const apiKeyRecord = await drizzleClient + .select({ + limited_to_orgs: schema.apikeys.limited_to_orgs, + limited_to_apps: schema.apikeys.limited_to_apps, + }) + .from(schema.apikeys) + .where(eq(schema.apikeys.key, apikey)) + .limit(1) + .then(data => data[0]) + + if (!apiKeyRecord) { + cloudlog({ + requestId: c.get('requestId'), + message: 'hasAppRightApikeyPg - apikey not found', + }) + return false + } + + // Check if apikey is limited to specific orgs + if (apiKeyRecord.limited_to_orgs && apiKeyRecord.limited_to_orgs.length > 0) { + if (!apiKeyRecord.limited_to_orgs.includes(orgId)) { + cloudlog({ + requestId: c.get('requestId'), + message: 'hasAppRightApikeyPg - org restriction denied', + orgId, + limitedToOrgs: apiKeyRecord.limited_to_orgs, + }) + return false + } + } + + // Check if apikey is limited to specific apps + if (apiKeyRecord.limited_to_apps && apiKeyRecord.limited_to_apps.length > 0) { + if (!apiKeyRecord.limited_to_apps.includes(appId)) { + cloudlog({ + requestId: c.get('requestId'), + message: 'hasAppRightApikeyPg - app restriction denied', + appId, + limitedToApps: apiKeyRecord.limited_to_apps, + }) + return false + } + } + + // Check minimum rights + const hasRights = await checkMinRightsPg(c, right, userId, orgId, appId, null, drizzleClient) + + cloudlog({ + requestId: c.get('requestId'), + message: 'hasAppRightApikeyPg - final result', + hasRights, + }) + + return hasRights + } + catch (e: unknown) { + logPgError(c, 'hasAppRightApikeyPg', e) + return false + } +} + +/** + * Get app by app_id with owner_org + */ +export async function getAppByAppIdPg( + c: Context, + appId: string, + drizzleClient: ReturnType, +): Promise<{ app_id: string, owner_org: string } | null> { + try { + const app = await drizzleClient + .select({ + app_id: schema.apps.app_id, + owner_org: schema.apps.owner_org, + }) + .from(schema.apps) + .where(eq(schema.apps.app_id, appId)) + .limit(1) + .then(data => data[0]) + + return app ?? null + } + catch (e: unknown) { + logPgError(c, 'getAppByAppIdPg', e) + return null + } +} + diff --git a/supabase/functions/_backend/utils/postgres_schema.ts b/supabase/functions/_backend/utils/postgres_schema.ts index fed093a90d..8e87af8544 100644 --- a/supabase/functions/_backend/utils/postgres_schema.ts +++ b/supabase/functions/_backend/utils/postgres_schema.ts @@ -3,6 +3,18 @@ import { bigint, boolean, pgEnum, pgTable, serial, text, timestamp, uuid, varcha // do_not_change export const disableUpdatePgEnum = pgEnum('disable_update', ['major', 'minor', 'patch', 'version_number', 'none']) +export const keyModePgEnum = pgEnum('key_mode', ['read', 'write', 'all', 'upload']) +export const userMinRightPgEnum = pgEnum('user_min_right', [ + 'invite_read', + 'invite_upload', + 'invite_write', + 'invite_admin', + 'read', + 'upload', + 'write', + 'admin', + 'super_admin', +]) export const apps = pgTable('apps', { created_at: timestamp('created_at').notNull().defaultNow(), @@ -92,3 +104,26 @@ export const stripe_info = pgTable('stripe_info', { storage_exceeded: boolean('storage_exceeded'), bandwidth_exceeded: boolean('bandwidth_exceeded'), }) + +export const apikeys = pgTable('apikeys', { + id: bigint('id', { mode: 'number' }).primaryKey().notNull(), + created_at: timestamp('created_at').defaultNow(), + user_id: uuid('user_id').notNull(), + key: varchar('key').notNull(), + mode: keyModePgEnum('mode').notNull(), + updated_at: timestamp('updated_at').defaultNow(), + name: varchar('name').notNull(), + limited_to_orgs: uuid('limited_to_orgs').array(), + limited_to_apps: varchar('limited_to_apps').array(), +}) + +export const org_users = pgTable('org_users', { + id: bigint('id', { mode: 'number' }).primaryKey().notNull(), + created_at: timestamp('created_at').defaultNow(), + updated_at: timestamp('updated_at').defaultNow(), + user_id: uuid('user_id').notNull(), + org_id: uuid('org_id').notNull(), + app_id: varchar('app_id'), + channel_id: bigint('channel_id', { mode: 'number' }), + user_right: userMinRightPgEnum('user_right'), +}) From 63ed16cd0af4a006f2843d8ee7968fec18e3856c Mon Sep 17 00:00:00 2001 From: WcaleNieWolny Date: Tue, 18 Nov 2025 19:25:31 +0100 Subject: [PATCH 2/3] chore: simplify postgres logic --- supabase/functions/_backend/utils/pg_files.ts | 370 ++++++------------ 1 file changed, 128 insertions(+), 242 deletions(-) diff --git a/supabase/functions/_backend/utils/pg_files.ts b/supabase/functions/_backend/utils/pg_files.ts index 47a5f9d18f..295c1a4ff3 100644 --- a/supabase/functions/_backend/utils/pg_files.ts +++ b/supabase/functions/_backend/utils/pg_files.ts @@ -1,295 +1,181 @@ import type { Context } from 'hono' import type { Database } from './supabase.types.ts' -import { and, eq } from 'drizzle-orm' +import { eq, sql } from 'drizzle-orm' import { cloudlog } from './logging.ts' import { getDrizzleClient, logPgError } from './pg.ts' import * as schema from './postgres_schema.ts' /** - * Get user_id from apikey using Postgres/Drizzle - * Equivalent to the get_user_id(apikey) RPC function + * Get user_id from apikey using the existing Postgres function */ export async function getUserIdFromApikey( - c: Context, - apikey: string, - drizzleClient: ReturnType, + c: Context, + apikey: string, + drizzleClient: ReturnType, ): Promise { - try { - cloudlog({ - requestId: c.get('requestId'), - message: 'getUserIdFromApikey - querying', - apikeyPrefix: apikey?.substring(0, 15), - }) + try { + cloudlog({ + requestId: c.get('requestId'), + message: 'getUserIdFromApikey - querying', + apikeyPrefix: apikey?.substring(0, 15), + }) + + // Call the existing Postgres function + const result = await drizzleClient.execute<{ get_user_id: string }>( + sql`SELECT get_user_id(${apikey})`, + ) - const result = await drizzleClient - .select({ user_id: schema.apikeys.user_id }) - .from(schema.apikeys) - .where(eq(schema.apikeys.key, apikey)) - .limit(1) - .then(data => data[0]) + const userId = result.rows[0]?.get_user_id ?? null - cloudlog({ - requestId: c.get('requestId'), - message: 'getUserIdFromApikey - result', - userId: result?.user_id ?? null, - }) + cloudlog({ + requestId: c.get('requestId'), + message: 'getUserIdFromApikey - result', + userId, + }) - return result?.user_id ?? null - } - catch (e: unknown) { - logPgError(c, 'getUserIdFromApikey', e) - return null - } + return userId + } + catch (e: unknown) { + logPgError(c, 'getUserIdFromApikey', e) + return null + } } /** - * Get owner_org from app_id using Postgres/Drizzle - * Equivalent to get_user_main_org_id_by_app_id(app_id) RPC function + * Get owner_org from app_id using the existing Postgres function */ export async function getOwnerOrgByAppId( - c: Context, - appId: string, - drizzleClient: ReturnType, + c: Context, + appId: string, + drizzleClient: ReturnType, ): Promise { - try { - const result = await drizzleClient - .select({ owner_org: schema.apps.owner_org }) - .from(schema.apps) - .where(eq(schema.apps.app_id, appId)) - .limit(1) - .then(data => data[0]) + try { + // Call the existing Postgres function + const result = await drizzleClient.execute<{ get_user_main_org_id_by_app_id: string }>( + sql`SELECT get_user_main_org_id_by_app_id(${appId})`, + ) - return result?.owner_org ?? null - } - catch (e: unknown) { - logPgError(c, 'getOwnerOrgByAppId', e) - return null - } + return result.rows[0]?.get_user_main_org_id_by_app_id ?? null + } + catch (e: unknown) { + logPgError(c, 'getOwnerOrgByAppId', e) + return null + } } /** - * Check minimum rights for a user - * Equivalent to check_min_rights(min_right, user_id, org_id, app_id, channel_id) RPC function + * Check minimum rights for a user using the existing Postgres function */ export async function checkMinRightsPg( - c: Context, - minRight: Database['public']['Enums']['user_min_right'], - userId: string, - orgId: string, - appId: string | null, - channelId: number | null, - drizzleClient: ReturnType, + c: Context, + minRight: Database['public']['Enums']['user_min_right'], + userId: string, + orgId: string, + appId: string | null, + channelId: number | null, + drizzleClient: ReturnType, ): Promise { - try { - if (!userId) { - cloudlog({ - requestId: c.get('requestId'), - message: 'checkMinRightsPg - userId is null', - }) - return false - } + try { + if (!userId) { + cloudlog({ + requestId: c.get('requestId'), + message: 'checkMinRightsPg - userId is null', + }) + return false + } + + // Call the existing Postgres function + const result = await drizzleClient.execute<{ check_min_rights: boolean }>( + sql`SELECT check_min_rights(${minRight}::user_min_right, ${userId}::uuid, ${orgId}::uuid, ${appId}, ${channelId})`, + ) + + const hasPermission = result.rows[0]?.check_min_rights ?? false - // Get all user rights for this org and user - const userRights = await drizzleClient - .select({ - user_right: schema.org_users.user_right, - app_id: schema.org_users.app_id, - channel_id: schema.org_users.channel_id, - }) - .from(schema.org_users) - .where(and( - eq(schema.org_users.org_id, orgId), - eq(schema.org_users.user_id, userId), - )) - - // Define the right hierarchy - const rightHierarchy: Record = { - 'invite_read': 0, - 'invite_upload': 1, - 'invite_write': 2, - 'invite_admin': 3, - 'invite_super_admin': 3.5, - 'read': 4, - 'upload': 5, - 'write': 6, - 'admin': 7, - 'super_admin': 8, - } - - const minRightLevel = rightHierarchy[minRight] - - // Check if any of the user's rights meet the minimum requirement - for (const userRight of userRights) { - if (!userRight.user_right) - continue - - const userRightLevel = rightHierarchy[userRight.user_right] - - // Check conditions as in the SQL function - const hasOrgWideRight = userRightLevel >= minRightLevel - && userRight.app_id === null - && userRight.channel_id === null - - const hasAppRight = userRightLevel >= minRightLevel - && userRight.app_id === appId - && userRight.channel_id === null - - const hasChannelRight = userRightLevel >= minRightLevel - && userRight.app_id === appId - && userRight.channel_id === channelId - - if (hasOrgWideRight || hasAppRight || hasChannelRight) { cloudlog({ - requestId: c.get('requestId'), - message: 'checkMinRightsPg - permission granted', - minRight, - userRight: userRight.user_right, - hasOrgWideRight, - hasAppRight, - hasChannelRight, + requestId: c.get('requestId'), + message: 'checkMinRightsPg - result', + hasPermission, + minRight, + userId, + orgId, + appId, + channelId, }) - return true - } - } - cloudlog({ - requestId: c.get('requestId'), - message: 'checkMinRightsPg - permission denied', - minRight, - userId, - orgId, - appId, - channelId, - userRightsCount: userRights.length, - }) - - return false - } - catch (e: unknown) { - logPgError(c, 'checkMinRightsPg', e) - return false - } + return hasPermission + } + catch (e: unknown) { + logPgError(c, 'checkMinRightsPg', e) + return false + } } /** - * Check if an API key has the right access to an app - * Equivalent to has_app_right_apikey(appid, right, userid, apikey) RPC function + * Check if an API key has the right access to an app using the existing Postgres function */ export async function hasAppRightApikeyPg( - c: Context, - appId: string, - right: Database['public']['Enums']['user_min_right'], - userId: string, - apikey: string, - drizzleClient: ReturnType, + c: Context, + appId: string, + right: Database['public']['Enums']['user_min_right'], + userId: string, + apikey: string, + drizzleClient: ReturnType, ): Promise { - try { - cloudlog({ - requestId: c.get('requestId'), - message: 'hasAppRightApikeyPg - start', - appId, - right, - userId, - apikeyPrefix: apikey?.substring(0, 15), - }) - - // Get owner_org for the app - const orgId = await getOwnerOrgByAppId(c, appId, drizzleClient) - if (!orgId) { - cloudlog({ - requestId: c.get('requestId'), - message: 'hasAppRightApikeyPg - org not found', - appId, - }) - return false - } + try { + cloudlog({ + requestId: c.get('requestId'), + message: 'hasAppRightApikeyPg - start', + appId, + right, + userId, + apikeyPrefix: apikey?.substring(0, 15), + }) - // Get the apikey record - const apiKeyRecord = await drizzleClient - .select({ - limited_to_orgs: schema.apikeys.limited_to_orgs, - limited_to_apps: schema.apikeys.limited_to_apps, - }) - .from(schema.apikeys) - .where(eq(schema.apikeys.key, apikey)) - .limit(1) - .then(data => data[0]) + // Call the existing Postgres function + const result = await drizzleClient.execute<{ has_app_right_apikey: boolean }>( + sql`SELECT has_app_right_apikey(${appId}, ${right}::user_min_right, ${userId}::uuid, ${apikey})`, + ) - if (!apiKeyRecord) { - cloudlog({ - requestId: c.get('requestId'), - message: 'hasAppRightApikeyPg - apikey not found', - }) - return false - } + const hasPermission = result.rows[0]?.has_app_right_apikey ?? false - // Check if apikey is limited to specific orgs - if (apiKeyRecord.limited_to_orgs && apiKeyRecord.limited_to_orgs.length > 0) { - if (!apiKeyRecord.limited_to_orgs.includes(orgId)) { cloudlog({ - requestId: c.get('requestId'), - message: 'hasAppRightApikeyPg - org restriction denied', - orgId, - limitedToOrgs: apiKeyRecord.limited_to_orgs, + requestId: c.get('requestId'), + message: 'hasAppRightApikeyPg - result', + hasPermission, }) - return false - } - } - // Check if apikey is limited to specific apps - if (apiKeyRecord.limited_to_apps && apiKeyRecord.limited_to_apps.length > 0) { - if (!apiKeyRecord.limited_to_apps.includes(appId)) { - cloudlog({ - requestId: c.get('requestId'), - message: 'hasAppRightApikeyPg - app restriction denied', - appId, - limitedToApps: apiKeyRecord.limited_to_apps, - }) + return hasPermission + } + catch (e: unknown) { + logPgError(c, 'hasAppRightApikeyPg', e) return false - } } - - // Check minimum rights - const hasRights = await checkMinRightsPg(c, right, userId, orgId, appId, null, drizzleClient) - - cloudlog({ - requestId: c.get('requestId'), - message: 'hasAppRightApikeyPg - final result', - hasRights, - }) - - return hasRights - } - catch (e: unknown) { - logPgError(c, 'hasAppRightApikeyPg', e) - return false - } } /** * Get app by app_id with owner_org */ export async function getAppByAppIdPg( - c: Context, - appId: string, - drizzleClient: ReturnType, + c: Context, + appId: string, + drizzleClient: ReturnType, ): Promise<{ app_id: string, owner_org: string } | null> { - try { - const app = await drizzleClient - .select({ - app_id: schema.apps.app_id, - owner_org: schema.apps.owner_org, - }) - .from(schema.apps) - .where(eq(schema.apps.app_id, appId)) - .limit(1) - .then(data => data[0]) - - return app ?? null - } - catch (e: unknown) { - logPgError(c, 'getAppByAppIdPg', e) - return null - } + try { + const app = await drizzleClient + .select({ + app_id: schema.apps.app_id, + owner_org: schema.apps.owner_org, + }) + .from(schema.apps) + .where(eq(schema.apps.app_id, appId)) + .limit(1) + .then(data => data[0]) + + return app ?? null + } + catch (e: unknown) { + logPgError(c, 'getAppByAppIdPg', e) + return null + } } From 4a39a91ec92dd3d0585c3d82ef08a3629419d33e Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Tue, 18 Nov 2025 21:47:06 +0000 Subject: [PATCH 3/3] refactor: clean up function signatures and logging in pg_files.ts --- supabase/functions/_backend/utils/pg_files.ts | 280 +++++++++--------- 1 file changed, 140 insertions(+), 140 deletions(-) diff --git a/supabase/functions/_backend/utils/pg_files.ts b/supabase/functions/_backend/utils/pg_files.ts index 295c1a4ff3..3b8da4224e 100644 --- a/supabase/functions/_backend/utils/pg_files.ts +++ b/supabase/functions/_backend/utils/pg_files.ts @@ -1,181 +1,181 @@ import type { Context } from 'hono' +import type { getDrizzleClient } from './pg.ts' import type { Database } from './supabase.types.ts' import { eq, sql } from 'drizzle-orm' import { cloudlog } from './logging.ts' -import { getDrizzleClient, logPgError } from './pg.ts' +import { logPgError } from './pg.ts' import * as schema from './postgres_schema.ts' /** * Get user_id from apikey using the existing Postgres function */ export async function getUserIdFromApikey( - c: Context, - apikey: string, - drizzleClient: ReturnType, + c: Context, + apikey: string, + drizzleClient: ReturnType, ): Promise { - try { - cloudlog({ - requestId: c.get('requestId'), - message: 'getUserIdFromApikey - querying', - apikeyPrefix: apikey?.substring(0, 15), - }) - - // Call the existing Postgres function - const result = await drizzleClient.execute<{ get_user_id: string }>( - sql`SELECT get_user_id(${apikey})`, - ) - - const userId = result.rows[0]?.get_user_id ?? null - - cloudlog({ - requestId: c.get('requestId'), - message: 'getUserIdFromApikey - result', - userId, - }) - - return userId - } - catch (e: unknown) { - logPgError(c, 'getUserIdFromApikey', e) - return null - } + try { + cloudlog({ + requestId: c.get('requestId'), + message: 'getUserIdFromApikey - querying', + apikeyPrefix: apikey?.substring(0, 15), + }) + + // Call the existing Postgres function + const result = await drizzleClient.execute<{ get_user_id: string }>( + sql`SELECT get_user_id(${apikey})`, + ) + + const userId = result.rows[0]?.get_user_id ?? null + + cloudlog({ + requestId: c.get('requestId'), + message: 'getUserIdFromApikey - result', + userId, + }) + + return userId + } + catch (e: unknown) { + logPgError(c, 'getUserIdFromApikey', e) + return null + } } /** * Get owner_org from app_id using the existing Postgres function */ export async function getOwnerOrgByAppId( - c: Context, - appId: string, - drizzleClient: ReturnType, + c: Context, + appId: string, + drizzleClient: ReturnType, ): Promise { - try { - // Call the existing Postgres function - const result = await drizzleClient.execute<{ get_user_main_org_id_by_app_id: string }>( - sql`SELECT get_user_main_org_id_by_app_id(${appId})`, - ) - - return result.rows[0]?.get_user_main_org_id_by_app_id ?? null - } - catch (e: unknown) { - logPgError(c, 'getOwnerOrgByAppId', e) - return null - } + try { + // Call the existing Postgres function + const result = await drizzleClient.execute<{ get_user_main_org_id_by_app_id: string }>( + sql`SELECT get_user_main_org_id_by_app_id(${appId})`, + ) + + return result.rows[0]?.get_user_main_org_id_by_app_id ?? null + } + catch (e: unknown) { + logPgError(c, 'getOwnerOrgByAppId', e) + return null + } } /** * Check minimum rights for a user using the existing Postgres function */ export async function checkMinRightsPg( - c: Context, - minRight: Database['public']['Enums']['user_min_right'], - userId: string, - orgId: string, - appId: string | null, - channelId: number | null, - drizzleClient: ReturnType, + c: Context, + minRight: Database['public']['Enums']['user_min_right'], + userId: string, + orgId: string, + appId: string | null, + channelId: number | null, + drizzleClient: ReturnType, ): Promise { - try { - if (!userId) { - cloudlog({ - requestId: c.get('requestId'), - message: 'checkMinRightsPg - userId is null', - }) - return false - } - - // Call the existing Postgres function - const result = await drizzleClient.execute<{ check_min_rights: boolean }>( - sql`SELECT check_min_rights(${minRight}::user_min_right, ${userId}::uuid, ${orgId}::uuid, ${appId}, ${channelId})`, - ) - - const hasPermission = result.rows[0]?.check_min_rights ?? false - - cloudlog({ - requestId: c.get('requestId'), - message: 'checkMinRightsPg - result', - hasPermission, - minRight, - userId, - orgId, - appId, - channelId, - }) - - return hasPermission - } - catch (e: unknown) { - logPgError(c, 'checkMinRightsPg', e) - return false + try { + if (!userId) { + cloudlog({ + requestId: c.get('requestId'), + message: 'checkMinRightsPg - userId is null', + }) + return false } + + // Call the existing Postgres function + const result = await drizzleClient.execute<{ check_min_rights: boolean }>( + sql`SELECT check_min_rights(${minRight}::user_min_right, ${userId}::uuid, ${orgId}::uuid, ${appId}, ${channelId})`, + ) + + const hasPermission = result.rows[0]?.check_min_rights ?? false + + cloudlog({ + requestId: c.get('requestId'), + message: 'checkMinRightsPg - result', + hasPermission, + minRight, + userId, + orgId, + appId, + channelId, + }) + + return hasPermission + } + catch (e: unknown) { + logPgError(c, 'checkMinRightsPg', e) + return false + } } /** * Check if an API key has the right access to an app using the existing Postgres function */ export async function hasAppRightApikeyPg( - c: Context, - appId: string, - right: Database['public']['Enums']['user_min_right'], - userId: string, - apikey: string, - drizzleClient: ReturnType, + c: Context, + appId: string, + right: Database['public']['Enums']['user_min_right'], + userId: string, + apikey: string, + drizzleClient: ReturnType, ): Promise { - try { - cloudlog({ - requestId: c.get('requestId'), - message: 'hasAppRightApikeyPg - start', - appId, - right, - userId, - apikeyPrefix: apikey?.substring(0, 15), - }) - - // Call the existing Postgres function - const result = await drizzleClient.execute<{ has_app_right_apikey: boolean }>( - sql`SELECT has_app_right_apikey(${appId}, ${right}::user_min_right, ${userId}::uuid, ${apikey})`, - ) - - const hasPermission = result.rows[0]?.has_app_right_apikey ?? false - - cloudlog({ - requestId: c.get('requestId'), - message: 'hasAppRightApikeyPg - result', - hasPermission, - }) - - return hasPermission - } - catch (e: unknown) { - logPgError(c, 'hasAppRightApikeyPg', e) - return false - } + try { + cloudlog({ + requestId: c.get('requestId'), + message: 'hasAppRightApikeyPg - start', + appId, + right, + userId, + apikeyPrefix: apikey?.substring(0, 15), + }) + + // Call the existing Postgres function + const result = await drizzleClient.execute<{ has_app_right_apikey: boolean }>( + sql`SELECT has_app_right_apikey(${appId}, ${right}::user_min_right, ${userId}::uuid, ${apikey})`, + ) + + const hasPermission = result.rows[0]?.has_app_right_apikey ?? false + + cloudlog({ + requestId: c.get('requestId'), + message: 'hasAppRightApikeyPg - result', + hasPermission, + }) + + return hasPermission + } + catch (e: unknown) { + logPgError(c, 'hasAppRightApikeyPg', e) + return false + } } /** * Get app by app_id with owner_org */ export async function getAppByAppIdPg( - c: Context, - appId: string, - drizzleClient: ReturnType, + c: Context, + appId: string, + drizzleClient: ReturnType, ): Promise<{ app_id: string, owner_org: string } | null> { - try { - const app = await drizzleClient - .select({ - app_id: schema.apps.app_id, - owner_org: schema.apps.owner_org, - }) - .from(schema.apps) - .where(eq(schema.apps.app_id, appId)) - .limit(1) - .then(data => data[0]) - - return app ?? null - } - catch (e: unknown) { - logPgError(c, 'getAppByAppIdPg', e) - return null - } + try { + const app = await drizzleClient + .select({ + app_id: schema.apps.app_id, + owner_org: schema.apps.owner_org, + }) + .from(schema.apps) + .where(eq(schema.apps.app_id, appId)) + .limit(1) + .then(data => data[0]) + + return app ?? null + } + catch (e: unknown) { + logPgError(c, 'getAppByAppIdPg', e) + return null + } } -