mirror of
https://github.com/kyantech/Palmr.git
synced 2025-11-02 13:03:15 +00:00
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:
@@ -1,126 +1,19 @@
|
||||
import { UpdateAuthProviderSchema } from "./dto";
|
||||
import { AuthProvidersService } from "./service";
|
||||
import {
|
||||
AuthorizeRequest,
|
||||
CallbackRequest,
|
||||
CreateProviderRequest,
|
||||
DeleteProviderRequest,
|
||||
RequestContext,
|
||||
UpdateProviderRequest,
|
||||
UpdateProvidersOrderRequest,
|
||||
} from "./types";
|
||||
import { FastifyRequest, FastifyReply } from "fastify";
|
||||
|
||||
export class AuthProvidersController {
|
||||
private authProvidersService: AuthProvidersService;
|
||||
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
constructor() {
|
||||
this.authProvidersService = new AuthProvidersService();
|
||||
}
|
||||
|
||||
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 providers = await this.authProvidersService.getEnabledProviders(requestContext);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: providers,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error getting auth providers:", error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
error: "Failed to get auth providers",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getAllProviders(request: FastifyRequest, reply: FastifyReply) {
|
||||
if (reply.sent) return;
|
||||
|
||||
try {
|
||||
const providers = await this.authProvidersService.getAllProviders();
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: providers,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error getting all providers:", error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
error: "Failed to get providers",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async createProvider(request: FastifyRequest<{ Body: any }>, reply: FastifyReply) {
|
||||
if (reply.sent) return;
|
||||
|
||||
try {
|
||||
const data = request.body as any;
|
||||
|
||||
// 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 provider = await this.authProvidersService.createProvider(data);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: 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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async updateProvider(request: FastifyRequest<{ Params: { id: string }; Body: any }>, reply: FastifyReply) {
|
||||
if (reply.sent) return;
|
||||
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const data = request.body as any;
|
||||
|
||||
// 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",
|
||||
});
|
||||
}
|
||||
|
||||
const isOfficial = this.authProvidersService.isOfficialProvider(existingProvider.name);
|
||||
|
||||
// Para providers oficiais, só permite alterar issuerUrl, clientId, clientSecret, enabled, autoRegister, icon
|
||||
if (isOfficial) {
|
||||
const allowedFields = [
|
||||
const OFFICIAL_PROVIDER_ALLOWED_FIELDS = [
|
||||
"issuerUrl",
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
@@ -129,113 +22,268 @@ export class AuthProvidersController {
|
||||
"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;
|
||||
|
||||
constructor() {
|
||||
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 allowedFields) {
|
||||
for (const field of OFFICIAL_PROVIDER_ALLOWED_FIELDS) {
|
||||
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);
|
||||
return 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",
|
||||
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 = this.buildRequestContext(request);
|
||||
const providers = await this.authProvidersService.getEnabledProviders(requestContext);
|
||||
|
||||
return this.sendSuccessResponse(reply, providers);
|
||||
} catch (error) {
|
||||
return this.handleControllerError(reply, error, "Failed to get auth providers");
|
||||
}
|
||||
}
|
||||
|
||||
async getAllProviders(request: FastifyRequest, reply: FastifyReply) {
|
||||
if (reply.sent) return;
|
||||
|
||||
try {
|
||||
const providers = await this.authProvidersService.getAllProviders();
|
||||
return this.sendSuccessResponse(reply, providers);
|
||||
} catch (error) {
|
||||
return this.handleControllerError(reply, error, "Failed to get providers");
|
||||
}
|
||||
}
|
||||
|
||||
async createProvider(request: FastifyRequest<CreateProviderRequest>, reply: FastifyReply) {
|
||||
if (reply.sent) return;
|
||||
|
||||
try {
|
||||
const data = request.body;
|
||||
|
||||
const validationError = this.validateCustomEndpoints(data);
|
||||
if (validationError) {
|
||||
return this.sendErrorResponse(reply, 400, validationError);
|
||||
}
|
||||
|
||||
const provider = await this.authProvidersService.createProvider(data);
|
||||
return this.sendSuccessResponse(reply, provider);
|
||||
} catch (error) {
|
||||
return this.handleControllerError(reply, error, "Failed to create provider");
|
||||
}
|
||||
}
|
||||
|
||||
async updateProvider(request: FastifyRequest<UpdateProviderRequest>, reply: FastifyReply) {
|
||||
if (reply.sent) return;
|
||||
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const data = request.body;
|
||||
|
||||
const existingProvider = await this.authProvidersService.getProviderById(id);
|
||||
if (!existingProvider) {
|
||||
return this.sendErrorResponse(reply, 404, ERROR_MESSAGES.PROVIDER_NOT_FOUND);
|
||||
}
|
||||
|
||||
const isOfficial = this.authProvidersService.isOfficialProvider(existingProvider.name);
|
||||
|
||||
if (isOfficial) {
|
||||
return this.updateOfficialProvider(reply, id, data);
|
||||
}
|
||||
|
||||
return this.updateCustomProvider(reply, id, data);
|
||||
} catch (error) {
|
||||
return this.handleControllerError(reply, error, "Failed to update provider");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
console.log(`[Controller] Provider updated successfully:`, provider?.id);
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: provider,
|
||||
});
|
||||
return this.sendSuccessResponse(reply, provider);
|
||||
}
|
||||
|
||||
// Para providers customizados, aplica validação normal
|
||||
private async updateCustomProvider(reply: FastifyReply, id: string, data: any) {
|
||||
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,
|
||||
});
|
||||
return this.sendSuccessResponse(reply, 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",
|
||||
});
|
||||
}
|
||||
} 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.sendErrorResponse(reply, 400, ERROR_MESSAGES.INVALID_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
async updateProvidersOrder(
|
||||
request: FastifyRequest<{ Body: { providers: { id: string; sortOrder: number }[] } }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
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}.`;
|
||||
return this.handleCallbackError(request, reply, error);
|
||||
}
|
||||
}
|
||||
|
||||
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}`;
|
||||
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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user