diff --git a/apps/server/prisma/seed.js b/apps/server/prisma/seed.js index 837ad46..f2acdcb 100644 --- a/apps/server/prisma/seed.js +++ b/apps/server/prisma/seed.js @@ -151,6 +151,44 @@ const defaultConfigs = [ ]; const defaultAuthProviders = [ + { + name: "google", + displayName: "Google", + type: "oauth2", + icon: "FcGoogle", + enabled: false, + issuerUrl: "https://accounts.google.com", + authorizationEndpoint: "/o/oauth2/v2/auth", + tokenEndpoint: "/o/oauth2/token", + userInfoEndpoint: "https://www.googleapis.com/oauth2/v3/userinfo", + scope: "openid profile email", + sortOrder: 1, + metadata: JSON.stringify({ + description: "Sign in with your Google account", + docs: "https://developers.google.com/identity/protocols/oauth2", + supportsDiscovery: true, + authMethod: "body" + }) + }, + { + name: "discord", + displayName: "Discord", + type: "oauth2", + icon: "FaDiscord", + enabled: false, + issuerUrl: "https://discord.com", + authorizationEndpoint: "/oauth2/authorize", + tokenEndpoint: "/api/oauth2/token", + userInfoEndpoint: "/api/users/@me", + scope: "identify email", + sortOrder: 2, + metadata: JSON.stringify({ + description: "Sign in with your Discord account", + docs: "https://discord.com/developers/docs/topics/oauth2", + supportsDiscovery: false, + authMethod: "body" + }) + }, { name: "github", displayName: "GitHub", @@ -162,7 +200,7 @@ const defaultAuthProviders = [ tokenEndpoint: "/access_token", userInfoEndpoint: "https://api.github.com/user", // GitHub usa URL absoluta para userInfo scope: "user:email", - sortOrder: 1, + sortOrder: 3, metadata: JSON.stringify({ description: "Sign in with your GitHub account", docs: "https://docs.github.com/en/developers/apps/building-oauth-apps", @@ -180,7 +218,7 @@ const defaultAuthProviders = [ tokenEndpoint: "/oauth/token", userInfoEndpoint: "/userinfo", scope: "openid profile email", - sortOrder: 2, + sortOrder: 4, metadata: JSON.stringify({ description: "Sign in with Auth0 - Replace 'your-tenant' with your Auth0 domain", docs: "https://auth0.com/docs/get-started/authentication-and-authorization-flow", @@ -198,7 +236,7 @@ const defaultAuthProviders = [ tokenEndpoint: "/oauth2/token", userInfoEndpoint: "/oauth2/user_profile", scope: "openid profile email", - sortOrder: 3, + sortOrder: 5, metadata: JSON.stringify({ description: "Sign in with Kinde - Replace 'your-tenant' with your Kinde domain", docs: "https://kinde.com/docs/developer-tools/about/", @@ -216,7 +254,7 @@ const defaultAuthProviders = [ tokenEndpoint: "/oauth/v2/token", userInfoEndpoint: "/oidc/v1/userinfo", scope: "openid profile email", - sortOrder: 4, + sortOrder: 6, metadata: JSON.stringify({ description: "Sign in with Zitadel - Replace with your Zitadel instance URL", docs: "https://zitadel.com/docs/guides/integrate/login/oidc", @@ -235,7 +273,7 @@ const defaultAuthProviders = [ tokenEndpoint: "/application/o/token/", userInfoEndpoint: "/application/o/userinfo/", scope: "openid profile email", - sortOrder: 5, + sortOrder: 7, metadata: JSON.stringify({ description: "Sign in with Authentik - Replace with your Authentik instance URL", docs: "https://goauthentik.io/docs/providers/oauth2", @@ -253,7 +291,7 @@ const defaultAuthProviders = [ tokenEndpoint: "/oauth/token", userInfoEndpoint: "/identity/resources/users/v2/me", scope: "openid profile email", - sortOrder: 6, + sortOrder: 8, metadata: JSON.stringify({ description: "Sign in with Frontegg - Replace 'your-tenant' with your Frontegg tenant", docs: "https://docs.frontegg.com", diff --git a/apps/server/src/modules/auth-providers/controller.ts b/apps/server/src/modules/auth-providers/controller.ts index 8a36241..49c70d4 100644 --- a/apps/server/src/modules/auth-providers/controller.ts +++ b/apps/server/src/modules/auth-providers/controller.ts @@ -166,8 +166,12 @@ export class AuthProvidersController { // Para providers customizados, aplica validação normal try { + console.log(`[Controller] Updating custom provider with data:`, data); + // Valida usando o schema do Zod const validatedData = UpdateAuthProviderSchema.parse(data); + console.log(`[Controller] Validation passed, validated data:`, validatedData); + const provider = await this.authProvidersService.updateProvider(id, validatedData); return reply.send({ @@ -176,6 +180,7 @@ export class AuthProvidersController { }); } catch (validationError) { console.error("Validation error for custom provider:", validationError); + console.error("Raw data that failed validation:", data); return reply.status(400).send({ success: false, error: "Invalid data provided", @@ -297,26 +302,53 @@ export class AuthProvidersController { } async callback(request: FastifyRequest<{ Params: { provider: string }; Querystring: any }>, reply: FastifyReply) { + console.log(`[Controller] Callback called for provider: ${request.params.provider}`); + console.log(`[Controller] Query params:`, request.query); + console.log(`[Controller] Headers:`, { + host: request.headers.host, + "x-forwarded-proto": request.headers["x-forwarded-proto"], + "x-forwarded-host": request.headers["x-forwarded-host"], + }); + try { const { provider: providerName } = request.params; const query = request.query as any; const { code, state, error } = query; + console.log(`[Controller] Extracted params:`, { providerName, code, state, error }); + console.log(`[Controller] All query params:`, query); + const requestContext = { protocol: (request.headers["x-forwarded-proto"] as string) || request.protocol, host: (request.headers["x-forwarded-host"] as string) || (request.headers.host as string), }; const baseUrl = `${requestContext.protocol}://${requestContext.host}`; + console.log(`[Controller] Request context:`, requestContext); + console.log(`[Controller] Base URL:`, baseUrl); + if (error) { console.error(`OAuth error from ${providerName}:`, error); return reply.redirect(`${baseUrl}/login?error=oauth_error&provider=${providerName}`); } - if (!code || !state) { + if (!code) { + console.error(`Missing code parameter for ${providerName}`); + return reply.redirect(`${baseUrl}/login?error=missing_code&provider=${providerName}`); + } + + // Validação de parâmetros obrigatórios + const requiredParams = { code: !!code, state: !!state }; + const missingParams = Object.entries(requiredParams) + .filter(([, hasValue]) => !hasValue) + .map(([param]) => param); + + if (missingParams.length > 0) { + console.error(`Missing parameters for ${providerName}:`, missingParams); return reply.redirect(`${baseUrl}/login?error=missing_parameters&provider=${providerName}`); } + console.log(`[Controller] Calling handleCallback for ${providerName}`); const result = await this.authProvidersService.handleCallback(providerName, code, state, requestContext); const jwt = await request.jwtSign({ @@ -334,6 +366,7 @@ export class AuthProvidersController { const redirectUrl = result.redirectUrl || "/dashboard"; const fullRedirectUrl = redirectUrl.startsWith("http") ? redirectUrl : `${baseUrl}${redirectUrl}`; + console.log(`[Controller] Redirecting to:`, fullRedirectUrl); return reply.redirect(fullRedirectUrl); } catch (error) { console.error(`Error in ${request.params.provider} callback:`, error); diff --git a/apps/server/src/modules/auth-providers/dto.ts b/apps/server/src/modules/auth-providers/dto.ts index 11c115a..1994206 100644 --- a/apps/server/src/modules/auth-providers/dto.ts +++ b/apps/server/src/modules/auth-providers/dto.ts @@ -36,21 +36,35 @@ export const ManualEndpointsSchema = BaseAuthProviderSchema.extend({ // Schema principal que aceita ambos os modos export const CreateAuthProviderSchema = BaseAuthProviderSchema.extend({ issuerUrl: z.string().url("Invalid issuer URL").optional(), - authorizationEndpoint: z.string().min(1).optional(), - tokenEndpoint: z.string().min(1).optional(), - userInfoEndpoint: z.string().min(1).optional(), + authorizationEndpoint: z.string().optional(), + tokenEndpoint: z.string().optional(), + userInfoEndpoint: z.string().optional(), }).refine( (data) => { - // Modo discovery: deve ter issuerUrl e não ter endpoints customizados const hasIssuerUrl = !!data.issuerUrl; - const hasCustomEndpoints = !!(data.authorizationEndpoint && data.tokenEndpoint && data.userInfoEndpoint); + const hasAnyCustomEndpoint = !!( + data.authorizationEndpoint?.trim() || + data.tokenEndpoint?.trim() || + data.userInfoEndpoint?.trim() + ); - // Deve ter ou issuerUrl OU todos os endpoints customizados - return hasIssuerUrl || hasCustomEndpoints; + // Deve ter pelo menos issuerUrl OU todos os endpoints customizados + if (hasIssuerUrl && !hasAnyCustomEndpoint) return true; // Modo discovery + + if (hasAnyCustomEndpoint) { + const hasAllCustomEndpoints = !!( + data.authorizationEndpoint?.trim() && + data.tokenEndpoint?.trim() && + data.userInfoEndpoint?.trim() + ); + return hasAllCustomEndpoints; // Precisa ter todos os 3 endpoints + } + + return false; // Precisa ter pelo menos um dos dois modos }, { message: - "Either provide issuerUrl for automatic discovery OR all three custom endpoints (authorization, token, userInfo)", + "Either provide issuerUrl for automatic discovery OR all three custom endpoints (authorization, token, userInfo).", } ); @@ -68,25 +82,40 @@ export const UpdateAuthProviderSchema = z clientId: z.string().min(1).optional(), clientSecret: z.string().min(1).optional(), issuerUrl: z.string().url().optional(), - authorizationEndpoint: z.string().min(1).optional(), - tokenEndpoint: z.string().min(1).optional(), - userInfoEndpoint: z.string().min(1).optional(), + authorizationEndpoint: z.string().optional(), + tokenEndpoint: z.string().optional(), + userInfoEndpoint: z.string().optional(), }) .refine( (data) => { - // Se está atualizando endpoints, deve seguir a mesma regra + // Se não está alterando nenhum campo de configuração, permite const hasIssuerUrl = !!data.issuerUrl; - const hasCustomEndpoints = !!(data.authorizationEndpoint || data.tokenEndpoint || data.userInfoEndpoint); + const hasAnyCustomEndpoint = !!( + data.authorizationEndpoint?.trim() || + data.tokenEndpoint?.trim() || + data.userInfoEndpoint?.trim() + ); - // Se nenhum dos dois foi fornecido, está OK (não está alterando o modo) - if (!hasIssuerUrl && !hasCustomEndpoints) return true; + // Se não está alterando nenhum campo de configuração, permite + if (!hasIssuerUrl && !hasAnyCustomEndpoint) return true; - // Se forneceu um, não pode fornecer o outro - return hasIssuerUrl !== hasCustomEndpoints; + // Se está fornecendo apenas issuerUrl, permite (modo discovery) + if (hasIssuerUrl && !hasAnyCustomEndpoint) return true; + + // Se está fornecendo endpoints customizados, deve fornecer todos os 3 + if (hasAnyCustomEndpoint) { + const hasAllCustomEndpoints = !!( + data.authorizationEndpoint?.trim() && + data.tokenEndpoint?.trim() && + data.userInfoEndpoint?.trim() + ); + return hasAllCustomEndpoints; + } + + return true; }, { - message: - "Either provide issuerUrl for automatic discovery OR all three custom endpoints (authorization, token, userInfo)", + message: "When providing custom endpoints, all three endpoints (authorization, token, userInfo) are required.", } ); diff --git a/apps/server/src/modules/auth-providers/provider-manager.ts b/apps/server/src/modules/auth-providers/provider-manager.ts index 0df473a..332b8a0 100644 --- a/apps/server/src/modules/auth-providers/provider-manager.ts +++ b/apps/server/src/modules/auth-providers/provider-manager.ts @@ -461,6 +461,8 @@ export class ProviderManager { ping: ["openid", "profile", "email"], azure: ["openid", "profile", "email", "User.Read"], aws: ["openid", "profile", "email"], + kinde: ["openid", "profile", "email"], + zitadel: ["openid", "profile", "email"], // Communication slack: ["identity.basic", "identity.email", "identity.avatar"], @@ -559,15 +561,8 @@ export class ProviderManager { { pattern: "monday.com", type: "monday" }, { pattern: "clickup.com", type: "clickup" }, { pattern: "linear.app", type: "linear" }, - { pattern: "jira", type: "jira" }, - { pattern: "confluence", type: "confluence" }, - { pattern: "bamboo", type: "bamboo" }, - { pattern: "bitbucket", type: "bitbucket" }, - { pattern: "crowd", type: "crowd" }, - { pattern: "fisheye", type: "fisheye" }, - { pattern: "crucible", type: "crucible" }, - { pattern: "statuspage", type: "statuspage" }, - { pattern: "opsgenie", type: "opsgenie" }, + { pattern: "kinde.com", type: "kinde" }, + { pattern: "zitadel.com", type: "zitadel" }, { pattern: "jira", type: "jira" }, { pattern: "confluence", type: "confluence" }, { pattern: "bamboo", type: "bamboo" }, diff --git a/apps/server/src/modules/auth-providers/providers.config.ts b/apps/server/src/modules/auth-providers/providers.config.ts index 154352f..a38534a 100644 --- a/apps/server/src/modules/auth-providers/providers.config.ts +++ b/apps/server/src/modules/auth-providers/providers.config.ts @@ -1,5 +1,42 @@ import { ProviderConfig, ProvidersConfigFile } from "./types"; +/** + * Configuração técnica oficial do Discord + * OAuth2 com mapeamentos específicos do Discord + * Endpoints vêm do banco de dados + */ +const discordConfig: ProviderConfig = { + supportsDiscovery: false, + authMethod: "body", + fieldMappings: { + id: ["id"], + email: ["email"], + name: ["global_name", "username"], + firstName: ["global_name"], + lastName: [], + avatar: ["avatar"], + }, +}; + +/** + * Configuração técnica oficial do Google + * OAuth2 com discovery automático + * Endpoints vêm do banco de dados + */ +const googleConfig: ProviderConfig = { + supportsDiscovery: true, + discoveryEndpoint: "/.well-known/openid_configuration", + authMethod: "body", + fieldMappings: { + id: ["sub"], + email: ["email"], + name: ["name"], + firstName: ["given_name"], + lastName: ["family_name"], + avatar: ["picture"], + }, +}; + /** * Configuração técnica oficial do GitHub * OAuth2 com busca separada de email @@ -153,6 +190,8 @@ const genericProviderTemplate: ProviderConfig = { */ export const providersConfig: ProvidersConfigFile = { officialProviders: { + google: googleConfig, + discord: discordConfig, github: githubConfig, auth0: auth0Config, kinde: kindeConfig, @@ -167,6 +206,8 @@ export const providersConfig: ProvidersConfigFile = { * Exportações individuais para facilitar importação */ export { + discordConfig, + googleConfig, githubConfig, auth0Config, kindeConfig, diff --git a/apps/server/src/modules/auth-providers/service.ts b/apps/server/src/modules/auth-providers/service.ts index 9ebe8a8..68b5b74 100644 --- a/apps/server/src/modules/auth-providers/service.ts +++ b/apps/server/src/modules/auth-providers/service.ts @@ -193,13 +193,16 @@ export class AuthProvidersService { async handleCallback(providerName: string, code: string, state: string, requestContext?: any) { console.log(`[AuthProvidersService] Handling callback for provider: ${providerName}`); + console.log(`[AuthProvidersService] State received: ${state}`); const pendingState = this.pendingStates.get(state); + if (!pendingState) { + console.error(`[AuthProvidersService] No valid pending state found for ${providerName}`); throw new Error("Invalid or expired state"); } - this.pendingStates.delete(state); + console.log(`[AuthProvidersService] Using pending state for ${providerName}`); const provider = await this.getProviderByName(providerName); if (!provider) { @@ -285,31 +288,33 @@ export class AuthProvidersService { const callbackUrl = provider.redirectUri || `${baseUrl}/api/auth/providers/${provider.name}/callback`; + // Constrói body para token exchange + const body = new URLSearchParams(); + body.append("client_id", provider.clientId); + body.append("code", code); + body.append("redirect_uri", callbackUrl); + body.append("grant_type", "authorization_code"); + + // Adiciona client_secret se necessário + if (authMethod === "body" && provider.clientSecret) { + body.append("client_secret", provider.clientSecret); + } + + // Adiciona code_verifier se disponível (PKCE) + if (codeVerifier) { + body.append("code_verifier", codeVerifier); + } + // Prepara headers const headers: Record = { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }; - // Prepara body - const body = new URLSearchParams({ - client_id: provider.clientId, - code, - redirect_uri: callbackUrl, - grant_type: "authorization_code", - }); - - // Adiciona code_verifier se for OIDC - if (provider.type === "oidc" && codeVerifier) { - body.append("code_verifier", codeVerifier); - } - - // Configura autenticação baseada no método - if (authMethod === "basic") { + // Configura autenticação basic se necessário + if (authMethod === "basic" && provider.clientSecret) { const auth = Buffer.from(`${provider.clientId}:${provider.clientSecret}`).toString("base64"); headers["Authorization"] = `Basic ${auth}`; - } else { - body.append("client_secret", provider.clientSecret); } console.log(`[AuthProvidersService] Token exchange request:`, { @@ -335,6 +340,7 @@ export class AuthProvidersService { statusText: tokenResponse.statusText, error: errorText, }); + throw new Error(`Token exchange failed: ${tokenResponse.status} - ${errorText}`); } @@ -368,7 +374,6 @@ export class AuthProvidersService { status: userInfoResponse.status, statusText: userInfoResponse.statusText, url: endpoints.userInfoEndpoint, - headers: Object.fromEntries(userInfoResponse.headers.entries()), error: errorText.substring(0, 500), // Limita o tamanho do log }); throw new Error(`UserInfo request failed: ${userInfoResponse.status} - ${errorText.substring(0, 200)}`); diff --git a/apps/web/src/app/settings/components/auth-provider-delete-modal.tsx b/apps/web/src/app/settings/components/auth-provider-delete-modal.tsx new file mode 100644 index 0000000..9b4a670 --- /dev/null +++ b/apps/web/src/app/settings/components/auth-provider-delete-modal.tsx @@ -0,0 +1,74 @@ +import { IconTrash } from "@tabler/icons-react"; +import { useTranslations } from "next-intl"; + +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; + +interface AuthProviderDeleteModalProps { + isOpen: boolean; + onClose: () => void; + provider: { id: string; name: string; displayName: string } | null; + onConfirm: () => Promise; + isDeleting: boolean; +} + +export function AuthProviderDeleteModal({ + isOpen, + onClose, + provider, + onConfirm, + isDeleting, +}: AuthProviderDeleteModalProps) { + const t = useTranslations(); + + const handleConfirm = async () => { + await onConfirm(); + }; + + return ( + !isDeleting && onClose()}> + + + + + Delete Authentication Provider + + + +
+

+ Are you sure you want to delete the "{provider?.displayName}" provider? This action cannot be undone. +

+ + {provider && ( +
+
+

{provider.displayName}

+

Provider ID: {provider.name}

+
+
+ )} +
+ + + + + +
+
+ ); +} diff --git a/apps/web/src/app/settings/components/auth-providers-settings.tsx b/apps/web/src/app/settings/components/auth-providers-settings.tsx index 2361c6c..a19d899 100644 --- a/apps/web/src/app/settings/components/auth-providers-settings.tsx +++ b/apps/web/src/app/settings/components/auth-providers-settings.tsx @@ -31,6 +31,7 @@ import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { TagsInput } from "@/components/ui/tags-input"; +import { AuthProviderDeleteModal } from "./auth-provider-delete-modal"; interface AuthProvider { id: string; @@ -76,6 +77,10 @@ export function AuthProvidersSettings() { const [editingProvider, setEditingProvider] = useState(null); const [editingFormData, setEditingFormData] = useState>({}); const [hideDisabledProviders, setHideDisabledProviders] = useState(false); + const [providerToDelete, setProviderToDelete] = useState<{ id: string; name: string; displayName: string } | null>( + null + ); + const [isDeleting, setIsDeleting] = useState(false); const [newProvider, setNewProvider] = useState({ name: "", @@ -138,6 +143,8 @@ export function AuthProvidersSettings() { { pattern: "monday.com", scopes: ["read"] }, { pattern: "clickup.com", scopes: ["read"] }, { pattern: "linear.app", scopes: ["read"] }, + { pattern: "kinde.com", scopes: ["openid", "profile", "email"] }, + { pattern: "zitadel.com", scopes: ["openid", "profile", "email"] }, ]; // Procura por padrões conhecidos @@ -231,10 +238,8 @@ export function AuthProvidersSettings() { // Delete provider const deleteProvider = async (id: string, name: string) => { - if (!confirm(`Delete "${name}" provider? This cannot be undone.`)) return; - try { - setSaving(id); + setIsDeleting(true); const response = await fetch(`/api/auth/providers/manage/${id}`, { method: "DELETE", }); @@ -244,6 +249,7 @@ export function AuthProvidersSettings() { if (data.success) { setProviders((prev) => prev.filter((p) => p.id !== id)); toast.success("Provider deleted"); + setProviderToDelete(null); } else { toast.error("Failed to delete provider"); } @@ -251,10 +257,19 @@ export function AuthProvidersSettings() { console.error("Error deleting provider:", error); toast.error("Failed to delete provider"); } finally { - setSaving(null); + setIsDeleting(false); } }; + // Open delete confirmation modal + const openDeleteModal = (provider: AuthProvider) => { + setProviderToDelete({ + id: provider.id, + name: provider.name, + displayName: provider.displayName, + }); + }; + // Add new provider const addProvider = async () => { if (!newProvider.name || !newProvider.displayName || !newProvider.clientId || !newProvider.clientSecret) { @@ -802,7 +817,7 @@ export function AuthProvidersSettings() { setEditingProvider(provider); } }} - onDelete={() => deleteProvider(provider.id, provider.displayName)} + onDelete={() => openDeleteModal(provider)} saving={saving === provider.id} getIcon={getProviderIcon} editingProvider={editingProvider} @@ -843,7 +858,7 @@ export function AuthProvidersSettings() { setEditingProvider(provider); } }} - onDelete={() => deleteProvider(provider.id, provider.displayName)} + onDelete={() => openDeleteModal(provider)} saving={saving === provider.id} getIcon={getProviderIcon} editingProvider={editingProvider} @@ -876,6 +891,19 @@ export function AuthProvidersSettings() { )} + + {/* Delete Confirmation Modal */} + setProviderToDelete(null)} + provider={providerToDelete} + onConfirm={async () => { + if (providerToDelete) { + await deleteProvider(providerToDelete.id, providerToDelete.name); + } + }} + isDeleting={isDeleting} + /> ); } @@ -1047,36 +1075,10 @@ function EditProviderForm({ { pattern: "facebook.com", scopes: ["public_profile", "email"] }, { pattern: "twitter.com", scopes: ["tweet.read", "users.read"] }, { pattern: "linkedin.com", scopes: ["r_liteprofile", "r_emailaddress"] }, - { pattern: "authentik", scopes: ["openid", "profile", "email"] }, - { pattern: "keycloak", scopes: ["openid", "profile", "email"] }, { pattern: "auth0.com", scopes: ["openid", "profile", "email"] }, { pattern: "okta.com", scopes: ["openid", "profile", "email"] }, - { pattern: "onelogin.com", scopes: ["openid", "profile", "email"] }, - { pattern: "pingidentity.com", scopes: ["openid", "profile", "email"] }, - { pattern: "azure.com", scopes: ["openid", "profile", "email", "User.Read"] }, - { pattern: "aws.amazon.com", scopes: ["openid", "profile", "email"] }, - { pattern: "slack.com", scopes: ["identity.basic", "identity.email", "identity.avatar"] }, - { pattern: "bitbucket.org", scopes: ["account", "repository"] }, - { pattern: "atlassian.com", scopes: ["read:jira-user", "read:jira-work"] }, - { pattern: "salesforce.com", scopes: ["api", "refresh_token"] }, - { pattern: "zendesk.com", scopes: ["read"] }, - { pattern: "shopify.com", scopes: ["read_products", "read_customers"] }, - { pattern: "stripe.com", scopes: ["read"] }, - { pattern: "twilio.com", scopes: ["read"] }, - { pattern: "sendgrid.com", scopes: ["mail.send"] }, - { pattern: "mailchimp.com", scopes: ["read"] }, - { pattern: "hubspot.com", scopes: ["contacts", "crm.objects.contacts.read"] }, - { pattern: "zoom.us", scopes: ["user:read:admin"] }, - { pattern: "teams.microsoft.com", scopes: ["openid", "profile", "email", "User.Read"] }, - { pattern: "notion.so", scopes: ["read"] }, - { pattern: "figma.com", scopes: ["files:read"] }, - { pattern: "dropbox.com", scopes: ["files.content.read"] }, - { pattern: "box.com", scopes: ["root_readwrite"] }, - { pattern: "trello.com", scopes: ["read"] }, - { pattern: "asana.com", scopes: ["default"] }, - { pattern: "monday.com", scopes: ["read"] }, - { pattern: "clickup.com", scopes: ["read"] }, - { pattern: "linear.app", scopes: ["read"] }, + { pattern: "kinde.com", scopes: ["openid", "profile", "email"] }, + { pattern: "zitadel.com", scopes: ["openid", "profile", "email"] }, ]; // Procura por padrões conhecidos