mirror of
				https://github.com/kyantech/Palmr.git
				synced 2025-11-03 21:43:20 +00:00 
			
		
		
		
	feat: add Google and Discord authentication providers with configuration updates
- Introduced Google and Discord as new authentication providers in the seed script, including their OAuth2 configurations and metadata. - Updated existing provider configurations to adjust sort orders and ensure proper integration with the new providers. - Enhanced validation logic in the DTOs to accommodate optional endpoint fields and ensure correct provider setup. - Implemented a delete confirmation modal in the web settings for managing authentication providers, improving user experience. - Added logging for better debugging and tracking of provider-related operations in the controller and service layers.
This commit is contained in:
		@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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.",
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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" },
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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<string, string> = {
 | 
			
		||||
      "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)}`);
 | 
			
		||||
 
 | 
			
		||||
@@ -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<void>;
 | 
			
		||||
  isDeleting: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function AuthProviderDeleteModal({
 | 
			
		||||
  isOpen,
 | 
			
		||||
  onClose,
 | 
			
		||||
  provider,
 | 
			
		||||
  onConfirm,
 | 
			
		||||
  isDeleting,
 | 
			
		||||
}: AuthProviderDeleteModalProps) {
 | 
			
		||||
  const t = useTranslations();
 | 
			
		||||
 | 
			
		||||
  const handleConfirm = async () => {
 | 
			
		||||
    await onConfirm();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog open={isOpen} onOpenChange={() => !isDeleting && onClose()}>
 | 
			
		||||
      <DialogContent className="sm:max-w-md">
 | 
			
		||||
        <DialogHeader>
 | 
			
		||||
          <DialogTitle className="flex items-center gap-2 text-destructive">
 | 
			
		||||
            <IconTrash size={20} />
 | 
			
		||||
            Delete Authentication Provider
 | 
			
		||||
          </DialogTitle>
 | 
			
		||||
        </DialogHeader>
 | 
			
		||||
 | 
			
		||||
        <div className="space-y-4">
 | 
			
		||||
          <p className="text-sm text-muted-foreground">
 | 
			
		||||
            Are you sure you want to delete the "{provider?.displayName}" provider? This action cannot be undone.
 | 
			
		||||
          </p>
 | 
			
		||||
 | 
			
		||||
          {provider && (
 | 
			
		||||
            <div className="rounded-lg border p-4 bg-muted/30">
 | 
			
		||||
              <div className="space-y-2">
 | 
			
		||||
                <h4 className="font-medium">{provider.displayName}</h4>
 | 
			
		||||
                <p className="text-sm text-muted-foreground">Provider ID: {provider.name}</p>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <DialogFooter className="flex gap-2">
 | 
			
		||||
          <Button variant="outline" onClick={onClose} disabled={isDeleting}>
 | 
			
		||||
            Cancel
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="destructive" onClick={handleConfirm} disabled={isDeleting}>
 | 
			
		||||
            {isDeleting ? (
 | 
			
		||||
              <>
 | 
			
		||||
                <IconTrash className="h-4 w-4 mr-2 animate-spin" />
 | 
			
		||||
                Deleting...
 | 
			
		||||
              </>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <>
 | 
			
		||||
                <IconTrash className="h-4 w-4 mr-2" />
 | 
			
		||||
                Delete Provider
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </DialogFooter>
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -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<AuthProvider | null>(null);
 | 
			
		||||
  const [editingFormData, setEditingFormData] = useState<Record<string, any>>({});
 | 
			
		||||
  const [hideDisabledProviders, setHideDisabledProviders] = useState<boolean>(false);
 | 
			
		||||
  const [providerToDelete, setProviderToDelete] = useState<{ id: string; name: string; displayName: string } | null>(
 | 
			
		||||
    null
 | 
			
		||||
  );
 | 
			
		||||
  const [isDeleting, setIsDeleting] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const [newProvider, setNewProvider] = useState<NewProvider>({
 | 
			
		||||
    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() {
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </CardContent>
 | 
			
		||||
 | 
			
		||||
      {/* Delete Confirmation Modal */}
 | 
			
		||||
      <AuthProviderDeleteModal
 | 
			
		||||
        isOpen={!!providerToDelete}
 | 
			
		||||
        onClose={() => setProviderToDelete(null)}
 | 
			
		||||
        provider={providerToDelete}
 | 
			
		||||
        onConfirm={async () => {
 | 
			
		||||
          if (providerToDelete) {
 | 
			
		||||
            await deleteProvider(providerToDelete.id, providerToDelete.name);
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
        isDeleting={isDeleting}
 | 
			
		||||
      />
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user