feat: enhance authentication provider management and error handling

- Refactored AuthProvidersController to improve request handling and error responses, introducing utility methods for success and error replies.
- Added new request context and validation logic for custom endpoints in provider configurations, ensuring robust setup and error messaging.
- Implemented detailed error handling for callback and authorization processes, enhancing user feedback during authentication flows.
- Removed the ProviderManager class and integrated its functionality directly into AuthProvidersService for streamlined provider management.
- Updated DTOs and types to support new request structures and validation requirements, improving overall code clarity and maintainability.
This commit is contained in:
Daniel Luiz Alves
2025-06-27 13:14:22 -03:00
parent f1449f6b10
commit 8f85874cbe
15 changed files with 918 additions and 1571 deletions

View File

@@ -1,7 +1,41 @@
import { UpdateAuthProviderSchema } from "./dto";
import { AuthProvidersService } from "./service";
import {
AuthorizeRequest,
CallbackRequest,
CreateProviderRequest,
DeleteProviderRequest,
RequestContext,
UpdateProviderRequest,
UpdateProvidersOrderRequest,
} from "./types";
import { FastifyRequest, FastifyReply } from "fastify";
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60 * 1000;
const OFFICIAL_PROVIDER_ALLOWED_FIELDS = [
"issuerUrl",
"clientId",
"clientSecret",
"enabled",
"autoRegister",
"adminEmailDomains",
"icon",
];
const ERROR_MESSAGES = {
ENDPOINTS_INCOMPLETE:
"When using manual endpoints, all three endpoints (authorization, token, userInfo) are required",
MISSING_CONFIG: "Either provide issuerUrl for automatic discovery OR all three custom endpoints",
PROVIDER_NOT_FOUND: "Provider not found",
INVALID_URL: "Invalid Provider URL format",
INVALID_DATA: "Invalid data provided",
OFFICIAL_CANNOT_DELETE: "Official providers cannot be deleted",
INVALID_PROVIDERS_ARRAY: "Invalid providers array",
AUTHORIZATION_FAILED: "Authorization failed",
AUTHENTICATION_FAILED: "Authentication failed",
} as const;
export class AuthProvidersController {
private authProvidersService: AuthProvidersService;
@@ -9,26 +43,148 @@ export class AuthProvidersController {
this.authProvidersService = new AuthProvidersService();
}
private buildRequestContext(request: FastifyRequest): RequestContext {
return {
protocol: (request.headers["x-forwarded-proto"] as string) || request.protocol,
host: (request.headers["x-forwarded-host"] as string) || (request.headers.host as string),
headers: request.headers,
};
}
private buildBaseUrl(requestContext: RequestContext): string {
return `${requestContext.protocol}://${requestContext.host}`;
}
private sendSuccessResponse(reply: FastifyReply, data?: any, message?: string) {
return reply.send({
success: true,
...(data && { data }),
...(message && { message }),
});
}
private sendErrorResponse(reply: FastifyReply, status: number, error: string) {
return reply.status(status).send({
success: false,
error,
});
}
private async handleControllerError(reply: FastifyReply, error: unknown, defaultMessage: string) {
console.error(`Controller error: ${defaultMessage}`, error);
if (error instanceof Error && error.message.includes("Either provide issuerUrl")) {
return this.sendErrorResponse(reply, 400, error.message);
}
return this.sendErrorResponse(reply, 500, defaultMessage);
}
private validateCustomEndpoints(data: any): string | null {
const hasAnyCustomEndpoint = !!(data.authorizationEndpoint || data.tokenEndpoint || data.userInfoEndpoint);
const hasAllCustomEndpoints = !!(data.authorizationEndpoint && data.tokenEndpoint && data.userInfoEndpoint);
if (hasAnyCustomEndpoint && !hasAllCustomEndpoints) {
return ERROR_MESSAGES.ENDPOINTS_INCOMPLETE;
}
if (!data.issuerUrl && !hasAllCustomEndpoints) {
return ERROR_MESSAGES.MISSING_CONFIG;
}
return null;
}
private validateIssuerUrl(issuerUrl: string): boolean {
try {
new URL(issuerUrl);
return true;
} catch {
return false;
}
}
private sanitizeOfficialProviderData(data: any): any {
const sanitizedData: any = {};
for (const field of OFFICIAL_PROVIDER_ALLOWED_FIELDS) {
if (data[field] !== undefined) {
sanitizedData[field] = data[field];
}
}
return sanitizedData;
}
private setAuthCookie(reply: FastifyReply, token: string, isSecure: boolean) {
reply.setCookie("token", token, {
httpOnly: true,
secure: isSecure,
sameSite: "lax",
maxAge: COOKIE_MAX_AGE,
path: "/",
});
}
private determineCallbackError(error: Error, provider: string): { type: string; message: string } {
const errorMessage = error.message;
if (errorMessage.includes("registration via") && errorMessage.includes("disabled")) {
return {
type: "registration_disabled",
message: `Registration via ${provider} is disabled. Contact your administrator.`,
};
}
if (errorMessage.includes("not enabled")) {
return {
type: "provider_disabled",
message: `${provider} authentication is currently disabled.`,
};
}
if (errorMessage.includes("expired")) {
return {
type: "state_expired",
message: "Authentication session expired. Please try again.",
};
}
if (errorMessage.includes("No email found")) {
return {
type: "no_email",
message: `No email address found in your ${provider} account.`,
};
}
if (errorMessage.includes("Token exchange failed")) {
return {
type: "token_exchange_failed",
message: `Failed to authenticate with ${provider}. Please try again.`,
};
}
if (errorMessage.includes("Missing required user information")) {
return {
type: "missing_user_info",
message: `Incomplete user information from ${provider}.`,
};
}
return {
type: "unknown_error",
message: ERROR_MESSAGES.AUTHENTICATION_FAILED,
};
}
async getProviders(request: FastifyRequest, reply: FastifyReply) {
try {
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),
headers: request.headers,
};
const requestContext = this.buildRequestContext(request);
const providers = await this.authProvidersService.getEnabledProviders(requestContext);
return reply.send({
success: true,
data: providers,
});
return this.sendSuccessResponse(reply, providers);
} catch (error) {
console.error("Error getting auth providers:", error);
return reply.status(500).send({
success: false,
error: "Failed to get auth providers",
});
return this.handleControllerError(reply, error, "Failed to get auth providers");
}
}
@@ -37,205 +193,97 @@ export class AuthProvidersController {
try {
const providers = await this.authProvidersService.getAllProviders();
return reply.send({
success: true,
data: providers,
});
return this.sendSuccessResponse(reply, providers);
} catch (error) {
console.error("Error getting all providers:", error);
return reply.status(500).send({
success: false,
error: "Failed to get providers",
});
return this.handleControllerError(reply, error, "Failed to get providers");
}
}
async createProvider(request: FastifyRequest<{ Body: any }>, reply: FastifyReply) {
async createProvider(request: FastifyRequest<CreateProviderRequest>, reply: FastifyReply) {
if (reply.sent) return;
try {
const data = request.body as any;
const data = request.body;
// Validação adicional: se modo manual, todos os 3 endpoints são obrigatórios
const hasAnyCustomEndpoint = !!(data.authorizationEndpoint || data.tokenEndpoint || data.userInfoEndpoint);
const hasAllCustomEndpoints = !!(data.authorizationEndpoint && data.tokenEndpoint && data.userInfoEndpoint);
if (hasAnyCustomEndpoint && !hasAllCustomEndpoints) {
return reply.status(400).send({
success: false,
error: "When using manual endpoints, all three endpoints (authorization, token, userInfo) are required",
});
}
// Validação: deve ter ou issuerUrl ou endpoints customizados
if (!data.issuerUrl && !hasAllCustomEndpoints) {
return reply.status(400).send({
success: false,
error: "Either provide issuerUrl for automatic discovery OR all three custom endpoints",
});
const validationError = this.validateCustomEndpoints(data);
if (validationError) {
return this.sendErrorResponse(reply, 400, validationError);
}
const provider = await this.authProvidersService.createProvider(data);
return reply.send({
success: true,
data: provider,
});
return this.sendSuccessResponse(reply, provider);
} catch (error) {
console.error("Error creating provider:", error);
// Se é erro de validação do Zod
if (error instanceof Error && error.message.includes("Either provide issuerUrl")) {
return reply.status(400).send({
success: false,
error: error.message,
});
}
return reply.status(500).send({
success: false,
error: "Failed to create provider",
});
return this.handleControllerError(reply, error, "Failed to create provider");
}
}
async updateProvider(request: FastifyRequest<{ Params: { id: string }; Body: any }>, reply: FastifyReply) {
async updateProvider(request: FastifyRequest<UpdateProviderRequest>, reply: FastifyReply) {
if (reply.sent) return;
try {
const { id } = request.params;
const data = request.body as any;
const data = request.body;
// Buscar provider para verificar se é oficial
const existingProvider = await this.authProvidersService.getProviderById(id);
if (!existingProvider) {
return reply.status(404).send({
success: false,
error: "Provider not found",
});
return this.sendErrorResponse(reply, 404, ERROR_MESSAGES.PROVIDER_NOT_FOUND);
}
const isOfficial = this.authProvidersService.isOfficialProvider(existingProvider.name);
// Para providers oficiais, só permite alterar issuerUrl, clientId, clientSecret, enabled, autoRegister, icon
if (isOfficial) {
const allowedFields = [
"issuerUrl",
"clientId",
"clientSecret",
"enabled",
"autoRegister",
"adminEmailDomains",
"icon",
];
const sanitizedData: any = {};
for (const field of allowedFields) {
if (data[field] !== undefined) {
sanitizedData[field] = data[field];
}
}
console.log(
`[Controller] Official provider ${existingProvider.name} - only allowing fields:`,
Object.keys(sanitizedData)
);
console.log(`[Controller] Sanitized data:`, sanitizedData);
// Validação adicional para issuerUrl se fornecida
if (sanitizedData.issuerUrl && typeof sanitizedData.issuerUrl === "string") {
try {
new URL(sanitizedData.issuerUrl);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return reply.status(400).send({
success: false,
error: "Invalid Provider URL format",
});
}
}
const provider = await this.authProvidersService.updateProvider(id, sanitizedData);
console.log(`[Controller] Provider updated successfully:`, provider?.id);
return reply.send({
success: true,
data: provider,
});
return this.updateOfficialProvider(reply, id, data);
}
// 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({
success: true,
data: provider,
});
} 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",
});
}
return this.updateCustomProvider(reply, id, data);
} catch (error) {
console.error("Error updating provider:", error);
// Se é erro de validação do Zod
if (error instanceof Error && error.message.includes("Either provide issuerUrl")) {
return reply.status(400).send({
success: false,
error: error.message,
});
}
return reply.status(500).send({
success: false,
error: "Failed to update provider",
});
return this.handleControllerError(reply, error, "Failed to update provider");
}
}
async updateProvidersOrder(
request: FastifyRequest<{ Body: { providers: { id: string; sortOrder: number }[] } }>,
reply: FastifyReply
) {
private async updateOfficialProvider(reply: FastifyReply, id: string, data: any) {
const sanitizedData = this.sanitizeOfficialProviderData(data);
if (sanitizedData.issuerUrl && typeof sanitizedData.issuerUrl === "string") {
if (!this.validateIssuerUrl(sanitizedData.issuerUrl)) {
return this.sendErrorResponse(reply, 400, ERROR_MESSAGES.INVALID_URL);
}
}
const provider = await this.authProvidersService.updateProvider(id, sanitizedData);
return this.sendSuccessResponse(reply, provider);
}
private async updateCustomProvider(reply: FastifyReply, id: string, data: any) {
try {
const validatedData = UpdateAuthProviderSchema.parse(data);
const provider = await this.authProvidersService.updateProvider(id, validatedData);
return this.sendSuccessResponse(reply, provider);
} catch (validationError) {
console.error("Validation error for custom provider:", validationError);
console.error("Raw data that failed validation:", data);
return this.sendErrorResponse(reply, 400, ERROR_MESSAGES.INVALID_DATA);
}
}
async updateProvidersOrder(request: FastifyRequest<UpdateProvidersOrderRequest>, reply: FastifyReply) {
if (reply.sent) return;
try {
const { providers } = request.body;
if (!Array.isArray(providers)) {
return reply.status(400).send({
success: false,
error: "Invalid providers array",
});
return this.sendErrorResponse(reply, 400, ERROR_MESSAGES.INVALID_PROVIDERS_ARRAY);
}
await this.authProvidersService.updateProvidersOrder(providers);
return reply.send({
success: true,
message: "Providers order updated successfully",
});
return this.sendSuccessResponse(reply, undefined, "Providers order updated successfully");
} catch (error) {
console.error("Error updating providers order:", error);
return reply.status(500).send({
success: false,
error: "Failed to update providers order",
});
return this.handleControllerError(reply, error, "Failed to update providers order");
}
}
async deleteProvider(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
async deleteProvider(request: FastifyRequest<DeleteProviderRequest>, reply: FastifyReply) {
if (reply.sent) return;
try {
@@ -243,47 +291,27 @@ export class AuthProvidersController {
const provider = await this.authProvidersService.getProviderById(id);
if (!provider) {
return reply.status(404).send({
success: false,
error: "Provider not found",
});
return this.sendErrorResponse(reply, 404, ERROR_MESSAGES.PROVIDER_NOT_FOUND);
}
const isOfficial = this.authProvidersService.isOfficialProvider(provider.name);
if (isOfficial) {
return reply.status(400).send({
success: false,
error: "Official providers cannot be deleted",
});
return this.sendErrorResponse(reply, 400, ERROR_MESSAGES.OFFICIAL_CANNOT_DELETE);
}
await this.authProvidersService.deleteProvider(id);
return reply.send({
success: true,
message: "Provider deleted successfully",
});
return this.sendSuccessResponse(reply, undefined, "Provider deleted successfully");
} catch (error) {
console.error("Error deleting provider:", error);
return reply.status(500).send({
success: false,
error: "Failed to delete provider",
});
return this.handleControllerError(reply, error, "Failed to delete provider");
}
}
async authorize(request: FastifyRequest<{ Params: { provider: string }; Querystring: any }>, reply: FastifyReply) {
async authorize(request: FastifyRequest<AuthorizeRequest>, reply: FastifyReply) {
try {
const { provider: providerName } = request.params;
const query = request.query as any;
const { state, redirect_uri } = 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),
headers: request.headers,
};
const { state, redirect_uri } = request.query;
const requestContext = this.buildRequestContext(request);
const authUrl = await this.authProvidersService.getAuthorizationUrl(
providerName,
state,
@@ -293,62 +321,31 @@ export class AuthProvidersController {
return reply.redirect(authUrl);
} catch (error) {
console.error("Error in authorize:", error);
return reply.status(400).send({
success: false,
error: error instanceof Error ? error.message : "Authorization failed",
});
const errorMessage = error instanceof Error ? error.message : ERROR_MESSAGES.AUTHORIZATION_FAILED;
return this.sendErrorResponse(reply, 400, errorMessage);
}
}
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"],
});
async callback(request: FastifyRequest<CallbackRequest>, reply: FastifyReply) {
try {
const { provider: providerName } = request.params;
const query = request.query as any;
const { code, state, error } = query;
const { code, state, error } = request.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);
const requestContext = this.buildRequestContext(request);
const baseUrl = this.buildBaseUrl(requestContext);
if (error) {
console.error(`OAuth error from ${providerName}:`, error);
return reply.redirect(`${baseUrl}/login?error=oauth_error&provider=${providerName}`);
}
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);
if (!state) {
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({
@@ -356,55 +353,29 @@ export class AuthProvidersController {
isAdmin: result.user.isAdmin,
});
reply.setCookie("token", jwt, {
httpOnly: true,
secure: request.protocol === "https",
sameSite: "lax",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: "/",
});
this.setAuthCookie(reply, jwt, request.protocol === "https");
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);
let errorType = "unknown_error";
let errorMessage = "Authentication failed";
if (error instanceof Error) {
if (error.message.includes("registration via") && error.message.includes("disabled")) {
errorType = "registration_disabled";
errorMessage = `Registration via ${request.params.provider} is disabled. Contact your administrator.`;
} else if (error.message.includes("not enabled")) {
errorType = "provider_disabled";
errorMessage = `${request.params.provider} authentication is currently disabled.`;
} else if (error.message.includes("expired")) {
errorType = "state_expired";
errorMessage = "Authentication session expired. Please try again.";
} else if (error.message.includes("No email found")) {
errorType = "no_email";
errorMessage = `No email address found in your ${request.params.provider} account.`;
} else if (error.message.includes("Token exchange failed")) {
errorType = "token_exchange_failed";
errorMessage = `Failed to authenticate with ${request.params.provider}. Please try again.`;
} else if (error.message.includes("Missing required user information")) {
errorType = "missing_user_info";
errorMessage = `Incomplete user information from ${request.params.provider}.`;
}
}
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}`;
const encodedMessage = encodeURIComponent(errorMessage);
return reply.redirect(
`${baseUrl}/login?error=${errorType}&provider=${request.params.provider}&message=${encodedMessage}`
);
return this.handleCallbackError(request, reply, error);
}
}
private handleCallbackError(request: FastifyRequest<CallbackRequest>, reply: FastifyReply, error: unknown) {
const { type: errorType, message: errorMessage } =
error instanceof Error
? this.determineCallbackError(error, request.params.provider)
: { type: "unknown_error", message: ERROR_MESSAGES.AUTHENTICATION_FAILED };
const requestContext = this.buildRequestContext(request);
const baseUrl = this.buildBaseUrl(requestContext);
const encodedMessage = encodeURIComponent(errorMessage);
return reply.redirect(
`${baseUrl}/login?error=${errorType}&provider=${request.params.provider}&message=${encodedMessage}`
);
}
}

View File

@@ -1,6 +1,5 @@
import { z } from "zod";
// Schema base para provider
export const BaseAuthProviderSchema = z.object({
name: z.string().min(1, "Name is required").describe("Provider name"),
displayName: z.string().min(1, "Display name is required").describe("Provider display name"),
@@ -14,7 +13,6 @@ export const BaseAuthProviderSchema = z.object({
clientSecret: z.string().min(1, "Client secret is required").describe("OAuth client secret"),
});
// Schema para modo discovery automático (apenas issuerUrl)
export const DiscoveryModeSchema = BaseAuthProviderSchema.extend({
issuerUrl: z.string().url("Invalid issuer URL").describe("Provider issuer URL for discovery"),
authorizationEndpoint: z.literal("").optional(),
@@ -22,7 +20,6 @@ export const DiscoveryModeSchema = BaseAuthProviderSchema.extend({
userInfoEndpoint: z.literal("").optional(),
});
// Schema para modo manual (todos os endpoints)
export const ManualEndpointsSchema = BaseAuthProviderSchema.extend({
issuerUrl: z.string().optional(),
authorizationEndpoint: z
@@ -33,7 +30,6 @@ export const ManualEndpointsSchema = BaseAuthProviderSchema.extend({
userInfoEndpoint: z.string().min(1, "User info endpoint is required").describe("User info endpoint URL or path"),
});
// Schema principal que aceita ambos os modos
export const CreateAuthProviderSchema = BaseAuthProviderSchema.extend({
issuerUrl: z.string().url("Invalid issuer URL").optional(),
authorizationEndpoint: z.string().optional(),
@@ -48,8 +44,7 @@ export const CreateAuthProviderSchema = BaseAuthProviderSchema.extend({
data.userInfoEndpoint?.trim()
);
// Deve ter pelo menos issuerUrl OU todos os endpoints customizados
if (hasIssuerUrl && !hasAnyCustomEndpoint) return true; // Modo discovery
if (hasIssuerUrl && !hasAnyCustomEndpoint) return true;
if (hasAnyCustomEndpoint) {
const hasAllCustomEndpoints = !!(
@@ -57,10 +52,10 @@ export const CreateAuthProviderSchema = BaseAuthProviderSchema.extend({
data.tokenEndpoint?.trim() &&
data.userInfoEndpoint?.trim()
);
return hasAllCustomEndpoints; // Precisa ter todos os 3 endpoints
return hasAllCustomEndpoints;
}
return false; // Precisa ter pelo menos um dos dois modos
return false;
},
{
message:
@@ -68,7 +63,6 @@ export const CreateAuthProviderSchema = BaseAuthProviderSchema.extend({
}
);
// Schema para atualização (todos os campos opcionais exceto validação de modo)
export const UpdateAuthProviderSchema = z
.object({
name: z.string().min(1).optional(),
@@ -88,7 +82,6 @@ export const UpdateAuthProviderSchema = z
})
.refine(
(data) => {
// Se não está alterando nenhum campo de configuração, permite
const hasIssuerUrl = !!data.issuerUrl;
const hasAnyCustomEndpoint = !!(
data.authorizationEndpoint?.trim() ||
@@ -96,13 +89,10 @@ export const UpdateAuthProviderSchema = z
data.userInfoEndpoint?.trim()
);
// Se não está alterando nenhum campo de configuração, permite
if (!hasIssuerUrl && !hasAnyCustomEndpoint) return true;
// 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() &&
@@ -119,7 +109,6 @@ export const UpdateAuthProviderSchema = z
}
);
// Schema específico para providers oficiais (apenas campos permitidos)
export const UpdateOfficialProviderSchema = z.object({
issuerUrl: z.string().url().optional(),
clientId: z.string().min(1).optional(),
@@ -129,7 +118,6 @@ export const UpdateOfficialProviderSchema = z.object({
adminEmailDomains: z.string().optional(),
});
// Schema para reordenação
export const UpdateProvidersOrderSchema = z.object({
providers: z.array(
z.object({

View File

@@ -1,795 +0,0 @@
import { providersConfig } from "./providers.config";
import { ProviderConfig, ProvidersConfigFile, ProviderEndpoints, ProviderUserInfo } from "./types";
export class ProviderManager {
private config!: ProvidersConfigFile;
constructor() {
this.loadConfiguration();
}
private loadConfiguration() {
try {
this.config = providersConfig;
} catch (error) {
console.error("Failed to load providers configuration:", error);
throw new Error("Failed to load providers configuration");
}
}
/**
* Determina se um provider é oficialmente suportado
*/
isOfficialProvider(providerName: string): boolean {
const normalizedName = this.normalizeProviderName(providerName);
return normalizedName in this.config.officialProviders;
}
/**
* Obtém configuração para um provider de forma INTELIGENTE
*/
getProviderConfig(provider: any): ProviderConfig {
console.log(`[ProviderManager] Getting config for provider:`, provider);
console.log(`[ProviderManager] Provider name: ${provider?.name}`);
console.log(`[ProviderManager] Provider type from DB: ${provider?.type}`);
console.log(`[ProviderManager] Provider issuerUrl: ${provider?.issuerUrl}`);
if (!provider || !provider.name) {
console.error(`[ProviderManager] Invalid provider object:`, provider);
throw new Error("Invalid provider object: missing name");
}
// PRIMEIRO: Se é um provider oficial, usa a configuração específica
const officialConfig = this.config.officialProviders[provider.name];
if (officialConfig) {
console.log(`[ProviderManager] Using official config for: ${provider.name}`);
return officialConfig;
}
// SEGUNDO: Detecta automaticamente o tipo baseado na URL
const detectedType = this.detectProviderType(provider.issuerUrl || "");
console.log(`[ProviderManager] Auto-detected type: ${detectedType}`);
// TERCEIRO: Se o provider tem um tipo definido no banco, usa ele
const providerType = provider.type || detectedType;
console.log(`[ProviderManager] Final provider type: ${providerType}`);
// QUARTO: Cria configuração inteligente baseada no tipo detectado
const intelligentConfig: ProviderConfig = {
...this.config.genericProviderTemplate,
name: provider.name,
// Ajusta configuração baseada no tipo detectado
supportsDiscovery: this.shouldSupportDiscovery(providerType),
discoveryEndpoint: this.getDiscoveryEndpoint(providerType),
// Ajusta fallbacks baseado no tipo
fallbackEndpoints: this.getFallbackEndpoints(providerType),
// Ajusta método de autenticação baseado no tipo
authMethod: this.getAuthMethod(providerType),
// Ajusta field mappings baseado no tipo
fieldMappings: this.getFieldMappings(providerType),
// Ajusta special handling baseado no tipo
specialHandling: this.getSpecialHandling(providerType),
};
console.log(`[ProviderManager] Generated intelligent config:`, intelligentConfig);
return intelligentConfig;
}
/**
* Obtém todos os providers oficiais disponíveis
*/
getOfficialProviders(): Record<string, ProviderConfig> {
return this.config.officialProviders;
}
/**
* Resolve endpoints para um provider com base na configuração
* ULTRA-INTELIGENTE com múltiplos fallbacks e detecção automática
*/
async resolveEndpoints(provider: any, config: ProviderConfig): Promise<ProviderEndpoints> {
console.log(`[ProviderManager] Resolving endpoints for provider: ${provider.name}, config: ${config.name}`);
console.log(`[ProviderManager] Provider issuerUrl: ${provider.issuerUrl}`);
console.log(`[ProviderManager] Provider custom endpoints:`, {
auth: provider.authorizationEndpoint,
token: provider.tokenEndpoint,
userInfo: provider.userInfoEndpoint,
});
// PRIMEIRO: Verifica se o usuário especificou endpoints customizados
if (provider.authorizationEndpoint && provider.tokenEndpoint && provider.userInfoEndpoint) {
console.log(`[ProviderManager] Using custom endpoints from user config`);
// Se endpoints são relativos, combina com issuerUrl
const resolvedEndpoints = {
authorizationEndpoint: this.resolveEndpointUrl(provider.authorizationEndpoint, provider.issuerUrl),
tokenEndpoint: this.resolveEndpointUrl(provider.tokenEndpoint, provider.issuerUrl),
userInfoEndpoint: this.resolveEndpointUrl(provider.userInfoEndpoint, provider.issuerUrl),
};
console.log(`[ProviderManager] Resolved custom endpoints:`, resolvedEndpoints);
return resolvedEndpoints;
}
// SEGUNDO: Tenta discovery automático se suportado
let endpoints: ProviderEndpoints = {
authorizationEndpoint: "",
tokenEndpoint: "",
userInfoEndpoint: "",
};
if (config.supportsDiscovery && provider.issuerUrl) {
console.log(`[ProviderManager] Attempting discovery for ${provider.issuerUrl}`);
// Tenta múltiplos endpoints de discovery
const discoveryEndpoints = [
"/.well-known/openid_configuration",
"/.well-known/openid-configuration",
"/.well-known/oauth-authorization-server",
"/.well-known/oauth2-authorization-server",
];
for (const discoveryPath of discoveryEndpoints) {
const discoveredEndpoints = await this.attemptDiscovery(provider.issuerUrl, discoveryPath);
if (discoveredEndpoints) {
console.log(`[ProviderManager] Discovery successful with ${discoveryPath}:`, discoveredEndpoints);
endpoints = discoveredEndpoints;
break;
}
}
if (!endpoints.tokenEndpoint) {
console.log(`[ProviderManager] All discovery attempts failed`);
}
}
// TERCEIRO: Se não conseguiu descobrir, usa fallbacks simples
if (!endpoints.tokenEndpoint) {
console.log(`[ProviderManager] Building fallback endpoints`);
endpoints = this.buildIntelligentFallbackEndpoints(provider);
}
console.log(`[ProviderManager] Final endpoints:`, endpoints);
return endpoints;
}
/**
* Resolve URL de endpoint - combina URLs relativas com issuerUrl
*/
private resolveEndpointUrl(endpoint: string, issuerUrl?: string): string {
// Se endpoint já é absoluto (contém http), retorna como está
if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
return endpoint;
}
// Se não tem issuerUrl, retorna endpoint como está
if (!issuerUrl) {
return endpoint;
}
// Remove trailing slash do issuerUrl
const baseUrl = issuerUrl.replace(/\/$/, "");
// Garante que endpoint comece com /
const path = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
return `${baseUrl}${path}`;
}
/**
* Tenta descobrir endpoints via discovery de forma ROBUSTA
*/
private async attemptDiscovery(issuerUrl: string, discoveryPath: string): Promise<ProviderEndpoints | null> {
try {
const discoveryUrl = `${issuerUrl}${discoveryPath}`;
console.log(`[ProviderManager] Attempting discovery at: ${discoveryUrl}`);
const response = await fetch(discoveryUrl, {
method: "GET",
headers: {
Accept: "application/json",
"User-Agent": "Palmr/1.0",
},
// Timeout mais generoso para discovery
signal: AbortSignal.timeout(10000),
});
if (!response.ok) {
console.log(`[ProviderManager] Discovery failed with status: ${response.status}`);
return null;
}
const discoveryData = (await response.json()) as any;
console.log(`[ProviderManager] Discovery response:`, discoveryData);
// Extrai endpoints com múltiplos fallbacks
const endpoints: ProviderEndpoints = {
authorizationEndpoint: "",
tokenEndpoint: "",
userInfoEndpoint: "",
};
// Authorization endpoint - tenta múltiplos campos
const authEndpoints = [
"authorization_endpoint",
"authorizationEndpoint",
"authorize_endpoint",
"authorizeEndpoint",
"auth_endpoint",
"authEndpoint",
];
for (const field of authEndpoints) {
if (discoveryData[field]) {
endpoints.authorizationEndpoint = discoveryData[field];
break;
}
}
// Token endpoint - tenta múltiplos campos
const tokenEndpoints = ["token_endpoint", "tokenEndpoint", "access_token_endpoint", "accessTokenEndpoint"];
for (const field of tokenEndpoints) {
if (discoveryData[field]) {
endpoints.tokenEndpoint = discoveryData[field];
break;
}
}
// UserInfo endpoint - tenta múltiplos campos
const userInfoEndpoints = [
"userinfo_endpoint",
"userInfoEndpoint",
"user_info_endpoint",
"userInfo_endpoint",
"profile_endpoint",
"profileEndpoint",
];
for (const field of userInfoEndpoints) {
if (discoveryData[field]) {
endpoints.userInfoEndpoint = discoveryData[field];
break;
}
}
// Valida se pelo menos os endpoints essenciais foram encontrados
if (!endpoints.authorizationEndpoint || !endpoints.tokenEndpoint) {
console.log(`[ProviderManager] Discovery incomplete - missing essential endpoints`);
return null;
}
console.log(`[ProviderManager] Discovery successful:`, endpoints);
return endpoints;
} catch (error) {
console.log(`[ProviderManager] Discovery error:`, error);
return null;
}
}
/**
* Constrói endpoints usando fallbacks simples baseados no tipo
*/
private buildIntelligentFallbackEndpoints(provider: any): ProviderEndpoints {
const baseUrl = provider.issuerUrl?.replace(/\/$/, "") || "";
console.log(`[ProviderManager] Building fallback endpoints for baseUrl: ${baseUrl}`);
// Detecta tipo do provider para usar padrão apropriado
const detectedType = this.detectProviderType(provider.issuerUrl || "");
const fallbackPattern = this.getFallbackEndpoints(detectedType);
return {
authorizationEndpoint: `${baseUrl}${fallbackPattern.authorizationEndpoint}`,
tokenEndpoint: `${baseUrl}${fallbackPattern.tokenEndpoint}`,
userInfoEndpoint: `${baseUrl}${fallbackPattern.userInfoEndpoint}`,
};
}
/**
* Extrai informações do usuário com base no mapeamento de campos
*/
extractUserInfo(rawUserInfo: any, config: ProviderConfig): ProviderUserInfo {
console.log(`[ProviderManager] Extracting user info for ${config.name}`);
console.log(`[ProviderManager] Raw user info:`, rawUserInfo);
const userInfo: ProviderUserInfo = {
id: "",
email: "",
};
// Mapeia campos usando as configurações
const mappings = config.fieldMappings;
console.log(`[ProviderManager] Field mappings:`, mappings);
userInfo.id = this.extractField(rawUserInfo, mappings.id) || "";
userInfo.email = this.extractField(rawUserInfo, mappings.email) || "";
userInfo.name = this.extractField(rawUserInfo, mappings.name);
userInfo.firstName = this.extractField(rawUserInfo, mappings.firstName);
userInfo.lastName = this.extractField(rawUserInfo, mappings.lastName);
userInfo.avatar = this.extractField(rawUserInfo, mappings.avatar);
console.log(`[ProviderManager] Extracted user info:`, userInfo);
return userInfo;
}
/**
* Extrai um campo específico usando lista de possíveis nomes de forma INTELIGENTE
*/
private extractField(obj: any, fieldNames: string[]): string | undefined {
if (!obj || !fieldNames.length) return undefined;
console.log(`[ProviderManager] Extracting field from:`, fieldNames);
console.log(`[ProviderManager] Available fields:`, Object.keys(obj));
// Para campos especiais como nome completo que pode ser composto
if (fieldNames.length > 1 && fieldNames.includes("first_name") && fieldNames.includes("last_name")) {
const firstName = obj["first_name"];
const lastName = obj["last_name"];
if (firstName && lastName) {
return `${firstName} ${lastName}`;
} else if (firstName) {
return firstName;
} else if (lastName) {
return lastName;
}
}
// Para campos especiais como nome completo com given_name/family_name
if (fieldNames.length > 1 && fieldNames.includes("given_name") && fieldNames.includes("family_name")) {
const firstName = obj["given_name"];
const lastName = obj["family_name"];
if (firstName && lastName) {
return `${firstName} ${lastName}`;
} else if (firstName) {
return firstName;
} else if (lastName) {
return lastName;
}
}
// Para campos normais - tenta cada nome na lista
for (const fieldName of fieldNames) {
if (obj[fieldName] !== undefined && obj[fieldName] !== null) {
console.log(`[ProviderManager] Found field: ${fieldName} = ${obj[fieldName]}`);
return String(obj[fieldName]);
}
}
// Tenta variações case-insensitive
for (const fieldName of fieldNames) {
const lowerFieldName = fieldName.toLowerCase();
for (const key of Object.keys(obj)) {
if (key.toLowerCase() === lowerFieldName && obj[key] !== undefined && obj[key] !== null) {
console.log(`[ProviderManager] Found field (case-insensitive): ${key} = ${obj[key]}`);
return String(obj[key]);
}
}
}
// Tenta variações com underscores vs camelCase
for (const fieldName of fieldNames) {
const variations = [
fieldName,
fieldName.replace(/_/g, ""),
fieldName.replace(/([A-Z])/g, "_$1").toLowerCase(),
fieldName.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()),
];
for (const variation of variations) {
if (obj[variation] !== undefined && obj[variation] !== null) {
console.log(`[ProviderManager] Found field (variation): ${variation} = ${obj[variation]}`);
return String(obj[variation]);
}
}
}
console.log(`[ProviderManager] Field not found for:`, fieldNames);
return undefined;
}
/**
* Obtém método de autenticação da configuração
*/
getAuthMethod(config: ProviderConfig): "body" | "basic" | "header" {
return config.authMethod || "body";
}
/**
* Determina se precisa buscar email em endpoint separado
*/
requiresEmailFetch(config: ProviderConfig): boolean {
return config.specialHandling?.emailFetchRequired || false;
}
/**
* Obtém endpoint específico para buscar email
*/
getEmailEndpoint(config: ProviderConfig): string | null {
return config.specialHandling?.emailEndpoint || null;
}
/**
* Normaliza nome do provider para busca
*/
private normalizeProviderName(name: string): string {
return name.toLowerCase().trim();
}
/**
* Obtém scopes para um provider de forma INTELIGENTE
*/
getScopes(provider: any): string[] {
console.log(`[ProviderManager] Getting scopes for provider: ${provider.name}`);
console.log(`[ProviderManager] Provider type: ${provider.type}`);
console.log(`[ProviderManager] Provider scope from DB: ${provider.scope}`);
// PRIMEIRO: Se o provider tem scope definido no banco, usa ele
if (provider.scope && provider.scope.trim()) {
const scopes = provider.scope.split(" ").filter((s: string) => s.trim());
console.log(`[ProviderManager] Using scope from database: ${scopes}`);
return scopes;
}
// SEGUNDO: Detecta automaticamente baseado no tipo
const detectedType = this.detectProviderType(provider.issuerUrl || "");
console.log(`[ProviderManager] Auto-detected type: ${detectedType}`);
// Scopes específicos por tipo de provider
const typeScopes: Record<string, string[]> = {
// Frontegg
frontegg: ["openid", "profile", "email"],
// Social providers
discord: ["identify", "email"],
github: ["read:user", "user:email"],
gitlab: ["read_user", "read_api"],
google: ["openid", "profile", "email"],
microsoft: ["openid", "profile", "email", "User.Read"],
facebook: ["public_profile", "email"],
twitter: ["tweet.read", "users.read"],
linkedin: ["r_liteprofile", "r_emailaddress"],
// Enterprise providers
authentik: ["openid", "profile", "email"],
keycloak: ["openid", "profile", "email"],
auth0: ["openid", "profile", "email"],
okta: ["openid", "profile", "email"],
onelogin: ["openid", "profile", "email"],
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"],
teams: ["openid", "profile", "email", "User.Read"],
// Development tools
bitbucket: ["account", "repository"],
atlassian: ["read:jira-user", "read:jira-work"],
jira: ["read:jira-user", "read:jira-work"],
confluence: ["read:confluence-content.summary"],
// Business tools
salesforce: ["api", "refresh_token"],
zendesk: ["read"],
shopify: ["read_products", "read_customers"],
stripe: ["read"],
twilio: ["read"],
sendgrid: ["mail.send"],
mailchimp: ["read"],
hubspot: ["contacts", "crm.objects.contacts.read"],
// Productivity
zoom: ["user:read:admin"],
notion: ["read"],
figma: ["files:read"],
dropbox: ["files.content.read"],
box: ["root_readwrite"],
trello: ["read"],
asana: ["default"],
monday: ["read"],
clickup: ["read"],
linear: ["read"],
};
// TERCEIRO: Se encontrou tipo específico, usa os scopes dele
if (typeScopes[detectedType]) {
console.log(`[ProviderManager] Using type-specific scopes for ${detectedType}: ${typeScopes[detectedType]}`);
return typeScopes[detectedType];
}
// QUARTO: Se o provider tem um tipo definido no banco, tenta usar ele
if (provider.type && typeScopes[provider.type]) {
console.log(`[ProviderManager] Using database type scopes for ${provider.type}: ${typeScopes[provider.type]}`);
return typeScopes[provider.type];
}
// QUINTO: Fallback para scopes OIDC padrão
console.log(`[ProviderManager] Using default OIDC scopes: openid, profile, email`);
return ["openid", "profile", "email"];
}
/**
* Detecta automaticamente o tipo de provider baseado na URL
*/
private detectProviderType(issuerUrl: string): string {
const url = issuerUrl.toLowerCase();
// Padrões conhecidos para detecção automática
const patterns = [
{ pattern: "frontegg.com", type: "frontegg" },
{ pattern: "discord.com", type: "discord" },
{ pattern: "github.com", type: "github" },
{ pattern: "gitlab.com", type: "gitlab" },
{ pattern: "google.com", type: "google" },
{ pattern: "microsoft.com", type: "microsoft" },
{ pattern: "facebook.com", type: "facebook" },
{ pattern: "twitter.com", type: "twitter" },
{ pattern: "linkedin.com", type: "linkedin" },
{ pattern: "authentik", type: "authentik" },
{ pattern: "keycloak", type: "keycloak" },
{ pattern: "auth0.com", type: "auth0" },
{ pattern: "okta.com", type: "okta" },
{ pattern: "onelogin.com", type: "onelogin" },
{ pattern: "pingidentity.com", type: "ping" },
{ pattern: "azure.com", type: "azure" },
{ pattern: "aws.amazon.com", type: "aws" },
{ pattern: "slack.com", type: "slack" },
{ pattern: "bitbucket.org", type: "bitbucket" },
{ pattern: "atlassian.com", type: "atlassian" },
{ pattern: "salesforce.com", type: "salesforce" },
{ pattern: "zendesk.com", type: "zendesk" },
{ pattern: "shopify.com", type: "shopify" },
{ pattern: "stripe.com", type: "stripe" },
{ pattern: "twilio.com", type: "twilio" },
{ pattern: "sendgrid.com", type: "sendgrid" },
{ pattern: "mailchimp.com", type: "mailchimp" },
{ pattern: "hubspot.com", type: "hubspot" },
{ pattern: "zoom.us", type: "zoom" },
{ pattern: "teams.microsoft.com", type: "teams" },
{ pattern: "notion.so", type: "notion" },
{ pattern: "figma.com", type: "figma" },
{ pattern: "dropbox.com", type: "dropbox" },
{ pattern: "box.com", type: "box" },
{ pattern: "trello.com", type: "trello" },
{ pattern: "asana.com", type: "asana" },
{ pattern: "monday.com", type: "monday" },
{ pattern: "clickup.com", type: "clickup" },
{ pattern: "linear.app", type: "linear" },
{ pattern: "kinde.com", type: "kinde" },
{ pattern: "zitadel.com", type: "zitadel" },
{ 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" },
];
for (const { pattern, type } of patterns) {
if (url.includes(pattern)) {
console.log(`[ProviderManager] Auto-detected provider type: ${type} from URL: ${issuerUrl}`);
return type;
}
}
// Se não encontrou padrão conhecido, tenta extrair domínio
try {
const domain = new URL(issuerUrl).hostname.replace("www.", "");
console.log(`[ProviderManager] No known pattern found, using domain: ${domain}`);
return domain;
} catch {
console.log(`[ProviderManager] Could not parse URL, using 'custom'`);
return "custom";
}
}
/**
* Determina se o provider deve suportar discovery baseado no tipo
*/
private shouldSupportDiscovery(providerType: string): boolean {
// Providers que geralmente suportam discovery
const discoveryProviders = [
"frontegg",
"oidc",
"authentik",
"keycloak",
"auth0",
"okta",
"onelogin",
"ping",
"azure",
"aws",
"google",
"microsoft",
];
return discoveryProviders.includes(providerType);
}
/**
* Obtém o endpoint de discovery apropriado para o tipo
*/
private getDiscoveryEndpoint(providerType: string): string {
const discoveryEndpoints: Record<string, string> = {
frontegg: "/.well-known/openid_configuration",
oidc: "/.well-known/openid_configuration",
authentik: "/.well-known/openid_configuration",
keycloak: "/.well-known/openid_configuration",
auth0: "/.well-known/openid_configuration",
okta: "/.well-known/openid_configuration",
onelogin: "/.well-known/openid_configuration",
ping: "/.well-known/openid_configuration",
azure: "/.well-known/openid_configuration",
aws: "/.well-known/openid_configuration",
google: "/.well-known/openid_configuration",
microsoft: "/.well-known/openid_configuration",
};
return discoveryEndpoints[providerType] || "/.well-known/openid_configuration";
}
/**
* Obtém fallback endpoints apropriados para o tipo
*/
private getFallbackEndpoints(providerType: string): any {
const fallbackPatterns: Record<string, any> = {
// Frontegg
frontegg: {
authorizationEndpoint: "/oauth/authorize",
tokenEndpoint: "/oauth/token",
userInfoEndpoint: "/api/oauth/userinfo",
},
// OIDC padrão
oidc: {
authorizationEndpoint: "/oauth2/authorize",
tokenEndpoint: "/oauth2/token",
userInfoEndpoint: "/oauth2/userinfo",
},
// OAuth2 padrão
oauth2: {
authorizationEndpoint: "/oauth/authorize",
tokenEndpoint: "/oauth/token",
userInfoEndpoint: "/oauth/userinfo",
},
// GitHub
github: {
authorizationEndpoint: "/login/oauth/authorize",
tokenEndpoint: "/login/oauth/access_token",
userInfoEndpoint: "/user",
},
// GitLab
gitlab: {
authorizationEndpoint: "/oauth/authorize",
tokenEndpoint: "/oauth/token",
userInfoEndpoint: "/api/v4/user",
},
// Discord
discord: {
authorizationEndpoint: "/oauth2/authorize",
tokenEndpoint: "/oauth2/token",
userInfoEndpoint: "/users/@me",
},
// Slack
slack: {
authorizationEndpoint: "/oauth/authorize",
tokenEndpoint: "/api/oauth.access",
userInfoEndpoint: "/api/users.identity",
},
};
return (
fallbackPatterns[providerType] || {
authorizationEndpoint: "/oauth2/authorize",
tokenEndpoint: "/oauth2/token",
userInfoEndpoint: "/oauth2/userinfo",
}
);
}
/**
* Obtém field mappings apropriados para o tipo
*/
private getFieldMappings(providerType: string): any {
const fieldMappings: Record<string, any> = {
// Frontegg
frontegg: {
id: ["sub", "id", "user_id"],
email: ["email", "preferred_username"],
name: ["name", "preferred_username"],
firstName: ["given_name", "name"],
lastName: ["family_name"],
avatar: ["picture"],
},
// GitHub
github: {
id: ["id"],
email: ["email"],
name: ["name", "login"],
firstName: ["name"],
lastName: [""],
avatar: ["avatar_url"],
},
// GitLab
gitlab: {
id: ["id"],
email: ["email"],
name: ["name", "username"],
firstName: ["name"],
lastName: [""],
avatar: ["avatar_url"],
},
// Discord
discord: {
id: ["id"],
email: ["email"],
name: ["username", "global_name"],
firstName: ["username"],
lastName: [""],
avatar: ["avatar"],
},
// Slack
slack: {
id: ["id"],
email: ["email"],
name: ["name", "real_name"],
firstName: ["name"],
lastName: [""],
avatar: ["image_192"],
},
};
return fieldMappings[providerType] || this.config.genericProviderTemplate.fieldMappings;
}
/**
* Obtém special handling apropriado para o tipo
*/
private getSpecialHandling(providerType: string): any {
const specialHandling: Record<string, any> = {
// Frontegg - usa OAuth2 padrão
frontegg: {
emailEndpoint: "",
emailFetchRequired: false,
responseFormat: "json",
urlCleaning: {
removeFromEnd: [],
},
},
// GitHub - precisa de endpoint adicional para email
github: {
emailEndpoint: "/user/emails",
emailFetchRequired: true,
responseFormat: "json",
},
// GitLab - OAuth2 padrão
gitlab: {
emailEndpoint: "",
emailFetchRequired: false,
responseFormat: "json",
},
// Discord - OAuth2 padrão
discord: {
emailEndpoint: "",
emailFetchRequired: false,
responseFormat: "json",
},
};
return (
specialHandling[providerType] || {
emailEndpoint: "",
emailFetchRequired: false,
responseFormat: "json",
}
);
}
}

View File

@@ -1,5 +1,82 @@
import { ProviderConfig, ProvidersConfigFile } from "./types";
export const PROVIDER_PATTERNS = [
{ pattern: "frontegg.com", type: "frontegg" },
{ pattern: "discord.com", type: "discord" },
{ pattern: "github.com", type: "github" },
{ pattern: "gitlab.com", type: "gitlab" },
{ pattern: "google.com", type: "google" },
{ pattern: "microsoft.com", type: "microsoft" },
{ pattern: "authentik", type: "authentik" },
{ pattern: "keycloak", type: "keycloak" },
{ pattern: "auth0.com", type: "auth0" },
{ pattern: "okta.com", type: "okta" },
{ pattern: "kinde.com", type: "kinde" },
{ pattern: "zitadel.com", type: "zitadel" },
] as const;
export const DEFAULT_SCOPES_BY_TYPE: Record<string, string[]> = {
frontegg: ["openid", "profile", "email"],
discord: ["identify", "email"],
github: ["read:user", "user:email"],
gitlab: ["read_user", "read_api"],
google: ["openid", "profile", "email"],
microsoft: ["openid", "profile", "email", "User.Read"],
authentik: ["openid", "profile", "email"],
keycloak: ["openid", "profile", "email"],
auth0: ["openid", "profile", "email"],
okta: ["openid", "profile", "email"],
kinde: ["openid", "profile", "email"],
zitadel: ["openid", "profile", "email"],
} as const;
export const DISCOVERY_SUPPORTED_PROVIDERS = [
"frontegg",
"oidc",
"authentik",
"keycloak",
"auth0",
"okta",
"google",
"microsoft",
"kinde",
"zitadel",
] as const;
export const DISCOVERY_PATHS = [
"/.well-known/openid_configuration",
"/.well-known/openid-configuration",
"/.well-known/oauth-authorization-server",
] as const;
export const FALLBACK_ENDPOINTS: Record<string, any> = {
frontegg: {
authorizationEndpoint: "/oauth/authorize",
tokenEndpoint: "/oauth/token",
userInfoEndpoint: "/api/oauth/userinfo",
},
github: {
authorizationEndpoint: "/login/oauth/authorize",
tokenEndpoint: "/login/oauth/access_token",
userInfoEndpoint: "/user",
},
gitlab: {
authorizationEndpoint: "/oauth/authorize",
tokenEndpoint: "/oauth/token",
userInfoEndpoint: "/api/v4/user",
},
discord: {
authorizationEndpoint: "/oauth2/authorize",
tokenEndpoint: "/oauth2/token",
userInfoEndpoint: "/users/@me",
},
oidc: {
authorizationEndpoint: "/oauth2/authorize",
tokenEndpoint: "/oauth2/token",
userInfoEndpoint: "/oauth2/userinfo",
},
} as const;
/**
* Configuração técnica oficial do Discord
* OAuth2 com mapeamentos específicos do Discord
@@ -216,3 +293,38 @@ export {
fronteggConfig,
genericProviderTemplate,
};
export function detectProviderType(issuerUrl: string): string {
const url = issuerUrl.toLowerCase();
for (const { pattern, type } of PROVIDER_PATTERNS) {
if (url.includes(pattern)) {
return type;
}
}
try {
return new URL(issuerUrl).hostname.replace("www.", "");
} catch {
return "custom";
}
}
export function getProviderScopes(provider: any): string[] {
if (provider.scope) {
return provider.scope.split(" ").filter((s: string) => s.trim());
}
const detectedType = detectProviderType(provider.issuerUrl || "");
return (
DEFAULT_SCOPES_BY_TYPE[detectedType] || DEFAULT_SCOPES_BY_TYPE[provider.type] || ["openid", "profile", "email"]
);
}
export function shouldSupportDiscovery(providerType: string): boolean {
return DISCOVERY_SUPPORTED_PROVIDERS.includes(providerType as any);
}
export function getFallbackEndpoints(providerType: string): any {
return FALLBACK_ENDPOINTS[providerType] || FALLBACK_ENDPOINTS.oidc;
}

View File

@@ -7,20 +7,16 @@ import { z } from "zod";
export async function authProvidersRoutes(fastify: FastifyInstance) {
const authProvidersController = new AuthProvidersController();
// Admin-only middleware
const adminPreValidation = async (request: any, reply: any) => {
try {
const usersCount = await prisma.user.count();
// Se há apenas 1 usuário ou menos, permite acesso (setup inicial)
if (usersCount <= 1) {
return;
}
// Verifica JWT
await request.jwtVerify();
// Verifica se é admin
if (!request.user.isAdmin) {
return reply.status(403).send({
success: false,
@@ -36,7 +32,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
}
};
// Get enabled providers for login page
fastify.get(
"/providers",
{
@@ -69,7 +64,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
authProvidersController.getProviders.bind(authProvidersController)
);
// Get all providers (admin only)
fastify.get(
"/providers/all",
{
@@ -102,7 +96,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
authProvidersController.getAllProviders.bind(authProvidersController)
);
// Create new provider (admin only)
fastify.post(
"/providers",
{
@@ -141,7 +134,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
authProvidersController.createProvider.bind(authProvidersController)
);
// Update providers order (admin only) - MUST be before /providers/:id route
fastify.put(
"/providers/order",
{
@@ -179,7 +171,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
authProvidersController.updateProvidersOrder.bind(authProvidersController)
);
// Update provider configuration (admin only)
fastify.put(
"/providers/:id",
{
@@ -193,7 +184,7 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
params: z.object({
id: z.string(),
}),
body: z.any(), // Validação manual no controller para providers oficiais
body: z.any(),
response: {
200: z.object({
success: z.boolean(),
@@ -221,7 +212,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
authProvidersController.updateProvider.bind(authProvidersController)
);
// Delete provider (admin only)
fastify.delete(
"/providers/:id",
{
@@ -257,7 +247,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
authProvidersController.deleteProvider.bind(authProvidersController)
);
// Initiate authentication with specific provider
fastify.get(
"/providers/:provider/authorize",
{
@@ -289,7 +278,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
authProvidersController.authorize.bind(authProvidersController)
);
// Handle callback from provider
fastify.get(
"/providers/:provider/callback",
{

File diff suppressed because it is too large Load Diff

View File

@@ -61,3 +61,61 @@ export interface AuthResult {
userInfo: ProviderUserInfo;
tokens: TokenResponse;
}
export interface RequestContext {
protocol: string;
host: string;
headers: any;
}
export interface CreateProviderRequest {
Body: {
authorizationEndpoint?: string;
tokenEndpoint?: string;
userInfoEndpoint?: string;
issuerUrl?: string;
[key: string]: any;
};
}
export interface UpdateProviderRequest {
Params: { id: string };
Body: any;
}
export interface UpdateProvidersOrderRequest {
Body: { providers: { id: string; sortOrder: number }[] };
}
export interface DeleteProviderRequest {
Params: { id: string };
}
export interface AuthorizeRequest {
Params: { provider: string };
Querystring: {
state?: string;
redirect_uri?: string;
};
}
export interface CallbackRequest {
Params: { provider: string };
Querystring: {
code?: string;
state?: string;
error?: string;
};
}
export interface PendingState {
codeVerifier: string;
redirectUrl: string;
expiresAt: number;
providerId: string;
}
export interface RequestContextService {
protocol: string;
host: string;
}

View File

@@ -8,7 +8,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const url = new URL(request.url);
const queryString = url.search;
// Forward the original host and protocol to backend
const originalHost = request.headers.get("host") || url.host;
const originalProtocol = request.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
@@ -18,17 +17,15 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
"Content-Type": "application/json",
"x-forwarded-host": originalHost,
"x-forwarded-proto": originalProtocol,
// Forward any authorization headers if needed
...Object.fromEntries(
Array.from(request.headers.entries()).filter(
([key]) => key.startsWith("authorization") || key.startsWith("cookie")
)
),
},
redirect: "manual", // Don't follow redirects automatically
redirect: "manual",
});
// If it's a redirect (OAuth flow), forward the redirect
if (apiRes.status >= 300 && apiRes.status < 400) {
const location = apiRes.headers.get("location");
if (location) {

View File

@@ -8,7 +8,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const url = new URL(request.url);
const queryString = url.search;
// Forward the original host and protocol to backend
const originalHost = request.headers.get("host") || url.host;
const originalProtocol = request.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
@@ -18,31 +17,26 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
"Content-Type": "application/json",
"x-forwarded-host": originalHost,
"x-forwarded-proto": originalProtocol,
// Forward any authorization headers if needed
...Object.fromEntries(
Array.from(request.headers.entries()).filter(
([key]) => key.startsWith("authorization") || key.startsWith("cookie")
)
),
},
redirect: "manual", // Don't follow redirects automatically
redirect: "manual",
});
// Handle redirects from the backend
if (apiRes.status >= 300 && apiRes.status < 400) {
const location = apiRes.headers.get("location");
if (location) {
// Create redirect response and forward any set-cookie headers
const response = NextResponse.redirect(location);
// Copy all set-cookie headers from backend response
const setCookieHeaders = apiRes.headers.getSetCookie?.() || [];
if (setCookieHeaders.length > 0) {
setCookieHeaders.forEach((cookie) => {
response.headers.append("set-cookie", cookie);
});
} else {
// Fallback for older implementations
const singleCookie = apiRes.headers.get("set-cookie");
if (singleCookie) {
response.headers.set("set-cookie", singleCookie);
@@ -53,12 +47,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
}
}
// Try to parse JSON response
let data;
try {
data = await apiRes.json();
} catch {
// If not JSON, it might be a redirect or other response
return new NextResponse(null, {
status: apiRes.status,
headers: Object.fromEntries(apiRes.headers.entries()),

View File

@@ -8,7 +8,6 @@ export async function GET(request: NextRequest) {
method: "GET",
headers: {
"Content-Type": "application/json",
// Forward any authorization headers
...Object.fromEntries(
Array.from(request.headers.entries()).filter(
([key]) => key.startsWith("authorization") || key.startsWith("cookie")

View File

@@ -11,7 +11,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
method: "PUT",
headers: {
"Content-Type": "application/json",
// Forward any authorization headers
...Object.fromEntries(
Array.from(request.headers.entries()).filter(
([key]) => key.startsWith("authorization") || key.startsWith("cookie")
@@ -41,7 +40,6 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
const apiRes = await fetch(`${API_BASE_URL}/auth/providers/${id}`, {
method: "DELETE",
headers: {
// Forward any authorization headers (but don't include Content-Type for DELETE)
...Object.fromEntries(
Array.from(request.headers.entries()).filter(
([key]) => key.startsWith("authorization") || key.startsWith("cookie")

View File

@@ -11,7 +11,6 @@ export async function PUT(request: NextRequest) {
headers: {
"Content-Type": "application/json",
cookie: request.headers.get("cookie") || "",
// Forward any authorization headers if needed
...Object.fromEntries(Array.from(request.headers.entries()).filter(([key]) => key.startsWith("authorization"))),
},
body: JSON.stringify(body),

View File

@@ -7,7 +7,6 @@ export async function GET(request: NextRequest) {
const url = new URL(request.url);
const queryString = url.search;
// Forward the original host and protocol to backend
const originalHost = request.headers.get("host") || url.host;
const originalProtocol = request.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
@@ -18,7 +17,6 @@ export async function GET(request: NextRequest) {
"x-forwarded-host": originalHost,
"x-forwarded-proto": originalProtocol,
cookie: request.headers.get("cookie") || "",
// Forward any authorization headers if needed
...Object.fromEntries(Array.from(request.headers.entries()).filter(([key]) => key.startsWith("authorization"))),
},
});
@@ -46,7 +44,6 @@ export async function POST(request: NextRequest) {
headers: {
"Content-Type": "application/json",
cookie: request.headers.get("cookie") || "",
// Forward any authorization headers if needed
...Object.fromEntries(Array.from(request.headers.entries()).filter(([key]) => key.startsWith("authorization"))),
},
body: JSON.stringify(body),

View File

@@ -3,7 +3,6 @@
import React, { useEffect, useState } from "react";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import {
IconAlertTriangle,
IconCheck,
IconChevronDown,
IconChevronUp,
@@ -16,7 +15,6 @@ import {
IconPlus,
IconSettings,
IconTrash,
IconX,
} from "@tabler/icons-react";
import { Globe } from "lucide-react";
import { toast } from "sonner";
@@ -62,7 +60,6 @@ interface NewProvider {
clientSecret: string;
issuerUrl: string;
scope: string;
// Endpoints customizados opcionais
authorizationEndpoint: string;
tokenEndpoint: string;
userInfoEndpoint: string;
@@ -96,13 +93,11 @@ export function AuthProvidersSettings() {
userInfoEndpoint: "",
});
// Auto-sugestão de scopes baseada na Provider URL
const detectProviderTypeAndSuggestScopes = (url: string): string[] => {
if (!url) return [];
const urlLower = url.toLowerCase();
// Padrões conhecidos para detecção automática
const providerPatterns = [
{ pattern: "frontegg.com", scopes: ["openid", "profile", "email"] },
{ pattern: "discord.com", scopes: ["identify", "email"] },
@@ -147,14 +142,12 @@ export function AuthProvidersSettings() {
{ pattern: "zitadel.com", scopes: ["openid", "profile", "email"] },
];
// Procura por padrões conhecidos
for (const { pattern, scopes } of providerPatterns) {
if (urlLower.includes(pattern)) {
return scopes;
}
}
// Fallback baseado no tipo do provider
if (newProvider.type === "oidc") {
return ["openid", "profile", "email"];
} else {
@@ -162,7 +155,6 @@ export function AuthProvidersSettings() {
}
};
// Função para auto-sugerir scopes baseado na Provider URL (onBlur)
const updateProviderUrl = (url: string) => {
if (!url.trim()) return;
@@ -178,7 +170,6 @@ export function AuthProvidersSettings() {
});
};
// Load hide disabled providers state from localStorage
useEffect(() => {
const savedState = localStorage.getItem("hideDisabledProviders");
if (savedState !== null) {
@@ -186,7 +177,6 @@ export function AuthProvidersSettings() {
}
}, []);
// Load providers
useEffect(() => {
loadProviders();
}, []);
@@ -210,7 +200,6 @@ export function AuthProvidersSettings() {
}
};
// Update provider
const updateProvider = async (id: string, updates: Partial<AuthProvider>) => {
try {
setSaving(id);
@@ -236,7 +225,6 @@ export function AuthProvidersSettings() {
}
};
// Delete provider
const deleteProvider = async (id: string, name: string) => {
try {
setIsDeleting(true);
@@ -261,7 +249,6 @@ export function AuthProvidersSettings() {
}
};
// Open delete confirmation modal
const openDeleteModal = (provider: AuthProvider) => {
setProviderToDelete({
id: provider.id,
@@ -270,14 +257,12 @@ export function AuthProvidersSettings() {
});
};
// Add new provider
const addProvider = async () => {
if (!newProvider.name || !newProvider.displayName || !newProvider.clientId || !newProvider.clientSecret) {
toast.error("Please fill in all required fields (name, display name, client ID, client secret)");
return;
}
// Validação de configuração
const hasIssuerUrl = !!newProvider.issuerUrl;
const hasAllCustomEndpoints = !!(
newProvider.authorizationEndpoint &&
@@ -311,7 +296,6 @@ export function AuthProvidersSettings() {
autoRegister: true,
scope: newProvider.scope || (newProvider.type === "oidc" ? "openid profile email" : "user:email"),
sortOrder: providers.length + 1,
// Incluir apenas campos relevantes baseado no modo
...(newProvider.issuerUrl ? { issuerUrl: newProvider.issuerUrl } : {}),
...(newProvider.authorizationEndpoint ? { authorizationEndpoint: newProvider.authorizationEndpoint } : {}),
...(newProvider.tokenEndpoint ? { tokenEndpoint: newProvider.tokenEndpoint } : {}),
@@ -349,7 +333,6 @@ export function AuthProvidersSettings() {
}
};
// Edit provider
const editProvider = async (providerData: Partial<AuthProvider>) => {
if (!editingProvider || !providerData.name || !providerData.displayName) {
toast.error("Please fill in all required fields");
@@ -384,7 +367,6 @@ export function AuthProvidersSettings() {
}
};
// Handle drag and drop
const handleDragEnd = async (result: DropResult) => {
if (!result.destination) return;
@@ -392,14 +374,12 @@ export function AuthProvidersSettings() {
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
// Update local state immediately for better UX
const updatedItems = items.map((provider, index) => ({
...provider,
sortOrder: index + 1,
}));
setProviders(updatedItems);
// Update sortOrder values for API
const updatedProviders = updatedItems.map((provider) => ({
id: provider.id,
sortOrder: provider.sortOrder,
@@ -416,22 +396,18 @@ export function AuthProvidersSettings() {
if (data.success) {
toast.success("Provider order updated");
// No need to reload - state is already updated locally
} else {
toast.error("Failed to update provider order");
// Revert local state on error
await loadProviders();
}
} catch (error) {
console.error("Error updating provider order:", error);
toast.error("Failed to update provider order");
// Revert local state on error
await loadProviders();
}
};
const getProviderIcon = (provider: AuthProvider) => {
// Use the icon saved in the database, fallback to FaCog if not set
const iconName = provider.icon || "FaCog";
return renderIconByName(iconName, "w-5 h-5");
};
@@ -481,7 +457,6 @@ export function AuthProvidersSettings() {
</div>
) : (
<div className="space-y-4">
{/* Add Provider Button */}
<div className="flex justify-between items-center">
<div className="text-sm text-muted-foreground">
{hideDisabledProviders
@@ -503,7 +478,6 @@ export function AuthProvidersSettings() {
</Button>
</div>
{/* Add Provider Form */}
{showAddForm && (
<div className="border border-dashed rounded-lg p-4 space-y-4">
<div className="flex items-center justify-between">
@@ -571,7 +545,6 @@ export function AuthProvidersSettings() {
</div>
</div>
{/* Configuration Method Toggle */}
<div className="space-y-4">
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-800 rounded-lg p-4">
<h4 className="text-sm font-medium mb-3">Configuration Method</h4>
@@ -640,7 +613,6 @@ export function AuthProvidersSettings() {
</div>
</div>
{/* Automatic Discovery Mode */}
{!newProvider.authorizationEndpoint &&
!newProvider.tokenEndpoint &&
!newProvider.userInfoEndpoint && (
@@ -658,7 +630,6 @@ export function AuthProvidersSettings() {
</div>
)}
{/* Manual Endpoints Mode */}
{(newProvider.authorizationEndpoint || newProvider.tokenEndpoint || newProvider.userInfoEndpoint) && (
<div className="space-y-4">
<div>
@@ -714,7 +685,6 @@ export function AuthProvidersSettings() {
)}
</div>
{/* Client Credentials */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="mb-2 block">Client ID *</Label>
@@ -735,7 +705,6 @@ export function AuthProvidersSettings() {
</div>
</div>
{/* OAuth Scopes */}
<div>
<Label className="mb-2 block">OAuth Scopes</Label>
<TagsInput
@@ -750,7 +719,6 @@ export function AuthProvidersSettings() {
</p>
</div>
{/* Show callback URL if provider name is filled */}
{newProvider.name && (
<div className="pt-2">
<CallbackUrlDisplay providerName={newProvider.name} />
@@ -767,7 +735,6 @@ export function AuthProvidersSettings() {
</div>
)}
{/* Hide Disabled Providers Checkbox */}
{providers.length > 0 && (
<div className="flex items-center space-x-2 py-2">
<Checkbox
@@ -781,7 +748,6 @@ export function AuthProvidersSettings() {
</div>
)}
{/* Providers List - Compact with Drag and Drop */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
@@ -892,7 +858,6 @@ export function AuthProvidersSettings() {
)}
</CardContent>
{/* Delete Confirmation Modal */}
<AuthProviderDeleteModal
isOpen={!!providerToDelete}
onClose={() => setProviderToDelete(null)}
@@ -908,7 +873,6 @@ export function AuthProvidersSettings() {
);
}
// Compact provider row component
interface ProviderRowProps {
provider: AuthProvider;
onUpdate: (updates: Partial<AuthProvider>) => void;
@@ -946,10 +910,8 @@ function ProviderRow({
return (
<div className={`border rounded-lg ${isDragging ? "border-blue-300 bg-blue-50 dark:bg-blue-950/20" : ""}`}>
{/* Compact Header */}
<div className="flex items-center justify-between p-3">
<div className="flex items-center gap-3">
{/* Drag Handle */}
{!isDragDisabled ? (
<div
{...dragHandleProps}
@@ -963,7 +925,6 @@ function ProviderRow({
<span className="text-lg">{getIcon(provider)}</span>
<div>
<div className="flex items-center gap-2">
{/* Status dot */}
<div
className={`w-2 h-2 rounded-full ${provider.enabled ? "bg-green-500" : "bg-gray-400"}`}
title={provider.enabled ? "Enabled" : "Disabled"}
@@ -996,7 +957,6 @@ function ProviderRow({
</div>
</div>
{/* Edit Form - Shows right below this provider when editing */}
{isEditing && (
<div className="border-t border-border dark:border-border p-4 space-y-4 bg-muted/50 dark:bg-muted/20">
<div className="flex items-center justify-between">
@@ -1019,7 +979,6 @@ function ProviderRow({
);
}
// Edit Provider Form Component
interface EditProviderFormProps {
provider: AuthProvider;
onSave: (data: Partial<AuthProvider>) => void;
@@ -1037,7 +996,6 @@ function EditProviderForm({
editingFormData,
setEditingFormData,
}: EditProviderFormProps) {
// Usar dados preservados se existirem, senão usar dados do provider
const savedData = editingFormData[provider.id] || {};
const [formData, setFormData] = useState({
name: savedData.name || provider.name || "",
@@ -1058,13 +1016,11 @@ function EditProviderForm({
const [showClientSecret, setShowClientSecret] = useState(false);
const isOfficial = provider.isOfficial;
// Auto-sugestão de scopes para formulário de edição
const detectProviderTypeAndSuggestScopesEdit = (url: string, currentType: string): string[] => {
if (!url) return [];
const urlLower = url.toLowerCase();
// Mesmos padrões do formulário de adição
const providerPatterns = [
{ pattern: "frontegg.com", scopes: ["openid", "profile", "email"] },
{ pattern: "discord.com", scopes: ["identify", "email"] },
@@ -1081,14 +1037,12 @@ function EditProviderForm({
{ pattern: "zitadel.com", scopes: ["openid", "profile", "email"] },
];
// Procura por padrões conhecidos
for (const { pattern, scopes } of providerPatterns) {
if (urlLower.includes(pattern)) {
return scopes;
}
}
// Fallback baseado no tipo do provider
if (currentType === "oidc") {
return ["openid", "profile", "email"];
} else {
@@ -1096,12 +1050,10 @@ function EditProviderForm({
}
};
// Função para auto-sugerir scopes baseado na Provider URL no formulário de edição (onBlur)
const updateProviderUrlEdit = (url: string) => {
if (!url.trim()) return;
if (isOfficial) {
// Para providers oficiais, não faz auto-sugestão de scopes
return;
}
@@ -1109,7 +1061,6 @@ function EditProviderForm({
const shouldUpdateScopes =
!formData.scope || formData.scope === "openid profile email" || formData.scope === "profile email";
// Só atualiza scopes, não a URL (já foi atualizada pelo onChange)
if (shouldUpdateScopes) {
updateFormData({
scope: suggestedScopes.join(" "),
@@ -1146,10 +1097,8 @@ function EditProviderForm({
</p>
</div>
)}
{/* Callback URL Display */}
<CallbackUrlDisplay providerName={formData.name || "provider"} />
{/* Only show basic fields for non-official providers */}
{!isOfficial && (
<div className="grid grid-cols-2 gap-4">
<div>
@@ -1195,7 +1144,6 @@ function EditProviderForm({
</div>
)}
{/* Configuration - Only for custom providers */}
{!isOfficial && (
<div className="space-y-4">
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-800 rounded-lg p-4">
@@ -1248,7 +1196,6 @@ function EditProviderForm({
</div>
</div>
{/* Automatic Discovery Mode */}
{!formData.authorizationEndpoint && !formData.tokenEndpoint && !formData.userInfoEndpoint && (
<div>
<Label className="mb-2 block">Provider URL *</Label>
@@ -1264,7 +1211,6 @@ function EditProviderForm({
</div>
)}
{/* Manual Endpoints Mode */}
{(formData.authorizationEndpoint || formData.tokenEndpoint || formData.userInfoEndpoint) && (
<div className="space-y-4">
<div>
@@ -1319,7 +1265,6 @@ function EditProviderForm({
</div>
)}
{/* Official Provider - Only Provider URL and Icon */}
{isOfficial && (
<div className="space-y-4">
<div>
@@ -1428,7 +1373,6 @@ function EditProviderForm({
);
}
// Callback URL Display Component
interface CallbackUrlDisplayProps {
providerName: string;
}
@@ -1436,7 +1380,6 @@ interface CallbackUrlDisplayProps {
function CallbackUrlDisplay({ providerName }: CallbackUrlDisplayProps) {
const [copied, setCopied] = useState(false);
// Usar a URL atual da página
const callbackUrl =
typeof window !== "undefined"
? `${window.location.origin}/api/auth/providers/${providerName}/callback`

View File

@@ -24,12 +24,10 @@ export function SettingsForm({
return (
<div className="flex flex-col gap-6">
{sortedGroups.map(([group, configs]) => {
// Render custom auth providers component
if (group === "auth-providers") {
return <AuthProvidersSettings key={group} />;
}
// Only render SettingsGroup if we have a form for this group
const form = groupForms[group as ValidGroup];
if (!form) {
return null;