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,7 +1,41 @@
|
|||||||
import { UpdateAuthProviderSchema } from "./dto";
|
import { UpdateAuthProviderSchema } from "./dto";
|
||||||
import { AuthProvidersService } from "./service";
|
import { AuthProvidersService } from "./service";
|
||||||
|
import {
|
||||||
|
AuthorizeRequest,
|
||||||
|
CallbackRequest,
|
||||||
|
CreateProviderRequest,
|
||||||
|
DeleteProviderRequest,
|
||||||
|
RequestContext,
|
||||||
|
UpdateProviderRequest,
|
||||||
|
UpdateProvidersOrderRequest,
|
||||||
|
} from "./types";
|
||||||
import { FastifyRequest, FastifyReply } from "fastify";
|
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 {
|
export class AuthProvidersController {
|
||||||
private authProvidersService: AuthProvidersService;
|
private authProvidersService: AuthProvidersService;
|
||||||
|
|
||||||
@@ -9,26 +43,148 @@ export class AuthProvidersController {
|
|||||||
this.authProvidersService = new AuthProvidersService();
|
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) {
|
async getProviders(request: FastifyRequest, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const requestContext = {
|
const requestContext = this.buildRequestContext(request);
|
||||||
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);
|
const providers = await this.authProvidersService.getEnabledProviders(requestContext);
|
||||||
|
|
||||||
return reply.send({
|
return this.sendSuccessResponse(reply, providers);
|
||||||
success: true,
|
|
||||||
data: providers,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting auth providers:", error);
|
return this.handleControllerError(reply, error, "Failed to get auth providers");
|
||||||
return reply.status(500).send({
|
|
||||||
success: false,
|
|
||||||
error: "Failed to get auth providers",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,205 +193,97 @@ export class AuthProvidersController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const providers = await this.authProvidersService.getAllProviders();
|
const providers = await this.authProvidersService.getAllProviders();
|
||||||
|
return this.sendSuccessResponse(reply, providers);
|
||||||
return reply.send({
|
|
||||||
success: true,
|
|
||||||
data: providers,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting all providers:", error);
|
return this.handleControllerError(reply, error, "Failed to get providers");
|
||||||
return reply.status(500).send({
|
|
||||||
success: false,
|
|
||||||
error: "Failed to get providers",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createProvider(request: FastifyRequest<{ Body: any }>, reply: FastifyReply) {
|
async createProvider(request: FastifyRequest<CreateProviderRequest>, reply: FastifyReply) {
|
||||||
if (reply.sent) return;
|
if (reply.sent) return;
|
||||||
|
|
||||||
try {
|
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 validationError = this.validateCustomEndpoints(data);
|
||||||
const hasAnyCustomEndpoint = !!(data.authorizationEndpoint || data.tokenEndpoint || data.userInfoEndpoint);
|
if (validationError) {
|
||||||
const hasAllCustomEndpoints = !!(data.authorizationEndpoint && data.tokenEndpoint && data.userInfoEndpoint);
|
return this.sendErrorResponse(reply, 400, validationError);
|
||||||
|
|
||||||
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);
|
const provider = await this.authProvidersService.createProvider(data);
|
||||||
|
return this.sendSuccessResponse(reply, provider);
|
||||||
return reply.send({
|
|
||||||
success: true,
|
|
||||||
data: provider,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating provider:", error);
|
return this.handleControllerError(reply, error, "Failed to create provider");
|
||||||
|
|
||||||
// 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) {
|
async updateProvider(request: FastifyRequest<UpdateProviderRequest>, reply: FastifyReply) {
|
||||||
if (reply.sent) return;
|
if (reply.sent) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { id } = request.params;
|
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);
|
const existingProvider = await this.authProvidersService.getProviderById(id);
|
||||||
if (!existingProvider) {
|
if (!existingProvider) {
|
||||||
return reply.status(404).send({
|
return this.sendErrorResponse(reply, 404, ERROR_MESSAGES.PROVIDER_NOT_FOUND);
|
||||||
success: false,
|
|
||||||
error: "Provider not found",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOfficial = this.authProvidersService.isOfficialProvider(existingProvider.name);
|
const isOfficial = this.authProvidersService.isOfficialProvider(existingProvider.name);
|
||||||
|
|
||||||
// Para providers oficiais, só permite alterar issuerUrl, clientId, clientSecret, enabled, autoRegister, icon
|
|
||||||
if (isOfficial) {
|
if (isOfficial) {
|
||||||
const allowedFields = [
|
return this.updateOfficialProvider(reply, id, data);
|
||||||
"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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Para providers customizados, aplica validação normal
|
return this.updateCustomProvider(reply, id, data);
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating provider:", error);
|
return this.handleControllerError(reply, error, "Failed to update provider");
|
||||||
|
|
||||||
// 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",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProvidersOrder(
|
private async updateOfficialProvider(reply: FastifyReply, id: string, data: any) {
|
||||||
request: FastifyRequest<{ Body: { providers: { id: string; sortOrder: number }[] } }>,
|
const sanitizedData = this.sanitizeOfficialProviderData(data);
|
||||||
reply: FastifyReply
|
|
||||||
) {
|
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;
|
if (reply.sent) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { providers } = request.body;
|
const { providers } = request.body;
|
||||||
|
|
||||||
if (!Array.isArray(providers)) {
|
if (!Array.isArray(providers)) {
|
||||||
return reply.status(400).send({
|
return this.sendErrorResponse(reply, 400, ERROR_MESSAGES.INVALID_PROVIDERS_ARRAY);
|
||||||
success: false,
|
|
||||||
error: "Invalid providers array",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.authProvidersService.updateProvidersOrder(providers);
|
await this.authProvidersService.updateProvidersOrder(providers);
|
||||||
|
return this.sendSuccessResponse(reply, undefined, "Providers order updated successfully");
|
||||||
return reply.send({
|
|
||||||
success: true,
|
|
||||||
message: "Providers order updated successfully",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating providers order:", error);
|
return this.handleControllerError(reply, error, "Failed to update providers order");
|
||||||
return reply.status(500).send({
|
|
||||||
success: false,
|
|
||||||
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;
|
if (reply.sent) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -243,47 +291,27 @@ export class AuthProvidersController {
|
|||||||
|
|
||||||
const provider = await this.authProvidersService.getProviderById(id);
|
const provider = await this.authProvidersService.getProviderById(id);
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
return reply.status(404).send({
|
return this.sendErrorResponse(reply, 404, ERROR_MESSAGES.PROVIDER_NOT_FOUND);
|
||||||
success: false,
|
|
||||||
error: "Provider not found",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOfficial = this.authProvidersService.isOfficialProvider(provider.name);
|
const isOfficial = this.authProvidersService.isOfficialProvider(provider.name);
|
||||||
if (isOfficial) {
|
if (isOfficial) {
|
||||||
return reply.status(400).send({
|
return this.sendErrorResponse(reply, 400, ERROR_MESSAGES.OFFICIAL_CANNOT_DELETE);
|
||||||
success: false,
|
|
||||||
error: "Official providers cannot be deleted",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.authProvidersService.deleteProvider(id);
|
await this.authProvidersService.deleteProvider(id);
|
||||||
|
return this.sendSuccessResponse(reply, undefined, "Provider deleted successfully");
|
||||||
return reply.send({
|
|
||||||
success: true,
|
|
||||||
message: "Provider deleted successfully",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting provider:", error);
|
return this.handleControllerError(reply, error, "Failed to delete provider");
|
||||||
return reply.status(500).send({
|
|
||||||
success: false,
|
|
||||||
error: "Failed to delete provider",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async authorize(request: FastifyRequest<{ Params: { provider: string }; Querystring: any }>, reply: FastifyReply) {
|
async authorize(request: FastifyRequest<AuthorizeRequest>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { provider: providerName } = request.params;
|
const { provider: providerName } = request.params;
|
||||||
const query = request.query as any;
|
const { state, redirect_uri } = request.query;
|
||||||
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 requestContext = this.buildRequestContext(request);
|
||||||
const authUrl = await this.authProvidersService.getAuthorizationUrl(
|
const authUrl = await this.authProvidersService.getAuthorizationUrl(
|
||||||
providerName,
|
providerName,
|
||||||
state,
|
state,
|
||||||
@@ -293,62 +321,31 @@ export class AuthProvidersController {
|
|||||||
|
|
||||||
return reply.redirect(authUrl);
|
return reply.redirect(authUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in authorize:", error);
|
const errorMessage = error instanceof Error ? error.message : ERROR_MESSAGES.AUTHORIZATION_FAILED;
|
||||||
return reply.status(400).send({
|
return this.sendErrorResponse(reply, 400, errorMessage);
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "Authorization failed",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async callback(request: FastifyRequest<{ Params: { provider: string }; Querystring: any }>, reply: FastifyReply) {
|
async callback(request: FastifyRequest<CallbackRequest>, reply: FastifyReply) {
|
||||||
console.log(`[Controller] Callback called for provider: ${request.params.provider}`);
|
|
||||||
console.log(`[Controller] Query params:`, request.query);
|
|
||||||
console.log(`[Controller] Headers:`, {
|
|
||||||
host: request.headers.host,
|
|
||||||
"x-forwarded-proto": request.headers["x-forwarded-proto"],
|
|
||||||
"x-forwarded-host": request.headers["x-forwarded-host"],
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { provider: providerName } = request.params;
|
const { provider: providerName } = request.params;
|
||||||
const query = request.query as any;
|
const { code, state, error } = request.query;
|
||||||
const { code, state, error } = query;
|
|
||||||
|
|
||||||
console.log(`[Controller] Extracted params:`, { providerName, code, state, error });
|
const requestContext = this.buildRequestContext(request);
|
||||||
console.log(`[Controller] All query params:`, query);
|
const baseUrl = this.buildBaseUrl(requestContext);
|
||||||
|
|
||||||
const requestContext = {
|
|
||||||
protocol: (request.headers["x-forwarded-proto"] as string) || request.protocol,
|
|
||||||
host: (request.headers["x-forwarded-host"] as string) || (request.headers.host as string),
|
|
||||||
};
|
|
||||||
const baseUrl = `${requestContext.protocol}://${requestContext.host}`;
|
|
||||||
|
|
||||||
console.log(`[Controller] Request context:`, requestContext);
|
|
||||||
console.log(`[Controller] Base URL:`, baseUrl);
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(`OAuth error from ${providerName}:`, error);
|
|
||||||
return reply.redirect(`${baseUrl}/login?error=oauth_error&provider=${providerName}`);
|
return reply.redirect(`${baseUrl}/login?error=oauth_error&provider=${providerName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
console.error(`Missing code parameter for ${providerName}`);
|
|
||||||
return reply.redirect(`${baseUrl}/login?error=missing_code&provider=${providerName}`);
|
return reply.redirect(`${baseUrl}/login?error=missing_code&provider=${providerName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validação de parâmetros obrigatórios
|
if (!state) {
|
||||||
const requiredParams = { code: !!code, state: !!state };
|
|
||||||
const missingParams = Object.entries(requiredParams)
|
|
||||||
.filter(([, hasValue]) => !hasValue)
|
|
||||||
.map(([param]) => param);
|
|
||||||
|
|
||||||
if (missingParams.length > 0) {
|
|
||||||
console.error(`Missing parameters for ${providerName}:`, missingParams);
|
|
||||||
return reply.redirect(`${baseUrl}/login?error=missing_parameters&provider=${providerName}`);
|
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 result = await this.authProvidersService.handleCallback(providerName, code, state, requestContext);
|
||||||
|
|
||||||
const jwt = await request.jwtSign({
|
const jwt = await request.jwtSign({
|
||||||
@@ -356,55 +353,29 @@ export class AuthProvidersController {
|
|||||||
isAdmin: result.user.isAdmin,
|
isAdmin: result.user.isAdmin,
|
||||||
});
|
});
|
||||||
|
|
||||||
reply.setCookie("token", jwt, {
|
this.setAuthCookie(reply, jwt, request.protocol === "https");
|
||||||
httpOnly: true,
|
|
||||||
secure: request.protocol === "https",
|
|
||||||
sameSite: "lax",
|
|
||||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
||||||
path: "/",
|
|
||||||
});
|
|
||||||
|
|
||||||
const redirectUrl = result.redirectUrl || "/dashboard";
|
const redirectUrl = result.redirectUrl || "/dashboard";
|
||||||
const fullRedirectUrl = redirectUrl.startsWith("http") ? redirectUrl : `${baseUrl}${redirectUrl}`;
|
const fullRedirectUrl = redirectUrl.startsWith("http") ? redirectUrl : `${baseUrl}${redirectUrl}`;
|
||||||
console.log(`[Controller] Redirecting to:`, fullRedirectUrl);
|
|
||||||
return reply.redirect(fullRedirectUrl);
|
return reply.redirect(fullRedirectUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error in ${request.params.provider} callback:`, error);
|
return this.handleCallbackError(request, reply, 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}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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";
|
import { z } from "zod";
|
||||||
|
|
||||||
// Schema base para provider
|
|
||||||
export const BaseAuthProviderSchema = z.object({
|
export const BaseAuthProviderSchema = z.object({
|
||||||
name: z.string().min(1, "Name is required").describe("Provider name"),
|
name: z.string().min(1, "Name is required").describe("Provider name"),
|
||||||
displayName: z.string().min(1, "Display name is required").describe("Provider display 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"),
|
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({
|
export const DiscoveryModeSchema = BaseAuthProviderSchema.extend({
|
||||||
issuerUrl: z.string().url("Invalid issuer URL").describe("Provider issuer URL for discovery"),
|
issuerUrl: z.string().url("Invalid issuer URL").describe("Provider issuer URL for discovery"),
|
||||||
authorizationEndpoint: z.literal("").optional(),
|
authorizationEndpoint: z.literal("").optional(),
|
||||||
@@ -22,7 +20,6 @@ export const DiscoveryModeSchema = BaseAuthProviderSchema.extend({
|
|||||||
userInfoEndpoint: z.literal("").optional(),
|
userInfoEndpoint: z.literal("").optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Schema para modo manual (todos os endpoints)
|
|
||||||
export const ManualEndpointsSchema = BaseAuthProviderSchema.extend({
|
export const ManualEndpointsSchema = BaseAuthProviderSchema.extend({
|
||||||
issuerUrl: z.string().optional(),
|
issuerUrl: z.string().optional(),
|
||||||
authorizationEndpoint: z
|
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"),
|
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({
|
export const CreateAuthProviderSchema = BaseAuthProviderSchema.extend({
|
||||||
issuerUrl: z.string().url("Invalid issuer URL").optional(),
|
issuerUrl: z.string().url("Invalid issuer URL").optional(),
|
||||||
authorizationEndpoint: z.string().optional(),
|
authorizationEndpoint: z.string().optional(),
|
||||||
@@ -48,8 +44,7 @@ export const CreateAuthProviderSchema = BaseAuthProviderSchema.extend({
|
|||||||
data.userInfoEndpoint?.trim()
|
data.userInfoEndpoint?.trim()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Deve ter pelo menos issuerUrl OU todos os endpoints customizados
|
if (hasIssuerUrl && !hasAnyCustomEndpoint) return true;
|
||||||
if (hasIssuerUrl && !hasAnyCustomEndpoint) return true; // Modo discovery
|
|
||||||
|
|
||||||
if (hasAnyCustomEndpoint) {
|
if (hasAnyCustomEndpoint) {
|
||||||
const hasAllCustomEndpoints = !!(
|
const hasAllCustomEndpoints = !!(
|
||||||
@@ -57,10 +52,10 @@ export const CreateAuthProviderSchema = BaseAuthProviderSchema.extend({
|
|||||||
data.tokenEndpoint?.trim() &&
|
data.tokenEndpoint?.trim() &&
|
||||||
data.userInfoEndpoint?.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:
|
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
|
export const UpdateAuthProviderSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).optional(),
|
name: z.string().min(1).optional(),
|
||||||
@@ -88,7 +82,6 @@ export const UpdateAuthProviderSchema = z
|
|||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
// Se não está alterando nenhum campo de configuração, permite
|
|
||||||
const hasIssuerUrl = !!data.issuerUrl;
|
const hasIssuerUrl = !!data.issuerUrl;
|
||||||
const hasAnyCustomEndpoint = !!(
|
const hasAnyCustomEndpoint = !!(
|
||||||
data.authorizationEndpoint?.trim() ||
|
data.authorizationEndpoint?.trim() ||
|
||||||
@@ -96,13 +89,10 @@ export const UpdateAuthProviderSchema = z
|
|||||||
data.userInfoEndpoint?.trim()
|
data.userInfoEndpoint?.trim()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Se não está alterando nenhum campo de configuração, permite
|
|
||||||
if (!hasIssuerUrl && !hasAnyCustomEndpoint) return true;
|
if (!hasIssuerUrl && !hasAnyCustomEndpoint) return true;
|
||||||
|
|
||||||
// Se está fornecendo apenas issuerUrl, permite (modo discovery)
|
|
||||||
if (hasIssuerUrl && !hasAnyCustomEndpoint) return true;
|
if (hasIssuerUrl && !hasAnyCustomEndpoint) return true;
|
||||||
|
|
||||||
// Se está fornecendo endpoints customizados, deve fornecer todos os 3
|
|
||||||
if (hasAnyCustomEndpoint) {
|
if (hasAnyCustomEndpoint) {
|
||||||
const hasAllCustomEndpoints = !!(
|
const hasAllCustomEndpoints = !!(
|
||||||
data.authorizationEndpoint?.trim() &&
|
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({
|
export const UpdateOfficialProviderSchema = z.object({
|
||||||
issuerUrl: z.string().url().optional(),
|
issuerUrl: z.string().url().optional(),
|
||||||
clientId: z.string().min(1).optional(),
|
clientId: z.string().min(1).optional(),
|
||||||
@@ -129,7 +118,6 @@ export const UpdateOfficialProviderSchema = z.object({
|
|||||||
adminEmailDomains: z.string().optional(),
|
adminEmailDomains: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Schema para reordenação
|
|
||||||
export const UpdateProvidersOrderSchema = z.object({
|
export const UpdateProvidersOrderSchema = z.object({
|
||||||
providers: z.array(
|
providers: z.array(
|
||||||
z.object({
|
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";
|
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
|
* Configuração técnica oficial do Discord
|
||||||
* OAuth2 com mapeamentos específicos do Discord
|
* OAuth2 com mapeamentos específicos do Discord
|
||||||
@@ -216,3 +293,38 @@ export {
|
|||||||
fronteggConfig,
|
fronteggConfig,
|
||||||
genericProviderTemplate,
|
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) {
|
export async function authProvidersRoutes(fastify: FastifyInstance) {
|
||||||
const authProvidersController = new AuthProvidersController();
|
const authProvidersController = new AuthProvidersController();
|
||||||
|
|
||||||
// Admin-only middleware
|
|
||||||
const adminPreValidation = async (request: any, reply: any) => {
|
const adminPreValidation = async (request: any, reply: any) => {
|
||||||
try {
|
try {
|
||||||
const usersCount = await prisma.user.count();
|
const usersCount = await prisma.user.count();
|
||||||
|
|
||||||
// Se há apenas 1 usuário ou menos, permite acesso (setup inicial)
|
|
||||||
if (usersCount <= 1) {
|
if (usersCount <= 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifica JWT
|
|
||||||
await request.jwtVerify();
|
await request.jwtVerify();
|
||||||
|
|
||||||
// Verifica se é admin
|
|
||||||
if (!request.user.isAdmin) {
|
if (!request.user.isAdmin) {
|
||||||
return reply.status(403).send({
|
return reply.status(403).send({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -36,7 +32,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get enabled providers for login page
|
|
||||||
fastify.get(
|
fastify.get(
|
||||||
"/providers",
|
"/providers",
|
||||||
{
|
{
|
||||||
@@ -69,7 +64,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
|
|||||||
authProvidersController.getProviders.bind(authProvidersController)
|
authProvidersController.getProviders.bind(authProvidersController)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get all providers (admin only)
|
|
||||||
fastify.get(
|
fastify.get(
|
||||||
"/providers/all",
|
"/providers/all",
|
||||||
{
|
{
|
||||||
@@ -102,7 +96,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
|
|||||||
authProvidersController.getAllProviders.bind(authProvidersController)
|
authProvidersController.getAllProviders.bind(authProvidersController)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create new provider (admin only)
|
|
||||||
fastify.post(
|
fastify.post(
|
||||||
"/providers",
|
"/providers",
|
||||||
{
|
{
|
||||||
@@ -141,7 +134,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
|
|||||||
authProvidersController.createProvider.bind(authProvidersController)
|
authProvidersController.createProvider.bind(authProvidersController)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update providers order (admin only) - MUST be before /providers/:id route
|
|
||||||
fastify.put(
|
fastify.put(
|
||||||
"/providers/order",
|
"/providers/order",
|
||||||
{
|
{
|
||||||
@@ -179,7 +171,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
|
|||||||
authProvidersController.updateProvidersOrder.bind(authProvidersController)
|
authProvidersController.updateProvidersOrder.bind(authProvidersController)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update provider configuration (admin only)
|
|
||||||
fastify.put(
|
fastify.put(
|
||||||
"/providers/:id",
|
"/providers/:id",
|
||||||
{
|
{
|
||||||
@@ -193,7 +184,7 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
body: z.any(), // Validação manual no controller para providers oficiais
|
body: z.any(),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
@@ -221,7 +212,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
|
|||||||
authProvidersController.updateProvider.bind(authProvidersController)
|
authProvidersController.updateProvider.bind(authProvidersController)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete provider (admin only)
|
|
||||||
fastify.delete(
|
fastify.delete(
|
||||||
"/providers/:id",
|
"/providers/:id",
|
||||||
{
|
{
|
||||||
@@ -257,7 +247,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
|
|||||||
authProvidersController.deleteProvider.bind(authProvidersController)
|
authProvidersController.deleteProvider.bind(authProvidersController)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initiate authentication with specific provider
|
|
||||||
fastify.get(
|
fastify.get(
|
||||||
"/providers/:provider/authorize",
|
"/providers/:provider/authorize",
|
||||||
{
|
{
|
||||||
@@ -289,7 +278,6 @@ export async function authProvidersRoutes(fastify: FastifyInstance) {
|
|||||||
authProvidersController.authorize.bind(authProvidersController)
|
authProvidersController.authorize.bind(authProvidersController)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle callback from provider
|
|
||||||
fastify.get(
|
fastify.get(
|
||||||
"/providers/:provider/callback",
|
"/providers/:provider/callback",
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -61,3 +61,61 @@ export interface AuthResult {
|
|||||||
userInfo: ProviderUserInfo;
|
userInfo: ProviderUserInfo;
|
||||||
tokens: TokenResponse;
|
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 url = new URL(request.url);
|
||||||
const queryString = url.search;
|
const queryString = url.search;
|
||||||
|
|
||||||
// Forward the original host and protocol to backend
|
|
||||||
const originalHost = request.headers.get("host") || url.host;
|
const originalHost = request.headers.get("host") || url.host;
|
||||||
const originalProtocol = request.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
|
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",
|
"Content-Type": "application/json",
|
||||||
"x-forwarded-host": originalHost,
|
"x-forwarded-host": originalHost,
|
||||||
"x-forwarded-proto": originalProtocol,
|
"x-forwarded-proto": originalProtocol,
|
||||||
// Forward any authorization headers if needed
|
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
Array.from(request.headers.entries()).filter(
|
Array.from(request.headers.entries()).filter(
|
||||||
([key]) => key.startsWith("authorization") || key.startsWith("cookie")
|
([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) {
|
if (apiRes.status >= 300 && apiRes.status < 400) {
|
||||||
const location = apiRes.headers.get("location");
|
const location = apiRes.headers.get("location");
|
||||||
if (location) {
|
if (location) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const queryString = url.search;
|
const queryString = url.search;
|
||||||
|
|
||||||
// Forward the original host and protocol to backend
|
|
||||||
const originalHost = request.headers.get("host") || url.host;
|
const originalHost = request.headers.get("host") || url.host;
|
||||||
const originalProtocol = request.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
|
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",
|
"Content-Type": "application/json",
|
||||||
"x-forwarded-host": originalHost,
|
"x-forwarded-host": originalHost,
|
||||||
"x-forwarded-proto": originalProtocol,
|
"x-forwarded-proto": originalProtocol,
|
||||||
// Forward any authorization headers if needed
|
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
Array.from(request.headers.entries()).filter(
|
Array.from(request.headers.entries()).filter(
|
||||||
([key]) => key.startsWith("authorization") || key.startsWith("cookie")
|
([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) {
|
if (apiRes.status >= 300 && apiRes.status < 400) {
|
||||||
const location = apiRes.headers.get("location");
|
const location = apiRes.headers.get("location");
|
||||||
if (location) {
|
if (location) {
|
||||||
// Create redirect response and forward any set-cookie headers
|
|
||||||
const response = NextResponse.redirect(location);
|
const response = NextResponse.redirect(location);
|
||||||
|
|
||||||
// Copy all set-cookie headers from backend response
|
|
||||||
const setCookieHeaders = apiRes.headers.getSetCookie?.() || [];
|
const setCookieHeaders = apiRes.headers.getSetCookie?.() || [];
|
||||||
if (setCookieHeaders.length > 0) {
|
if (setCookieHeaders.length > 0) {
|
||||||
setCookieHeaders.forEach((cookie) => {
|
setCookieHeaders.forEach((cookie) => {
|
||||||
response.headers.append("set-cookie", cookie);
|
response.headers.append("set-cookie", cookie);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Fallback for older implementations
|
|
||||||
const singleCookie = apiRes.headers.get("set-cookie");
|
const singleCookie = apiRes.headers.get("set-cookie");
|
||||||
if (singleCookie) {
|
if (singleCookie) {
|
||||||
response.headers.set("set-cookie", 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;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await apiRes.json();
|
data = await apiRes.json();
|
||||||
} catch {
|
} catch {
|
||||||
// If not JSON, it might be a redirect or other response
|
|
||||||
return new NextResponse(null, {
|
return new NextResponse(null, {
|
||||||
status: apiRes.status,
|
status: apiRes.status,
|
||||||
headers: Object.fromEntries(apiRes.headers.entries()),
|
headers: Object.fromEntries(apiRes.headers.entries()),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export async function GET(request: NextRequest) {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
// Forward any authorization headers
|
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
Array.from(request.headers.entries()).filter(
|
Array.from(request.headers.entries()).filter(
|
||||||
([key]) => key.startsWith("authorization") || key.startsWith("cookie")
|
([key]) => key.startsWith("authorization") || key.startsWith("cookie")
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
// Forward any authorization headers
|
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
Array.from(request.headers.entries()).filter(
|
Array.from(request.headers.entries()).filter(
|
||||||
([key]) => key.startsWith("authorization") || key.startsWith("cookie")
|
([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}`, {
|
const apiRes = await fetch(`${API_BASE_URL}/auth/providers/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
// Forward any authorization headers (but don't include Content-Type for DELETE)
|
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
Array.from(request.headers.entries()).filter(
|
Array.from(request.headers.entries()).filter(
|
||||||
([key]) => key.startsWith("authorization") || key.startsWith("cookie")
|
([key]) => key.startsWith("authorization") || key.startsWith("cookie")
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export async function PUT(request: NextRequest) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
cookie: request.headers.get("cookie") || "",
|
cookie: request.headers.get("cookie") || "",
|
||||||
// Forward any authorization headers if needed
|
|
||||||
...Object.fromEntries(Array.from(request.headers.entries()).filter(([key]) => key.startsWith("authorization"))),
|
...Object.fromEntries(Array.from(request.headers.entries()).filter(([key]) => key.startsWith("authorization"))),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export async function GET(request: NextRequest) {
|
|||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const queryString = url.search;
|
const queryString = url.search;
|
||||||
|
|
||||||
// Forward the original host and protocol to backend
|
|
||||||
const originalHost = request.headers.get("host") || url.host;
|
const originalHost = request.headers.get("host") || url.host;
|
||||||
const originalProtocol = request.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
|
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-host": originalHost,
|
||||||
"x-forwarded-proto": originalProtocol,
|
"x-forwarded-proto": originalProtocol,
|
||||||
cookie: request.headers.get("cookie") || "",
|
cookie: request.headers.get("cookie") || "",
|
||||||
// Forward any authorization headers if needed
|
|
||||||
...Object.fromEntries(Array.from(request.headers.entries()).filter(([key]) => key.startsWith("authorization"))),
|
...Object.fromEntries(Array.from(request.headers.entries()).filter(([key]) => key.startsWith("authorization"))),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -46,7 +44,6 @@ export async function POST(request: NextRequest) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
cookie: request.headers.get("cookie") || "",
|
cookie: request.headers.get("cookie") || "",
|
||||||
// Forward any authorization headers if needed
|
|
||||||
...Object.fromEntries(Array.from(request.headers.entries()).filter(([key]) => key.startsWith("authorization"))),
|
...Object.fromEntries(Array.from(request.headers.entries()).filter(([key]) => key.startsWith("authorization"))),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
||||||
import {
|
import {
|
||||||
IconAlertTriangle,
|
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
IconChevronUp,
|
IconChevronUp,
|
||||||
@@ -16,7 +15,6 @@ import {
|
|||||||
IconPlus,
|
IconPlus,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
IconX,
|
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { Globe } from "lucide-react";
|
import { Globe } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -62,7 +60,6 @@ interface NewProvider {
|
|||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
issuerUrl: string;
|
issuerUrl: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
// Endpoints customizados opcionais
|
|
||||||
authorizationEndpoint: string;
|
authorizationEndpoint: string;
|
||||||
tokenEndpoint: string;
|
tokenEndpoint: string;
|
||||||
userInfoEndpoint: string;
|
userInfoEndpoint: string;
|
||||||
@@ -96,13 +93,11 @@ export function AuthProvidersSettings() {
|
|||||||
userInfoEndpoint: "",
|
userInfoEndpoint: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-sugestão de scopes baseada na Provider URL
|
|
||||||
const detectProviderTypeAndSuggestScopes = (url: string): string[] => {
|
const detectProviderTypeAndSuggestScopes = (url: string): string[] => {
|
||||||
if (!url) return [];
|
if (!url) return [];
|
||||||
|
|
||||||
const urlLower = url.toLowerCase();
|
const urlLower = url.toLowerCase();
|
||||||
|
|
||||||
// Padrões conhecidos para detecção automática
|
|
||||||
const providerPatterns = [
|
const providerPatterns = [
|
||||||
{ pattern: "frontegg.com", scopes: ["openid", "profile", "email"] },
|
{ pattern: "frontegg.com", scopes: ["openid", "profile", "email"] },
|
||||||
{ pattern: "discord.com", scopes: ["identify", "email"] },
|
{ pattern: "discord.com", scopes: ["identify", "email"] },
|
||||||
@@ -147,14 +142,12 @@ export function AuthProvidersSettings() {
|
|||||||
{ pattern: "zitadel.com", scopes: ["openid", "profile", "email"] },
|
{ pattern: "zitadel.com", scopes: ["openid", "profile", "email"] },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Procura por padrões conhecidos
|
|
||||||
for (const { pattern, scopes } of providerPatterns) {
|
for (const { pattern, scopes } of providerPatterns) {
|
||||||
if (urlLower.includes(pattern)) {
|
if (urlLower.includes(pattern)) {
|
||||||
return scopes;
|
return scopes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback baseado no tipo do provider
|
|
||||||
if (newProvider.type === "oidc") {
|
if (newProvider.type === "oidc") {
|
||||||
return ["openid", "profile", "email"];
|
return ["openid", "profile", "email"];
|
||||||
} else {
|
} else {
|
||||||
@@ -162,7 +155,6 @@ export function AuthProvidersSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Função para auto-sugerir scopes baseado na Provider URL (onBlur)
|
|
||||||
const updateProviderUrl = (url: string) => {
|
const updateProviderUrl = (url: string) => {
|
||||||
if (!url.trim()) return;
|
if (!url.trim()) return;
|
||||||
|
|
||||||
@@ -178,7 +170,6 @@ export function AuthProvidersSettings() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load hide disabled providers state from localStorage
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedState = localStorage.getItem("hideDisabledProviders");
|
const savedState = localStorage.getItem("hideDisabledProviders");
|
||||||
if (savedState !== null) {
|
if (savedState !== null) {
|
||||||
@@ -186,7 +177,6 @@ export function AuthProvidersSettings() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Load providers
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProviders();
|
loadProviders();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -210,7 +200,6 @@ export function AuthProvidersSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update provider
|
|
||||||
const updateProvider = async (id: string, updates: Partial<AuthProvider>) => {
|
const updateProvider = async (id: string, updates: Partial<AuthProvider>) => {
|
||||||
try {
|
try {
|
||||||
setSaving(id);
|
setSaving(id);
|
||||||
@@ -236,7 +225,6 @@ export function AuthProvidersSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete provider
|
|
||||||
const deleteProvider = async (id: string, name: string) => {
|
const deleteProvider = async (id: string, name: string) => {
|
||||||
try {
|
try {
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
@@ -261,7 +249,6 @@ export function AuthProvidersSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Open delete confirmation modal
|
|
||||||
const openDeleteModal = (provider: AuthProvider) => {
|
const openDeleteModal = (provider: AuthProvider) => {
|
||||||
setProviderToDelete({
|
setProviderToDelete({
|
||||||
id: provider.id,
|
id: provider.id,
|
||||||
@@ -270,14 +257,12 @@ export function AuthProvidersSettings() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add new provider
|
|
||||||
const addProvider = async () => {
|
const addProvider = async () => {
|
||||||
if (!newProvider.name || !newProvider.displayName || !newProvider.clientId || !newProvider.clientSecret) {
|
if (!newProvider.name || !newProvider.displayName || !newProvider.clientId || !newProvider.clientSecret) {
|
||||||
toast.error("Please fill in all required fields (name, display name, client ID, client secret)");
|
toast.error("Please fill in all required fields (name, display name, client ID, client secret)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validação de configuração
|
|
||||||
const hasIssuerUrl = !!newProvider.issuerUrl;
|
const hasIssuerUrl = !!newProvider.issuerUrl;
|
||||||
const hasAllCustomEndpoints = !!(
|
const hasAllCustomEndpoints = !!(
|
||||||
newProvider.authorizationEndpoint &&
|
newProvider.authorizationEndpoint &&
|
||||||
@@ -311,7 +296,6 @@ export function AuthProvidersSettings() {
|
|||||||
autoRegister: true,
|
autoRegister: true,
|
||||||
scope: newProvider.scope || (newProvider.type === "oidc" ? "openid profile email" : "user:email"),
|
scope: newProvider.scope || (newProvider.type === "oidc" ? "openid profile email" : "user:email"),
|
||||||
sortOrder: providers.length + 1,
|
sortOrder: providers.length + 1,
|
||||||
// Incluir apenas campos relevantes baseado no modo
|
|
||||||
...(newProvider.issuerUrl ? { issuerUrl: newProvider.issuerUrl } : {}),
|
...(newProvider.issuerUrl ? { issuerUrl: newProvider.issuerUrl } : {}),
|
||||||
...(newProvider.authorizationEndpoint ? { authorizationEndpoint: newProvider.authorizationEndpoint } : {}),
|
...(newProvider.authorizationEndpoint ? { authorizationEndpoint: newProvider.authorizationEndpoint } : {}),
|
||||||
...(newProvider.tokenEndpoint ? { tokenEndpoint: newProvider.tokenEndpoint } : {}),
|
...(newProvider.tokenEndpoint ? { tokenEndpoint: newProvider.tokenEndpoint } : {}),
|
||||||
@@ -349,7 +333,6 @@ export function AuthProvidersSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Edit provider
|
|
||||||
const editProvider = async (providerData: Partial<AuthProvider>) => {
|
const editProvider = async (providerData: Partial<AuthProvider>) => {
|
||||||
if (!editingProvider || !providerData.name || !providerData.displayName) {
|
if (!editingProvider || !providerData.name || !providerData.displayName) {
|
||||||
toast.error("Please fill in all required fields");
|
toast.error("Please fill in all required fields");
|
||||||
@@ -384,7 +367,6 @@ export function AuthProvidersSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle drag and drop
|
|
||||||
const handleDragEnd = async (result: DropResult) => {
|
const handleDragEnd = async (result: DropResult) => {
|
||||||
if (!result.destination) return;
|
if (!result.destination) return;
|
||||||
|
|
||||||
@@ -392,14 +374,12 @@ export function AuthProvidersSettings() {
|
|||||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||||
items.splice(result.destination.index, 0, reorderedItem);
|
items.splice(result.destination.index, 0, reorderedItem);
|
||||||
|
|
||||||
// Update local state immediately for better UX
|
|
||||||
const updatedItems = items.map((provider, index) => ({
|
const updatedItems = items.map((provider, index) => ({
|
||||||
...provider,
|
...provider,
|
||||||
sortOrder: index + 1,
|
sortOrder: index + 1,
|
||||||
}));
|
}));
|
||||||
setProviders(updatedItems);
|
setProviders(updatedItems);
|
||||||
|
|
||||||
// Update sortOrder values for API
|
|
||||||
const updatedProviders = updatedItems.map((provider) => ({
|
const updatedProviders = updatedItems.map((provider) => ({
|
||||||
id: provider.id,
|
id: provider.id,
|
||||||
sortOrder: provider.sortOrder,
|
sortOrder: provider.sortOrder,
|
||||||
@@ -416,22 +396,18 @@ export function AuthProvidersSettings() {
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toast.success("Provider order updated");
|
toast.success("Provider order updated");
|
||||||
// No need to reload - state is already updated locally
|
|
||||||
} else {
|
} else {
|
||||||
toast.error("Failed to update provider order");
|
toast.error("Failed to update provider order");
|
||||||
// Revert local state on error
|
|
||||||
await loadProviders();
|
await loadProviders();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating provider order:", error);
|
console.error("Error updating provider order:", error);
|
||||||
toast.error("Failed to update provider order");
|
toast.error("Failed to update provider order");
|
||||||
// Revert local state on error
|
|
||||||
await loadProviders();
|
await loadProviders();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProviderIcon = (provider: AuthProvider) => {
|
const getProviderIcon = (provider: AuthProvider) => {
|
||||||
// Use the icon saved in the database, fallback to FaCog if not set
|
|
||||||
const iconName = provider.icon || "FaCog";
|
const iconName = provider.icon || "FaCog";
|
||||||
return renderIconByName(iconName, "w-5 h-5");
|
return renderIconByName(iconName, "w-5 h-5");
|
||||||
};
|
};
|
||||||
@@ -481,7 +457,6 @@ export function AuthProvidersSettings() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Add Provider Button */}
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{hideDisabledProviders
|
{hideDisabledProviders
|
||||||
@@ -503,7 +478,6 @@ export function AuthProvidersSettings() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Provider Form */}
|
|
||||||
{showAddForm && (
|
{showAddForm && (
|
||||||
<div className="border border-dashed rounded-lg p-4 space-y-4">
|
<div className="border border-dashed rounded-lg p-4 space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -571,7 +545,6 @@ export function AuthProvidersSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Configuration Method Toggle */}
|
|
||||||
<div className="space-y-4">
|
<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">
|
<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>
|
<h4 className="text-sm font-medium mb-3">Configuration Method</h4>
|
||||||
@@ -640,7 +613,6 @@ export function AuthProvidersSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Automatic Discovery Mode */}
|
|
||||||
{!newProvider.authorizationEndpoint &&
|
{!newProvider.authorizationEndpoint &&
|
||||||
!newProvider.tokenEndpoint &&
|
!newProvider.tokenEndpoint &&
|
||||||
!newProvider.userInfoEndpoint && (
|
!newProvider.userInfoEndpoint && (
|
||||||
@@ -658,7 +630,6 @@ export function AuthProvidersSettings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Manual Endpoints Mode */}
|
|
||||||
{(newProvider.authorizationEndpoint || newProvider.tokenEndpoint || newProvider.userInfoEndpoint) && (
|
{(newProvider.authorizationEndpoint || newProvider.tokenEndpoint || newProvider.userInfoEndpoint) && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -714,7 +685,6 @@ export function AuthProvidersSettings() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Client Credentials */}
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label className="mb-2 block">Client ID *</Label>
|
<Label className="mb-2 block">Client ID *</Label>
|
||||||
@@ -735,7 +705,6 @@ export function AuthProvidersSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* OAuth Scopes */}
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="mb-2 block">OAuth Scopes</Label>
|
<Label className="mb-2 block">OAuth Scopes</Label>
|
||||||
<TagsInput
|
<TagsInput
|
||||||
@@ -750,7 +719,6 @@ export function AuthProvidersSettings() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Show callback URL if provider name is filled */}
|
|
||||||
{newProvider.name && (
|
{newProvider.name && (
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<CallbackUrlDisplay providerName={newProvider.name} />
|
<CallbackUrlDisplay providerName={newProvider.name} />
|
||||||
@@ -767,7 +735,6 @@ export function AuthProvidersSettings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hide Disabled Providers Checkbox */}
|
|
||||||
{providers.length > 0 && (
|
{providers.length > 0 && (
|
||||||
<div className="flex items-center space-x-2 py-2">
|
<div className="flex items-center space-x-2 py-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -781,7 +748,6 @@ export function AuthProvidersSettings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Providers List - Compact with Drag and Drop */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
@@ -892,7 +858,6 @@ export function AuthProvidersSettings() {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
|
||||||
<AuthProviderDeleteModal
|
<AuthProviderDeleteModal
|
||||||
isOpen={!!providerToDelete}
|
isOpen={!!providerToDelete}
|
||||||
onClose={() => setProviderToDelete(null)}
|
onClose={() => setProviderToDelete(null)}
|
||||||
@@ -908,7 +873,6 @@ export function AuthProvidersSettings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compact provider row component
|
|
||||||
interface ProviderRowProps {
|
interface ProviderRowProps {
|
||||||
provider: AuthProvider;
|
provider: AuthProvider;
|
||||||
onUpdate: (updates: Partial<AuthProvider>) => void;
|
onUpdate: (updates: Partial<AuthProvider>) => void;
|
||||||
@@ -946,10 +910,8 @@ function ProviderRow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`border rounded-lg ${isDragging ? "border-blue-300 bg-blue-50 dark:bg-blue-950/20" : ""}`}>
|
<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 justify-between p-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* Drag Handle */}
|
|
||||||
{!isDragDisabled ? (
|
{!isDragDisabled ? (
|
||||||
<div
|
<div
|
||||||
{...dragHandleProps}
|
{...dragHandleProps}
|
||||||
@@ -963,7 +925,6 @@ function ProviderRow({
|
|||||||
<span className="text-lg">{getIcon(provider)}</span>
|
<span className="text-lg">{getIcon(provider)}</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Status dot */}
|
|
||||||
<div
|
<div
|
||||||
className={`w-2 h-2 rounded-full ${provider.enabled ? "bg-green-500" : "bg-gray-400"}`}
|
className={`w-2 h-2 rounded-full ${provider.enabled ? "bg-green-500" : "bg-gray-400"}`}
|
||||||
title={provider.enabled ? "Enabled" : "Disabled"}
|
title={provider.enabled ? "Enabled" : "Disabled"}
|
||||||
@@ -996,7 +957,6 @@ function ProviderRow({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Edit Form - Shows right below this provider when editing */}
|
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<div className="border-t border-border dark:border-border p-4 space-y-4 bg-muted/50 dark:bg-muted/20">
|
<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">
|
<div className="flex items-center justify-between">
|
||||||
@@ -1019,7 +979,6 @@ function ProviderRow({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit Provider Form Component
|
|
||||||
interface EditProviderFormProps {
|
interface EditProviderFormProps {
|
||||||
provider: AuthProvider;
|
provider: AuthProvider;
|
||||||
onSave: (data: Partial<AuthProvider>) => void;
|
onSave: (data: Partial<AuthProvider>) => void;
|
||||||
@@ -1037,7 +996,6 @@ function EditProviderForm({
|
|||||||
editingFormData,
|
editingFormData,
|
||||||
setEditingFormData,
|
setEditingFormData,
|
||||||
}: EditProviderFormProps) {
|
}: EditProviderFormProps) {
|
||||||
// Usar dados preservados se existirem, senão usar dados do provider
|
|
||||||
const savedData = editingFormData[provider.id] || {};
|
const savedData = editingFormData[provider.id] || {};
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: savedData.name || provider.name || "",
|
name: savedData.name || provider.name || "",
|
||||||
@@ -1058,13 +1016,11 @@ function EditProviderForm({
|
|||||||
const [showClientSecret, setShowClientSecret] = useState(false);
|
const [showClientSecret, setShowClientSecret] = useState(false);
|
||||||
const isOfficial = provider.isOfficial;
|
const isOfficial = provider.isOfficial;
|
||||||
|
|
||||||
// Auto-sugestão de scopes para formulário de edição
|
|
||||||
const detectProviderTypeAndSuggestScopesEdit = (url: string, currentType: string): string[] => {
|
const detectProviderTypeAndSuggestScopesEdit = (url: string, currentType: string): string[] => {
|
||||||
if (!url) return [];
|
if (!url) return [];
|
||||||
|
|
||||||
const urlLower = url.toLowerCase();
|
const urlLower = url.toLowerCase();
|
||||||
|
|
||||||
// Mesmos padrões do formulário de adição
|
|
||||||
const providerPatterns = [
|
const providerPatterns = [
|
||||||
{ pattern: "frontegg.com", scopes: ["openid", "profile", "email"] },
|
{ pattern: "frontegg.com", scopes: ["openid", "profile", "email"] },
|
||||||
{ pattern: "discord.com", scopes: ["identify", "email"] },
|
{ pattern: "discord.com", scopes: ["identify", "email"] },
|
||||||
@@ -1081,14 +1037,12 @@ function EditProviderForm({
|
|||||||
{ pattern: "zitadel.com", scopes: ["openid", "profile", "email"] },
|
{ pattern: "zitadel.com", scopes: ["openid", "profile", "email"] },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Procura por padrões conhecidos
|
|
||||||
for (const { pattern, scopes } of providerPatterns) {
|
for (const { pattern, scopes } of providerPatterns) {
|
||||||
if (urlLower.includes(pattern)) {
|
if (urlLower.includes(pattern)) {
|
||||||
return scopes;
|
return scopes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback baseado no tipo do provider
|
|
||||||
if (currentType === "oidc") {
|
if (currentType === "oidc") {
|
||||||
return ["openid", "profile", "email"];
|
return ["openid", "profile", "email"];
|
||||||
} else {
|
} 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) => {
|
const updateProviderUrlEdit = (url: string) => {
|
||||||
if (!url.trim()) return;
|
if (!url.trim()) return;
|
||||||
|
|
||||||
if (isOfficial) {
|
if (isOfficial) {
|
||||||
// Para providers oficiais, não faz auto-sugestão de scopes
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1109,7 +1061,6 @@ function EditProviderForm({
|
|||||||
const shouldUpdateScopes =
|
const shouldUpdateScopes =
|
||||||
!formData.scope || formData.scope === "openid profile email" || formData.scope === "profile email";
|
!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) {
|
if (shouldUpdateScopes) {
|
||||||
updateFormData({
|
updateFormData({
|
||||||
scope: suggestedScopes.join(" "),
|
scope: suggestedScopes.join(" "),
|
||||||
@@ -1146,10 +1097,8 @@ function EditProviderForm({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Callback URL Display */}
|
|
||||||
<CallbackUrlDisplay providerName={formData.name || "provider"} />
|
<CallbackUrlDisplay providerName={formData.name || "provider"} />
|
||||||
|
|
||||||
{/* Only show basic fields for non-official providers */}
|
|
||||||
{!isOfficial && (
|
{!isOfficial && (
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -1195,7 +1144,6 @@ function EditProviderForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Configuration - Only for custom providers */}
|
|
||||||
{!isOfficial && (
|
{!isOfficial && (
|
||||||
<div className="space-y-4">
|
<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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Automatic Discovery Mode */}
|
|
||||||
{!formData.authorizationEndpoint && !formData.tokenEndpoint && !formData.userInfoEndpoint && (
|
{!formData.authorizationEndpoint && !formData.tokenEndpoint && !formData.userInfoEndpoint && (
|
||||||
<div>
|
<div>
|
||||||
<Label className="mb-2 block">Provider URL *</Label>
|
<Label className="mb-2 block">Provider URL *</Label>
|
||||||
@@ -1264,7 +1211,6 @@ function EditProviderForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Manual Endpoints Mode */}
|
|
||||||
{(formData.authorizationEndpoint || formData.tokenEndpoint || formData.userInfoEndpoint) && (
|
{(formData.authorizationEndpoint || formData.tokenEndpoint || formData.userInfoEndpoint) && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -1319,7 +1265,6 @@ function EditProviderForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Official Provider - Only Provider URL and Icon */}
|
|
||||||
{isOfficial && (
|
{isOfficial && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -1428,7 +1373,6 @@ function EditProviderForm({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback URL Display Component
|
|
||||||
interface CallbackUrlDisplayProps {
|
interface CallbackUrlDisplayProps {
|
||||||
providerName: string;
|
providerName: string;
|
||||||
}
|
}
|
||||||
@@ -1436,7 +1380,6 @@ interface CallbackUrlDisplayProps {
|
|||||||
function CallbackUrlDisplay({ providerName }: CallbackUrlDisplayProps) {
|
function CallbackUrlDisplay({ providerName }: CallbackUrlDisplayProps) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
// Usar a URL atual da página
|
|
||||||
const callbackUrl =
|
const callbackUrl =
|
||||||
typeof window !== "undefined"
|
typeof window !== "undefined"
|
||||||
? `${window.location.origin}/api/auth/providers/${providerName}/callback`
|
? `${window.location.origin}/api/auth/providers/${providerName}/callback`
|
||||||
|
|||||||
@@ -24,12 +24,10 @@ export function SettingsForm({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{sortedGroups.map(([group, configs]) => {
|
{sortedGroups.map(([group, configs]) => {
|
||||||
// Render custom auth providers component
|
|
||||||
if (group === "auth-providers") {
|
if (group === "auth-providers") {
|
||||||
return <AuthProvidersSettings key={group} />;
|
return <AuthProvidersSettings key={group} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only render SettingsGroup if we have a form for this group
|
|
||||||
const form = groupForms[group as ValidGroup];
|
const form = groupForms[group as ValidGroup];
|
||||||
if (!form) {
|
if (!form) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user