From b9a0908331ef140e358aaa1c4f94fc77ef9093df Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 21 Jul 2025 12:45:53 +0800 Subject: [PATCH] Refactor post calls from lib/common/fetch in auth pages to data/fetchers (#36506) * Deprecate use of getWithTimeout, refactor BuildingState and RestoringState to use RQ * Refactor profile-create-mutation to use data/fetchers, and edge-function-status-query to use fetch * Shift post from lib/common/fetch, refactor bucket-object-download-mutation * Address feedback * Minor fix * Refactor post calls from lib/common/fetch in auth pages to data/fetchers * Add missing POST users endpoint + small fix when deleting user via context menu * simplify handleFetchError * allow handleFetchError to accept unknown * non-breaking change --------- Co-authored-by: Alaister Young --- .../interfaces/Auth/Users/UsersV2.tsx | 5 +- apps/studio/data/auth/auth-config-query.ts | 3 +- apps/studio/data/fetchers.ts | 37 +++- apps/studio/lib/self-hosted.ts | 4 +- .../pages/api/platform/auth/[ref]/config.ts | 179 ------------------ .../pages/api/platform/auth/[ref]/invite.ts | 18 +- .../api/platform/auth/[ref]/magiclink.ts | 16 +- .../pages/api/platform/auth/[ref]/otp.ts | 16 +- .../pages/api/platform/auth/[ref]/recover.ts | 16 +- .../pages/api/platform/auth/[ref]/users.ts | 132 ------------- .../api/platform/auth/[ref]/users/index.ts | 27 +++ .../api/platform/pg-meta/[ref]/policies.ts | 54 +----- .../api/platform/pg-meta/[ref]/query/index.ts | 7 +- 13 files changed, 121 insertions(+), 393 deletions(-) delete mode 100644 apps/studio/pages/api/platform/auth/[ref]/config.ts delete mode 100644 apps/studio/pages/api/platform/auth/[ref]/users.ts create mode 100644 apps/studio/pages/api/platform/auth/[ref]/users/index.ts diff --git a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx index 63ee824ffb8..0593a02a59b 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx @@ -587,7 +587,10 @@ export const UsersV2 = () => { visible={!!selectedUserToDelete} selectedUser={selectedUserToDelete} onClose={() => setSelectedUserToDelete(undefined)} - onDeleteSuccess={() => setSelectedUserToDelete(undefined)} + onDeleteSuccess={() => { + if (selectedUserToDelete?.id === selectedUser) setSelectedUser(undefined) + setSelectedUserToDelete(undefined) + }} /> ) diff --git a/apps/studio/data/auth/auth-config-query.ts b/apps/studio/data/auth/auth-config-query.ts index 3a697a7e054..337b30f4bac 100644 --- a/apps/studio/data/auth/auth-config-query.ts +++ b/apps/studio/data/auth/auth-config-query.ts @@ -1,6 +1,7 @@ import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' import type { components } from 'data/api' import { get, handleError } from 'data/fetchers' +import { IS_PLATFORM } from 'lib/constants' import { useCallback } from 'react' import type { ResponseError } from 'types' import { authKeys } from './keys' @@ -40,7 +41,7 @@ export const useAuthConfigQuery = ( authKeys.authConfig(projectRef), ({ signal }) => getProjectAuthConfig({ projectRef }, signal), { - enabled: enabled && typeof projectRef !== 'undefined', + enabled: enabled && IS_PLATFORM && typeof projectRef !== 'undefined', ...options, } ) diff --git a/apps/studio/data/fetchers.ts b/apps/studio/data/fetchers.ts index b2c3d4aaa6f..7b8bb0ef7de 100644 --- a/apps/studio/data/fetchers.ts +++ b/apps/studio/data/fetchers.ts @@ -193,6 +193,39 @@ async function handleFetchResponse(response: Response): Promise { + let resJson: any = {} + + if (response instanceof Error) { + resJson = response + } + + if (response instanceof Response) { + resJson = await response.json() + } + + const status = response instanceof Response ? response.status : undefined + + const message = + resJson.message ?? + resJson.msg ?? + resJson.error ?? + `An error has occurred: ${status ?? 'Unknown error'}` + const retryAfter = + response instanceof Response && response.headers.get('Retry-After') + ? parseInt(response.headers.get('Retry-After')!) + : undefined + + let error = new ResponseError(message, status, undefined, retryAfter) + + // @ts-expect-error - [Alaister] many of our local api routes check `if (response.error)`. + // This is a fix to keep those checks working without breaking changes. + // In future we should check for `if (response instanceof ResponseError)` instead. + error.error = error + + return error +} + /** * To be used only for dashboard API endpoints. Use `fetch` directly if calling a non dashboard API endpoint * @@ -218,9 +251,9 @@ export async function fetchPost( ...otherOptions, signal: abortSignal, }) - if (!response.ok) return handleError(response) + if (!response.ok) return handleFetchError(response) return handleFetchResponse(response) } catch (error) { - return handleError(error) + return handleFetchError(error) } } diff --git a/apps/studio/lib/self-hosted.ts b/apps/studio/lib/self-hosted.ts index b8ab1b779b7..8ce7676a8e1 100644 --- a/apps/studio/lib/self-hosted.ts +++ b/apps/studio/lib/self-hosted.ts @@ -1,11 +1,11 @@ +import { fetchPost } from 'data/fetchers' import { constructHeaders } from 'lib/api/apiHelpers' -import { post } from 'lib/common/fetch' import { PG_META_URL } from 'lib/constants' import type { ResponseError } from 'types' export async function queryPgMetaSelfHosted(sql: string, headersInit?: { [prop: string]: any }) { const headers = constructHeaders(headersInit ?? {}) - const response = await post(`${PG_META_URL}/query`, { query: sql }, { headers }) + const response = await fetchPost(`${PG_META_URL}/query`, { query: sql }, { headers }) if (response.error) { return { error: response.error as ResponseError } diff --git a/apps/studio/pages/api/platform/auth/[ref]/config.ts b/apps/studio/pages/api/platform/auth/[ref]/config.ts deleted file mode 100644 index 796a37165fc..00000000000 --- a/apps/studio/pages/api/platform/auth/[ref]/config.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next' -import apiWrapper from 'lib/api/apiWrapper' -import { get } from 'lib/common/fetch' -import { constructHeaders } from 'lib/api/apiHelpers' - -export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) - -async function handler(req: NextApiRequest, res: NextApiResponse) { - const { method } = req - - switch (method) { - case 'GET': - return handleGetAll(req, res) - case 'POST': - return handlePost(req, res) - default: - res.setHeader('Allow', ['GET', 'POST']) - res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) - } -} - -const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { - const headers = constructHeaders({}) - const url = `${process.env.SUPABASE_URL}/auth/v1/settings` - const { external, disable_signup, mailer_autoconfirm, phone_autoconfirm } = await get(url, { - headers, - }) - // Platform only API - return res.status(200).json({ - app_version: '', - config_override_id: '', - project_id: '', - jwt_secret_encrypted: '', - isFreeTier: true, - SITE_URL: '', - OPERATOR_TOKEN: null, - DISABLE_SIGNUP: disable_signup ?? false, - RATE_LIMIT_HEADER: null, - JWT_EXP: 3600, - JWT_AUD: '', - JWT_DEFAULT_GROUP_NAME: '', - URI_ALLOW_LIST: '', - MAILER_AUTOCONFIRM: mailer_autoconfirm ?? false, - MAILER_OTP_EXP: 3600, - MAILER_OTP_LENGTH: 6, - MAILER_URLPATHS_INVITE: '', - MAILER_URLPATHS_CONFIRMATION: '', - MAILER_URLPATHS_RECOVERY: '', - MAILER_URLPATHS_EMAIL_CHANGE: '', - SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION: false, - SMTP_ADMIN_EMAIL: '', - SMTP_HOST: null, - SMTP_PORT: null, - SMTP_USER: null, - SMTP_PASS: null, - SMTP_MAX_FREQUENCY: 0, - EXTERNAL_ANONYMOUS_USERS_ENABLED: external?.anonymous_users ?? true, - EXTERNAL_EMAIL_ENABLED: external?.email ?? true, - EXTERNAL_PHONE_ENABLED: external?.phone ?? true, - EXTERNAL_APPLE_ENABLED: external?.apple ?? false, - EXTERNAL_APPLE_CLIENT_ID: null, - EXTERNAL_APPLE_SECRET: null, - EXTERNAL_AZURE_ENABLED: external?.azure ?? false, - EXTERNAL_AZURE_CLIENT_ID: null, - EXTERNAL_AZURE_SECRET: null, - EXTERNAL_AZURE_URL: null, - EXTERNAL_BITBUCKET_ENABLED: external?.bitbucket ?? false, - EXTERNAL_BITBUCKET_CLIENT_ID: null, - EXTERNAL_BITBUCKET_SECRET: null, - EXTERNAL_DISCORD_ENABLED: external?.discord ?? false, - EXTERNAL_DISCORD_CLIENT_ID: null, - EXTERNAL_DISCORD_SECRET: null, - EXTERNAL_FACEBOOK_ENABLED: external?.facebook ?? false, - EXTERNAL_FACEBOOK_CLIENT_ID: null, - EXTERNAL_FACEBOOK_SECRET: null, - EXTERNAL_FIGMA_ENABLED: external?.figma ?? false, - EXTERNAL_FIGMA_CLIENT_ID: null, - EXTERNAL_FIGMA_SECRET: null, - EXTERNAL_GITHUB_ENABLED: external?.github ?? false, - EXTERNAL_GITHUB_CLIENT_ID: null, - EXTERNAL_GITHUB_SECRET: null, - EXTERNAL_GITLAB_ENABLED: external?.gitlab ?? false, - EXTERNAL_GITLAB_CLIENT_ID: null, - EXTERNAL_GITLAB_SECRET: null, - EXTERNAL_GITLAB_REDIRECT_URI: null, - EXTERNAL_GOOGLE_ENABLED: external?.google ?? false, - EXTERNAL_GOOGLE_CLIENT_ID: null, - EXTERNAL_GOOGLE_SECRET: null, - EXTERNAL_KAKAO_ENABLED: external?.kakao ?? false, - EXTERNAL_KAKAO_CLIENT_ID: null, - EXTERNAL_KAKAO_SECRET: null, - EXTERNAL_KEYCLOAK_ENABLED: external?.keycloak ?? false, - EXTERNAL_KEYCLOAK_CLIENT_ID: null, - EXTERNAL_KEYCLOAK_SECRET: null, - EXTERNAL_KEYCLOAK_URL: null, - EXTERNAL_NOTION_ENABLED: external?.notion ?? false, - EXTERNAL_NOTION_CLIENT_ID: null, - EXTERNAL_NOTION_SECRET: null, - EXTERNAL_SPOTIFY_ENABLED: external?.spotify ?? false, - EXTERNAL_SPOTIFY_CLIENT_ID: null, - EXTERNAL_SPOTIFY_SECRET: null, - EXTERNAL_SLACK_OIDC_ENABLED: external?.slack ?? false, - EXTERNAL_SLACK_OIDC_CLIENT_ID: null, - EXTERNAL_SLACK_OIDC_SECRET: null, - EXTERNAL_TWITTER_ENABLED: external?.twitter ?? false, - EXTERNAL_TWITTER_CLIENT_ID: null, - EXTERNAL_TWITTER_SECRET: null, - EXTERNAL_TWITCH_ENABLED: external?.twitch ?? false, - EXTERNAL_TWITCH_CLIENT_ID: null, - EXTERNAL_TWITCH_SECRET: null, - EXTERNAL_WORKOS_ENABLED: external?.workos ?? false, - EXTERNAL_WORKOS_CLIENT_ID: null, - EXTERNAL_WORKOS_SECRET: null, - EXTERNAL_WORKOS_URL: null, - EXTERNAL_ZOOM_ENABLED: external?.zoom ?? false, - EXTERNAL_ZOOM_CLIENT_ID: null, - EXTERNAL_ZOOM_SECRET: null, - MAILER_SUBJECTS_INVITE: 'You have been invited', - MAILER_SUBJECTS_CONFIRMATION: 'Confirm Your Signup', - MAILER_SUBJECTS_RECOVERY: 'Reset Your Password', - MAILER_SUBJECTS_EMAIL_CHANGE: 'Confirm Email Change', - MAILER_SUBJECTS_MAGIC_LINK: 'Your Magic Link', - MAILER_SUBJECTS_REAUTHENTICATION: 'Confirm Reauthentication', - MAILER_TEMPLATES_INVITE: null, - MAILER_TEMPLATES_INVITE_CONTENT: - '

You have been invited

\n\n

You have been invited to create a user on {{ .SiteURL }}. Follow this link to accept the invite:

\n

Accept the invite

', - MAILER_TEMPLATES_CONFIRMATION: null, - MAILER_TEMPLATES_CONFIRMATION_CONTENT: - '

Confirm your email

\n\n

Follow this link to confirm your email:

\n\n

Confirm your email address

', - MAILER_TEMPLATES_RECOVERY: null, - MAILER_TEMPLATES_RECOVERY_CONTENT: - '

Reset Password

\n\n

Follow this link to reset the password for your user:

\n

Reset Password

', - MAILER_TEMPLATES_EMAIL_CHANGE: null, - MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT: - '

Confirm Change of Email

\n\n

Follow this link to confirm the update of your email from {{ .Email }} to {{ .NewEmail }}:

\n

Change Email

', - MAILER_TEMPLATES_MAGIC_LINK: null, - MAILER_TEMPLATES_MAGIC_LINK_CONTENT: - '

Magic Link

\n\n

Follow this link to login:

\n

Log In

', - PASSWORD_MIN_LENGTH: 6, - SMTP_SENDER_NAME: null, - SMS_AUTOCONFIRM: phone_autoconfirm ?? false, - SMS_MAX_FREQUENCY: 0, - SMS_OTP_EXP: 60, - SMS_OTP_LENGTH: 6, - SMS_PROVIDER: 'twilio', - SMS_TWILIO_ACCOUNT_SID: null, - SMS_TWILIO_AUTH_TOKEN: null, - SMS_TWILIO_CONTENT_SID: null, - SMS_TWILIO_MESSAGE_SERVICE_SID: null, - SMS_TWILIO_VERIFY_ACCOUNT_SID: null, - SMS_TWILIO_VERIFY_AUTH_TOKEN: null, - SMS_TWILIO_VERIFY_MESSAGE_SERVICE_SID: null, - SMS_TEMPLATE: 'Your code is {{ .Code }}', - SECURITY_CAPTCHA_ENABLED: false, - SECURITY_CAPTCHA_PROVIDER: 'hcaptcha', - SECURITY_CAPTCHA_SECRET: null, - SECURITY_MANUAL_LINKING_ENABLED: false, - SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: '10', - RATE_LIMIT_EMAIL_SENT: 0, - RATE_LIMIT_SMS_SENT: 0, - RATE_LIMIT_ANONYMOUS_USERS: 0, - MAILER_SECURE_EMAIL_CHANGE_ENABLED: true, - SMS_MESSAGEBIRD_ACCESS_KEY: null, - SMS_MESSAGEBIRD_ORIGINATOR: null, - SMS_VONAGE_API_KEY: null, - SMS_VONAGE_API_SECRET: null, - SMS_VONAGE_FROM: null, - SMS_TEXTLOCAL_API_KEY: null, - SMS_TEXTLOCAL_SENDER: null, - MAILER_TEMPLATES_REAUTHENTICATION: null, - MAILER_TEMPLATES_REAUTHENTICATION_CONTENT: - '

Confirm reauthentication

Enter the code: {{ .Token }}

', - }) -} - -const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { - // Platform only API - return res.status(200).json({}) -} diff --git a/apps/studio/pages/api/platform/auth/[ref]/invite.ts b/apps/studio/pages/api/platform/auth/[ref]/invite.ts index 5e626df5f59..662df08ded0 100644 --- a/apps/studio/pages/api/platform/auth/[ref]/invite.ts +++ b/apps/studio/pages/api/platform/auth/[ref]/invite.ts @@ -1,7 +1,11 @@ +import { createClient } from '@supabase/supabase-js' import { NextApiRequest, NextApiResponse } from 'next' -import apiWrapper from 'lib/api/apiWrapper' + +import { fetchPost } from 'data/fetchers' import { constructHeaders } from 'lib/api/apiHelpers' -import { post } from 'lib/common/fetch' +import apiWrapper from 'lib/api/apiWrapper' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) @@ -25,6 +29,12 @@ const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { }) const url = `${process.env.SUPABASE_URL}/auth/v1/invite` const payload = { email: req.body.email } - const response = await post(url, payload, { headers }) - return res.status(200).json(response) + + const response = await fetchPost(url, payload, { headers }) + if (response.error) { + const { code, message } = response.error + return res.status(code).json({ message }) + } else { + return res.status(200).json(response) + } } diff --git a/apps/studio/pages/api/platform/auth/[ref]/magiclink.ts b/apps/studio/pages/api/platform/auth/[ref]/magiclink.ts index 50c25e9199e..2063d47447e 100644 --- a/apps/studio/pages/api/platform/auth/[ref]/magiclink.ts +++ b/apps/studio/pages/api/platform/auth/[ref]/magiclink.ts @@ -1,10 +1,8 @@ import { NextApiRequest, NextApiResponse } from 'next' -import SqlString from 'sqlstring' -import apiWrapper from 'lib/api/apiWrapper' +import { fetchPost } from 'data/fetchers' import { constructHeaders } from 'lib/api/apiHelpers' -import { post } from 'lib/common/fetch' -import { tryParseInt } from 'lib/helpers' +import apiWrapper from 'lib/api/apiWrapper' export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) @@ -28,6 +26,12 @@ const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { }) const url = `${process.env.SUPABASE_URL}/auth/v1/magiclink` const payload = { email: req.body.email } - const response = await post(url, payload, { headers }) - return res.status(200).json(response) + + const response = await fetchPost(url, payload, { headers }) + if (response.error) { + const { code, message } = response.error + return res.status(code).json({ message }) + } else { + return res.status(200).json(response) + } } diff --git a/apps/studio/pages/api/platform/auth/[ref]/otp.ts b/apps/studio/pages/api/platform/auth/[ref]/otp.ts index e9e8892a388..ca70cd0298b 100644 --- a/apps/studio/pages/api/platform/auth/[ref]/otp.ts +++ b/apps/studio/pages/api/platform/auth/[ref]/otp.ts @@ -1,10 +1,8 @@ import { NextApiRequest, NextApiResponse } from 'next' -import SqlString from 'sqlstring' -import apiWrapper from 'lib/api/apiWrapper' +import { fetchPost } from 'data/fetchers' import { constructHeaders } from 'lib/api/apiHelpers' -import { post } from 'lib/common/fetch' -import { tryParseInt } from 'lib/helpers' +import apiWrapper from 'lib/api/apiWrapper' export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) @@ -28,6 +26,12 @@ const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { }) const url = `${process.env.SUPABASE_URL}/auth/v1/otp` const payload = { phone: req.body.phone } - const response = await post(url, payload, { headers }) - return res.status(200).json(response) + + const response = await fetchPost(url, payload, { headers }) + if (response.error) { + const { code, message } = response.error + return res.status(code).json({ message }) + } else { + return res.status(200).json(response) + } } diff --git a/apps/studio/pages/api/platform/auth/[ref]/recover.ts b/apps/studio/pages/api/platform/auth/[ref]/recover.ts index 6a05ff85708..50e4c4afa21 100644 --- a/apps/studio/pages/api/platform/auth/[ref]/recover.ts +++ b/apps/studio/pages/api/platform/auth/[ref]/recover.ts @@ -1,10 +1,8 @@ import { NextApiRequest, NextApiResponse } from 'next' -import SqlString from 'sqlstring' -import apiWrapper from 'lib/api/apiWrapper' +import { fetchPost } from 'data/fetchers' import { constructHeaders } from 'lib/api/apiHelpers' -import { post } from 'lib/common/fetch' -import { tryParseInt } from 'lib/helpers' +import apiWrapper from 'lib/api/apiWrapper' export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) @@ -28,6 +26,12 @@ const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { }) const url = `${process.env.SUPABASE_URL}/auth/v1/recover` const payload = { email: req.body.email } - const response = await post(url, payload, { headers }) - return res.status(200).json(response) + + const response = await fetchPost(url, payload, { headers }) + if (response.error) { + const { code, message } = response.error + return res.status(code).json({ message }) + } else { + return res.status(200).json(response) + } } diff --git a/apps/studio/pages/api/platform/auth/[ref]/users.ts b/apps/studio/pages/api/platform/auth/[ref]/users.ts deleted file mode 100644 index 0fbcf299d36..00000000000 --- a/apps/studio/pages/api/platform/auth/[ref]/users.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next' -import SqlString from 'sqlstring' -import { createClient } from '@supabase/supabase-js' - -import { post } from 'lib/common/fetch' -import { tryParseInt } from 'lib/helpers' -import { PG_META_URL } from 'lib/constants' -import apiWrapper from 'lib/api/apiWrapper' -import { constructHeaders } from 'lib/api/apiHelpers' - -const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) - -export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) - -async function handler(req: NextApiRequest, res: NextApiResponse) { - const { method } = req - - switch (method) { - case 'GET': - return handleGetAll(req, res) - case 'POST': - return handlePost(req, res) - case 'DELETE': - return handleDelete(req, res) - default: - res.setHeader('Allow', ['GET', 'POST', 'DELETE']) - res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) - } -} - -const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { - const headers = constructHeaders(req.headers) - const { keywords, limit, offset, verified } = req.query - const limitInt = tryParseInt(limit as string) || 10 - const offsetInt = tryParseInt(offset as string) || 0 - const hasValidKeywords = keywords && keywords != '' - const hasVerifiedValue = verified && verified != '' - - let queryCount = '' - let queryUsers = '' - - if (hasValidKeywords && !hasVerifiedValue) { - queryCount = SqlString.format( - 'SELECT count(*) from auth.users WHERE (email ilike ? OR id::text ilike ?);', - [`%${keywords}%`, `%${keywords}%`] - ) - queryUsers = SqlString.format( - 'SELECT * from auth.users WHERE (email ilike ? OR id::text ilike ?) ORDER BY created_at DESC LIMIT ? OFFSET ?;', - [`%${keywords}%`, `%${keywords}%`, limitInt, offsetInt] - ) - } - - if (!hasValidKeywords && hasVerifiedValue) { - if (verified === 'verified') { - queryCount = SqlString.format( - 'SELECT count(*) from auth.users WHERE (email_confirmed_at IS NOT NULL or phone_confirmed_at IS NOT NULL);' - ) - queryUsers = SqlString.format( - 'SELECT * from auth.users WHERE (email_confirmed_at IS NOT NULL or phone_confirmed_at IS NOT NULL) ORDER BY created_at DESC LIMIT ? OFFSET ?;', - [limitInt, offsetInt] - ) - } - if (verified === 'unverified') { - queryCount = SqlString.format( - 'SELECT count(*) from auth.users WHERE (email_confirmed_at IS NULL AND phone_confirmed_at IS NULL);' - ) - queryUsers = SqlString.format( - 'SELECT * from auth.users WHERE (email_confirmed_at IS NULL AND phone_confirmed_at IS NULL) ORDER BY created_at DESC LIMIT ? OFFSET ?;', - [limitInt, offsetInt] - ) - } - } - - if (hasValidKeywords && hasVerifiedValue) { - if (verified === 'verified') { - queryCount = SqlString.format( - 'SELECT count(*) from auth.users WHERE (email_confirmed_at IS NOT NULL or phone_confirmed_at IS NOT NULL) AND (email ilike ? OR id::text ilike ?);', - [`%${keywords}%`, `%${keywords}%`] - ) - queryUsers = SqlString.format( - 'SELECT * from auth.users WHERE (email_confirmed_at IS NOT NULL or phone_confirmed_at IS NOT NULL) AND (email ilike ? OR id::text ilike ?) ORDER BY created_at DESC LIMIT ? OFFSET ?;', - [`%${keywords}%`, `%${keywords}%`, limitInt, offsetInt] - ) - } - if (verified === 'unverified') { - queryCount = SqlString.format( - 'SELECT count(*) from auth.users WHERE (email_confirmed_at IS NULL AND phone_confirmed_at IS NULL) AND (email ilike ? OR id::text ilike ?);', - [`%${keywords}%`, `%${keywords}%`] - ) - queryUsers = SqlString.format( - 'SELECT * from auth.users WHERE (email_confirmed_at IS NULL AND phone_confirmed_at IS NULL) AND (email ilike ? OR id::text ilike ?) ORDER BY created_at DESC LIMIT ? OFFSET ?;', - [`%${keywords}%`, `%${keywords}%`, limitInt, offsetInt] - ) - } - } - - if (!hasValidKeywords && !hasVerifiedValue) { - queryCount = 'SELECT count(*) from auth.users;' - queryUsers = SqlString.format( - 'SELECT * from auth.users ORDER BY created_at DESC LIMIT ? OFFSET ?;', - [limitInt, offsetInt] - ) - } - - const [getTotal, getUsers] = await Promise.all([ - post(`${PG_META_URL}/query`, { query: queryCount }, { headers }), - post(`${PG_META_URL}/query`, { query: queryUsers }, { headers }), - ]) - - let total = 0 - if (getTotal && (getTotal as any[]).length > 0) { - total = (getTotal[0] as any).count - } - - return res.status(200).json({ total, users: getUsers }) -} - -const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { - const { email, password, email_confirm } = req.body - const { data, error } = await supabase.auth.admin.createUser({ email, password, email_confirm }) - - if (error) return res.status(400).json({ error: { message: error.message } }) - return res.status(200).json(data.user) -} - -const handleDelete = async (req: NextApiRequest, res: NextApiResponse) => { - const { id } = req.body - const { data, error } = await supabase.auth.admin.deleteUser(id) - - if (error) return res.status(400).json({ error: { message: error.message } }) - return res.status(200).json(data.user) -} diff --git a/apps/studio/pages/api/platform/auth/[ref]/users/index.ts b/apps/studio/pages/api/platform/auth/[ref]/users/index.ts new file mode 100644 index 00000000000..a4c2239d381 --- /dev/null +++ b/apps/studio/pages/api/platform/auth/[ref]/users/index.ts @@ -0,0 +1,27 @@ +import { createClient } from '@supabase/supabase-js' +import { NextApiRequest, NextApiResponse } from 'next' + +import apiWrapper from 'lib/api/apiWrapper' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'POST': + return handlePost(req, res) + default: + res.setHeader('Allow', ['POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { + const { data, error } = await supabase.auth.admin.createUser(req.body) + + if (error) return res.status(400).json({ error: { message: error.message } }) + return res.status(200).json(data.user) +} diff --git a/apps/studio/pages/api/platform/pg-meta/[ref]/policies.ts b/apps/studio/pages/api/platform/pg-meta/[ref]/policies.ts index 479c41efd5e..51ded58c9d3 100644 --- a/apps/studio/pages/api/platform/pg-meta/[ref]/policies.ts +++ b/apps/studio/pages/api/platform/pg-meta/[ref]/policies.ts @@ -1,8 +1,8 @@ -import { NextApiRequest, NextApiResponse } from 'next' +import { constructHeaders } from 'lib/api/apiHelpers' import apiWrapper from 'lib/api/apiWrapper' -import { constructHeaders, toSnakeCase } from 'lib/api/apiHelpers' +import { get } from 'lib/common/fetch' import { PG_META_URL } from 'lib/constants' -import { delete_, get, patch, post } from 'lib/common/fetch' +import { NextApiRequest, NextApiResponse } from 'next' export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler, { withAuth: true }) @@ -13,12 +13,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { switch (method) { case 'GET': return handleGetAll(req, res) - case 'POST': - return handlePost(req, res) - case 'PATCH': - return handlePatch(req, res) - case 'DELETE': - return handleDelete(req, res) default: res.setHeader('Allow', ['GET', 'POST', 'PATCH', 'DELETE']) res.status(405).json({ error: { message: `Method ${method} Not Allowed` } }) @@ -35,45 +29,3 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { } return res.status(200).json(response) } - -const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { - const headers = constructHeaders(req.headers) - const payload = toSnakeCase(req.body) - const response = await post(`${PG_META_URL}/policies`, payload, { - headers, - }) - - if (response.error) { - console.error('Policies POST:', response.error) - return res.status(400).json({ error: response.error }) - } - - return res.status(200).json(response) -} - -const handlePatch = async (req: NextApiRequest, res: NextApiResponse) => { - const headers = constructHeaders(req.headers) - const payload = toSnakeCase(req.body) - const response = await patch(`${PG_META_URL}/policies/${req.query.id}`, payload, { - headers, - }) - - if (response.error) { - console.error('Policies PATCH:', response) - return res.status(400).json({ error: response.error }) - } - - return res.status(200).json(response) -} - -const handleDelete = async (req: NextApiRequest, res: NextApiResponse) => { - const headers = constructHeaders(req.headers) - const response = await delete_(`${PG_META_URL}/policies/${req.query.id}`, {}, { headers }) - - if (response.error) { - console.error('Policies DELETE:', response.error) - return res.status(400).json({ error: response.error }) - } - - return res.status(200).json(response) -} diff --git a/apps/studio/pages/api/platform/pg-meta/[ref]/query/index.ts b/apps/studio/pages/api/platform/pg-meta/[ref]/query/index.ts index fc76638b99f..3a88a32927d 100644 --- a/apps/studio/pages/api/platform/pg-meta/[ref]/query/index.ts +++ b/apps/studio/pages/api/platform/pg-meta/[ref]/query/index.ts @@ -1,6 +1,6 @@ +import { fetchPost } from 'data/fetchers' import { constructHeaders } from 'lib/api/apiHelpers' import apiWrapper from 'lib/api/apiWrapper' -import { post } from 'lib/common/fetch' import { PG_META_URL } from 'lib/constants' import { NextApiRequest, NextApiResponse } from 'next' @@ -22,10 +22,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { const { query } = req.body const headers = constructHeaders(req.headers) - const response = await post(`${PG_META_URL}/query`, { query }, { headers }) + const response = await fetchPost(`${PG_META_URL}/query`, { query }, { headers }) if (response.error) { - return res.status(400).json(response.error) + const { code, message } = response.error + return res.status(code).json({ message, formattedError: message }) } else { return res.status(200).json(response) }