mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
feat: add Google and Discord authentication providers with configuration updates
- Introduced Google and Discord as new authentication providers in the seed script, including their OAuth2 configurations and metadata. - Updated existing provider configurations to adjust sort orders and ensure proper integration with the new providers. - Enhanced validation logic in the DTOs to accommodate optional endpoint fields and ensure correct provider setup. - Implemented a delete confirmation modal in the web settings for managing authentication providers, improving user experience. - Added logging for better debugging and tracking of provider-related operations in the controller and service layers.
This commit is contained in:
@@ -151,6 +151,44 @@ const defaultConfigs = [
|
||||
];
|
||||
|
||||
const defaultAuthProviders = [
|
||||
{
|
||||
name: "google",
|
||||
displayName: "Google",
|
||||
type: "oauth2",
|
||||
icon: "FcGoogle",
|
||||
enabled: false,
|
||||
issuerUrl: "https://accounts.google.com",
|
||||
authorizationEndpoint: "/o/oauth2/v2/auth",
|
||||
tokenEndpoint: "/o/oauth2/token",
|
||||
userInfoEndpoint: "https://www.googleapis.com/oauth2/v3/userinfo",
|
||||
scope: "openid profile email",
|
||||
sortOrder: 1,
|
||||
metadata: JSON.stringify({
|
||||
description: "Sign in with your Google account",
|
||||
docs: "https://developers.google.com/identity/protocols/oauth2",
|
||||
supportsDiscovery: true,
|
||||
authMethod: "body"
|
||||
})
|
||||
},
|
||||
{
|
||||
name: "discord",
|
||||
displayName: "Discord",
|
||||
type: "oauth2",
|
||||
icon: "FaDiscord",
|
||||
enabled: false,
|
||||
issuerUrl: "https://discord.com",
|
||||
authorizationEndpoint: "/oauth2/authorize",
|
||||
tokenEndpoint: "/api/oauth2/token",
|
||||
userInfoEndpoint: "/api/users/@me",
|
||||
scope: "identify email",
|
||||
sortOrder: 2,
|
||||
metadata: JSON.stringify({
|
||||
description: "Sign in with your Discord account",
|
||||
docs: "https://discord.com/developers/docs/topics/oauth2",
|
||||
supportsDiscovery: false,
|
||||
authMethod: "body"
|
||||
})
|
||||
},
|
||||
{
|
||||
name: "github",
|
||||
displayName: "GitHub",
|
||||
@@ -162,7 +200,7 @@ const defaultAuthProviders = [
|
||||
tokenEndpoint: "/access_token",
|
||||
userInfoEndpoint: "https://api.github.com/user", // GitHub usa URL absoluta para userInfo
|
||||
scope: "user:email",
|
||||
sortOrder: 1,
|
||||
sortOrder: 3,
|
||||
metadata: JSON.stringify({
|
||||
description: "Sign in with your GitHub account",
|
||||
docs: "https://docs.github.com/en/developers/apps/building-oauth-apps",
|
||||
@@ -180,7 +218,7 @@ const defaultAuthProviders = [
|
||||
tokenEndpoint: "/oauth/token",
|
||||
userInfoEndpoint: "/userinfo",
|
||||
scope: "openid profile email",
|
||||
sortOrder: 2,
|
||||
sortOrder: 4,
|
||||
metadata: JSON.stringify({
|
||||
description: "Sign in with Auth0 - Replace 'your-tenant' with your Auth0 domain",
|
||||
docs: "https://auth0.com/docs/get-started/authentication-and-authorization-flow",
|
||||
@@ -198,7 +236,7 @@ const defaultAuthProviders = [
|
||||
tokenEndpoint: "/oauth2/token",
|
||||
userInfoEndpoint: "/oauth2/user_profile",
|
||||
scope: "openid profile email",
|
||||
sortOrder: 3,
|
||||
sortOrder: 5,
|
||||
metadata: JSON.stringify({
|
||||
description: "Sign in with Kinde - Replace 'your-tenant' with your Kinde domain",
|
||||
docs: "https://kinde.com/docs/developer-tools/about/",
|
||||
@@ -216,7 +254,7 @@ const defaultAuthProviders = [
|
||||
tokenEndpoint: "/oauth/v2/token",
|
||||
userInfoEndpoint: "/oidc/v1/userinfo",
|
||||
scope: "openid profile email",
|
||||
sortOrder: 4,
|
||||
sortOrder: 6,
|
||||
metadata: JSON.stringify({
|
||||
description: "Sign in with Zitadel - Replace with your Zitadel instance URL",
|
||||
docs: "https://zitadel.com/docs/guides/integrate/login/oidc",
|
||||
@@ -235,7 +273,7 @@ const defaultAuthProviders = [
|
||||
tokenEndpoint: "/application/o/token/",
|
||||
userInfoEndpoint: "/application/o/userinfo/",
|
||||
scope: "openid profile email",
|
||||
sortOrder: 5,
|
||||
sortOrder: 7,
|
||||
metadata: JSON.stringify({
|
||||
description: "Sign in with Authentik - Replace with your Authentik instance URL",
|
||||
docs: "https://goauthentik.io/docs/providers/oauth2",
|
||||
@@ -253,7 +291,7 @@ const defaultAuthProviders = [
|
||||
tokenEndpoint: "/oauth/token",
|
||||
userInfoEndpoint: "/identity/resources/users/v2/me",
|
||||
scope: "openid profile email",
|
||||
sortOrder: 6,
|
||||
sortOrder: 8,
|
||||
metadata: JSON.stringify({
|
||||
description: "Sign in with Frontegg - Replace 'your-tenant' with your Frontegg tenant",
|
||||
docs: "https://docs.frontegg.com",
|
||||
|
@@ -166,8 +166,12 @@ export class AuthProvidersController {
|
||||
|
||||
// Para providers customizados, aplica validação normal
|
||||
try {
|
||||
console.log(`[Controller] Updating custom provider with data:`, data);
|
||||
|
||||
// Valida usando o schema do Zod
|
||||
const validatedData = UpdateAuthProviderSchema.parse(data);
|
||||
console.log(`[Controller] Validation passed, validated data:`, validatedData);
|
||||
|
||||
const provider = await this.authProvidersService.updateProvider(id, validatedData);
|
||||
|
||||
return reply.send({
|
||||
@@ -176,6 +180,7 @@ export class AuthProvidersController {
|
||||
});
|
||||
} catch (validationError) {
|
||||
console.error("Validation error for custom provider:", validationError);
|
||||
console.error("Raw data that failed validation:", data);
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: "Invalid data provided",
|
||||
@@ -297,26 +302,53 @@ export class AuthProvidersController {
|
||||
}
|
||||
|
||||
async callback(request: FastifyRequest<{ Params: { provider: string }; Querystring: any }>, reply: FastifyReply) {
|
||||
console.log(`[Controller] Callback called for provider: ${request.params.provider}`);
|
||||
console.log(`[Controller] Query params:`, request.query);
|
||||
console.log(`[Controller] Headers:`, {
|
||||
host: request.headers.host,
|
||||
"x-forwarded-proto": request.headers["x-forwarded-proto"],
|
||||
"x-forwarded-host": request.headers["x-forwarded-host"],
|
||||
});
|
||||
|
||||
try {
|
||||
const { provider: providerName } = request.params;
|
||||
const query = request.query as any;
|
||||
const { code, state, error } = query;
|
||||
|
||||
console.log(`[Controller] Extracted params:`, { providerName, code, state, error });
|
||||
console.log(`[Controller] All query params:`, query);
|
||||
|
||||
const requestContext = {
|
||||
protocol: (request.headers["x-forwarded-proto"] as string) || request.protocol,
|
||||
host: (request.headers["x-forwarded-host"] as string) || (request.headers.host as string),
|
||||
};
|
||||
const baseUrl = `${requestContext.protocol}://${requestContext.host}`;
|
||||
|
||||
console.log(`[Controller] Request context:`, requestContext);
|
||||
console.log(`[Controller] Base URL:`, baseUrl);
|
||||
|
||||
if (error) {
|
||||
console.error(`OAuth error from ${providerName}:`, error);
|
||||
return reply.redirect(`${baseUrl}/login?error=oauth_error&provider=${providerName}`);
|
||||
}
|
||||
|
||||
if (!code || !state) {
|
||||
if (!code) {
|
||||
console.error(`Missing code parameter for ${providerName}`);
|
||||
return reply.redirect(`${baseUrl}/login?error=missing_code&provider=${providerName}`);
|
||||
}
|
||||
|
||||
// Validação de parâmetros obrigatórios
|
||||
const requiredParams = { code: !!code, state: !!state };
|
||||
const missingParams = Object.entries(requiredParams)
|
||||
.filter(([, hasValue]) => !hasValue)
|
||||
.map(([param]) => param);
|
||||
|
||||
if (missingParams.length > 0) {
|
||||
console.error(`Missing parameters for ${providerName}:`, missingParams);
|
||||
return reply.redirect(`${baseUrl}/login?error=missing_parameters&provider=${providerName}`);
|
||||
}
|
||||
|
||||
console.log(`[Controller] Calling handleCallback for ${providerName}`);
|
||||
const result = await this.authProvidersService.handleCallback(providerName, code, state, requestContext);
|
||||
|
||||
const jwt = await request.jwtSign({
|
||||
@@ -334,6 +366,7 @@ export class AuthProvidersController {
|
||||
|
||||
const redirectUrl = result.redirectUrl || "/dashboard";
|
||||
const fullRedirectUrl = redirectUrl.startsWith("http") ? redirectUrl : `${baseUrl}${redirectUrl}`;
|
||||
console.log(`[Controller] Redirecting to:`, fullRedirectUrl);
|
||||
return reply.redirect(fullRedirectUrl);
|
||||
} catch (error) {
|
||||
console.error(`Error in ${request.params.provider} callback:`, error);
|
||||
|
@@ -36,21 +36,35 @@ export const ManualEndpointsSchema = BaseAuthProviderSchema.extend({
|
||||
// Schema principal que aceita ambos os modos
|
||||
export const CreateAuthProviderSchema = BaseAuthProviderSchema.extend({
|
||||
issuerUrl: z.string().url("Invalid issuer URL").optional(),
|
||||
authorizationEndpoint: z.string().min(1).optional(),
|
||||
tokenEndpoint: z.string().min(1).optional(),
|
||||
userInfoEndpoint: z.string().min(1).optional(),
|
||||
authorizationEndpoint: z.string().optional(),
|
||||
tokenEndpoint: z.string().optional(),
|
||||
userInfoEndpoint: z.string().optional(),
|
||||
}).refine(
|
||||
(data) => {
|
||||
// Modo discovery: deve ter issuerUrl e não ter endpoints customizados
|
||||
const hasIssuerUrl = !!data.issuerUrl;
|
||||
const hasCustomEndpoints = !!(data.authorizationEndpoint && data.tokenEndpoint && data.userInfoEndpoint);
|
||||
const hasAnyCustomEndpoint = !!(
|
||||
data.authorizationEndpoint?.trim() ||
|
||||
data.tokenEndpoint?.trim() ||
|
||||
data.userInfoEndpoint?.trim()
|
||||
);
|
||||
|
||||
// Deve ter ou issuerUrl OU todos os endpoints customizados
|
||||
return hasIssuerUrl || hasCustomEndpoints;
|
||||
// Deve ter pelo menos issuerUrl OU todos os endpoints customizados
|
||||
if (hasIssuerUrl && !hasAnyCustomEndpoint) return true; // Modo discovery
|
||||
|
||||
if (hasAnyCustomEndpoint) {
|
||||
const hasAllCustomEndpoints = !!(
|
||||
data.authorizationEndpoint?.trim() &&
|
||||
data.tokenEndpoint?.trim() &&
|
||||
data.userInfoEndpoint?.trim()
|
||||
);
|
||||
return hasAllCustomEndpoints; // Precisa ter todos os 3 endpoints
|
||||
}
|
||||
|
||||
return false; // Precisa ter pelo menos um dos dois modos
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Either provide issuerUrl for automatic discovery OR all three custom endpoints (authorization, token, userInfo)",
|
||||
"Either provide issuerUrl for automatic discovery OR all three custom endpoints (authorization, token, userInfo).",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -68,25 +82,40 @@ export const UpdateAuthProviderSchema = z
|
||||
clientId: z.string().min(1).optional(),
|
||||
clientSecret: z.string().min(1).optional(),
|
||||
issuerUrl: z.string().url().optional(),
|
||||
authorizationEndpoint: z.string().min(1).optional(),
|
||||
tokenEndpoint: z.string().min(1).optional(),
|
||||
userInfoEndpoint: z.string().min(1).optional(),
|
||||
authorizationEndpoint: z.string().optional(),
|
||||
tokenEndpoint: z.string().optional(),
|
||||
userInfoEndpoint: z.string().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// Se está atualizando endpoints, deve seguir a mesma regra
|
||||
// Se não está alterando nenhum campo de configuração, permite
|
||||
const hasIssuerUrl = !!data.issuerUrl;
|
||||
const hasCustomEndpoints = !!(data.authorizationEndpoint || data.tokenEndpoint || data.userInfoEndpoint);
|
||||
const hasAnyCustomEndpoint = !!(
|
||||
data.authorizationEndpoint?.trim() ||
|
||||
data.tokenEndpoint?.trim() ||
|
||||
data.userInfoEndpoint?.trim()
|
||||
);
|
||||
|
||||
// Se nenhum dos dois foi fornecido, está OK (não está alterando o modo)
|
||||
if (!hasIssuerUrl && !hasCustomEndpoints) return true;
|
||||
// Se não está alterando nenhum campo de configuração, permite
|
||||
if (!hasIssuerUrl && !hasAnyCustomEndpoint) return true;
|
||||
|
||||
// Se forneceu um, não pode fornecer o outro
|
||||
return hasIssuerUrl !== hasCustomEndpoints;
|
||||
// Se está fornecendo apenas issuerUrl, permite (modo discovery)
|
||||
if (hasIssuerUrl && !hasAnyCustomEndpoint) return true;
|
||||
|
||||
// Se está fornecendo endpoints customizados, deve fornecer todos os 3
|
||||
if (hasAnyCustomEndpoint) {
|
||||
const hasAllCustomEndpoints = !!(
|
||||
data.authorizationEndpoint?.trim() &&
|
||||
data.tokenEndpoint?.trim() &&
|
||||
data.userInfoEndpoint?.trim()
|
||||
);
|
||||
return hasAllCustomEndpoints;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Either provide issuerUrl for automatic discovery OR all three custom endpoints (authorization, token, userInfo)",
|
||||
message: "When providing custom endpoints, all three endpoints (authorization, token, userInfo) are required.",
|
||||
}
|
||||
);
|
||||
|
||||
|
@@ -461,6 +461,8 @@ export class ProviderManager {
|
||||
ping: ["openid", "profile", "email"],
|
||||
azure: ["openid", "profile", "email", "User.Read"],
|
||||
aws: ["openid", "profile", "email"],
|
||||
kinde: ["openid", "profile", "email"],
|
||||
zitadel: ["openid", "profile", "email"],
|
||||
|
||||
// Communication
|
||||
slack: ["identity.basic", "identity.email", "identity.avatar"],
|
||||
@@ -559,15 +561,8 @@ export class ProviderManager {
|
||||
{ pattern: "monday.com", type: "monday" },
|
||||
{ pattern: "clickup.com", type: "clickup" },
|
||||
{ pattern: "linear.app", type: "linear" },
|
||||
{ pattern: "jira", type: "jira" },
|
||||
{ pattern: "confluence", type: "confluence" },
|
||||
{ pattern: "bamboo", type: "bamboo" },
|
||||
{ pattern: "bitbucket", type: "bitbucket" },
|
||||
{ pattern: "crowd", type: "crowd" },
|
||||
{ pattern: "fisheye", type: "fisheye" },
|
||||
{ pattern: "crucible", type: "crucible" },
|
||||
{ pattern: "statuspage", type: "statuspage" },
|
||||
{ pattern: "opsgenie", type: "opsgenie" },
|
||||
{ pattern: "kinde.com", type: "kinde" },
|
||||
{ pattern: "zitadel.com", type: "zitadel" },
|
||||
{ pattern: "jira", type: "jira" },
|
||||
{ pattern: "confluence", type: "confluence" },
|
||||
{ pattern: "bamboo", type: "bamboo" },
|
||||
|
@@ -1,5 +1,42 @@
|
||||
import { ProviderConfig, ProvidersConfigFile } from "./types";
|
||||
|
||||
/**
|
||||
* Configuração técnica oficial do Discord
|
||||
* OAuth2 com mapeamentos específicos do Discord
|
||||
* Endpoints vêm do banco de dados
|
||||
*/
|
||||
const discordConfig: ProviderConfig = {
|
||||
supportsDiscovery: false,
|
||||
authMethod: "body",
|
||||
fieldMappings: {
|
||||
id: ["id"],
|
||||
email: ["email"],
|
||||
name: ["global_name", "username"],
|
||||
firstName: ["global_name"],
|
||||
lastName: [],
|
||||
avatar: ["avatar"],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuração técnica oficial do Google
|
||||
* OAuth2 com discovery automático
|
||||
* Endpoints vêm do banco de dados
|
||||
*/
|
||||
const googleConfig: ProviderConfig = {
|
||||
supportsDiscovery: true,
|
||||
discoveryEndpoint: "/.well-known/openid_configuration",
|
||||
authMethod: "body",
|
||||
fieldMappings: {
|
||||
id: ["sub"],
|
||||
email: ["email"],
|
||||
name: ["name"],
|
||||
firstName: ["given_name"],
|
||||
lastName: ["family_name"],
|
||||
avatar: ["picture"],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuração técnica oficial do GitHub
|
||||
* OAuth2 com busca separada de email
|
||||
@@ -153,6 +190,8 @@ const genericProviderTemplate: ProviderConfig = {
|
||||
*/
|
||||
export const providersConfig: ProvidersConfigFile = {
|
||||
officialProviders: {
|
||||
google: googleConfig,
|
||||
discord: discordConfig,
|
||||
github: githubConfig,
|
||||
auth0: auth0Config,
|
||||
kinde: kindeConfig,
|
||||
@@ -167,6 +206,8 @@ export const providersConfig: ProvidersConfigFile = {
|
||||
* Exportações individuais para facilitar importação
|
||||
*/
|
||||
export {
|
||||
discordConfig,
|
||||
googleConfig,
|
||||
githubConfig,
|
||||
auth0Config,
|
||||
kindeConfig,
|
||||
|
@@ -193,13 +193,16 @@ export class AuthProvidersService {
|
||||
|
||||
async handleCallback(providerName: string, code: string, state: string, requestContext?: any) {
|
||||
console.log(`[AuthProvidersService] Handling callback for provider: ${providerName}`);
|
||||
console.log(`[AuthProvidersService] State received: ${state}`);
|
||||
|
||||
const pendingState = this.pendingStates.get(state);
|
||||
|
||||
if (!pendingState) {
|
||||
console.error(`[AuthProvidersService] No valid pending state found for ${providerName}`);
|
||||
throw new Error("Invalid or expired state");
|
||||
}
|
||||
|
||||
this.pendingStates.delete(state);
|
||||
console.log(`[AuthProvidersService] Using pending state for ${providerName}`);
|
||||
|
||||
const provider = await this.getProviderByName(providerName);
|
||||
if (!provider) {
|
||||
@@ -285,31 +288,33 @@ export class AuthProvidersService {
|
||||
|
||||
const callbackUrl = provider.redirectUri || `${baseUrl}/api/auth/providers/${provider.name}/callback`;
|
||||
|
||||
// Constrói body para token exchange
|
||||
const body = new URLSearchParams();
|
||||
body.append("client_id", provider.clientId);
|
||||
body.append("code", code);
|
||||
body.append("redirect_uri", callbackUrl);
|
||||
body.append("grant_type", "authorization_code");
|
||||
|
||||
// Adiciona client_secret se necessário
|
||||
if (authMethod === "body" && provider.clientSecret) {
|
||||
body.append("client_secret", provider.clientSecret);
|
||||
}
|
||||
|
||||
// Adiciona code_verifier se disponível (PKCE)
|
||||
if (codeVerifier) {
|
||||
body.append("code_verifier", codeVerifier);
|
||||
}
|
||||
|
||||
// Prepara headers
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
};
|
||||
|
||||
// Prepara body
|
||||
const body = new URLSearchParams({
|
||||
client_id: provider.clientId,
|
||||
code,
|
||||
redirect_uri: callbackUrl,
|
||||
grant_type: "authorization_code",
|
||||
});
|
||||
|
||||
// Adiciona code_verifier se for OIDC
|
||||
if (provider.type === "oidc" && codeVerifier) {
|
||||
body.append("code_verifier", codeVerifier);
|
||||
}
|
||||
|
||||
// Configura autenticação baseada no método
|
||||
if (authMethod === "basic") {
|
||||
// Configura autenticação basic se necessário
|
||||
if (authMethod === "basic" && provider.clientSecret) {
|
||||
const auth = Buffer.from(`${provider.clientId}:${provider.clientSecret}`).toString("base64");
|
||||
headers["Authorization"] = `Basic ${auth}`;
|
||||
} else {
|
||||
body.append("client_secret", provider.clientSecret);
|
||||
}
|
||||
|
||||
console.log(`[AuthProvidersService] Token exchange request:`, {
|
||||
@@ -335,6 +340,7 @@ export class AuthProvidersService {
|
||||
statusText: tokenResponse.statusText,
|
||||
error: errorText,
|
||||
});
|
||||
|
||||
throw new Error(`Token exchange failed: ${tokenResponse.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
@@ -368,7 +374,6 @@ export class AuthProvidersService {
|
||||
status: userInfoResponse.status,
|
||||
statusText: userInfoResponse.statusText,
|
||||
url: endpoints.userInfoEndpoint,
|
||||
headers: Object.fromEntries(userInfoResponse.headers.entries()),
|
||||
error: errorText.substring(0, 500), // Limita o tamanho do log
|
||||
});
|
||||
throw new Error(`UserInfo request failed: ${userInfoResponse.status} - ${errorText.substring(0, 200)}`);
|
||||
|
@@ -0,0 +1,74 @@
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
|
||||
interface AuthProviderDeleteModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
provider: { id: string; name: string; displayName: string } | null;
|
||||
onConfirm: () => Promise<void>;
|
||||
isDeleting: boolean;
|
||||
}
|
||||
|
||||
export function AuthProviderDeleteModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
provider,
|
||||
onConfirm,
|
||||
isDeleting,
|
||||
}: AuthProviderDeleteModalProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const handleConfirm = async () => {
|
||||
await onConfirm();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={() => !isDeleting && onClose()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-destructive">
|
||||
<IconTrash size={20} />
|
||||
Delete Authentication Provider
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Are you sure you want to delete the "{provider?.displayName}" provider? This action cannot be undone.
|
||||
</p>
|
||||
|
||||
{provider && (
|
||||
<div className="rounded-lg border p-4 bg-muted/30">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">{provider.displayName}</h4>
|
||||
<p className="text-sm text-muted-foreground">Provider ID: {provider.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex gap-2">
|
||||
<Button variant="outline" onClick={onClose} disabled={isDeleting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleConfirm} disabled={isDeleting}>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<IconTrash className="h-4 w-4 mr-2 animate-spin" />
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconTrash className="h-4 w-4 mr-2" />
|
||||
Delete Provider
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@@ -31,6 +31,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { TagsInput } from "@/components/ui/tags-input";
|
||||
import { AuthProviderDeleteModal } from "./auth-provider-delete-modal";
|
||||
|
||||
interface AuthProvider {
|
||||
id: string;
|
||||
@@ -76,6 +77,10 @@ export function AuthProvidersSettings() {
|
||||
const [editingProvider, setEditingProvider] = useState<AuthProvider | null>(null);
|
||||
const [editingFormData, setEditingFormData] = useState<Record<string, any>>({});
|
||||
const [hideDisabledProviders, setHideDisabledProviders] = useState<boolean>(false);
|
||||
const [providerToDelete, setProviderToDelete] = useState<{ id: string; name: string; displayName: string } | null>(
|
||||
null
|
||||
);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const [newProvider, setNewProvider] = useState<NewProvider>({
|
||||
name: "",
|
||||
@@ -138,6 +143,8 @@ export function AuthProvidersSettings() {
|
||||
{ pattern: "monday.com", scopes: ["read"] },
|
||||
{ pattern: "clickup.com", scopes: ["read"] },
|
||||
{ pattern: "linear.app", scopes: ["read"] },
|
||||
{ pattern: "kinde.com", scopes: ["openid", "profile", "email"] },
|
||||
{ pattern: "zitadel.com", scopes: ["openid", "profile", "email"] },
|
||||
];
|
||||
|
||||
// Procura por padrões conhecidos
|
||||
@@ -231,10 +238,8 @@ export function AuthProvidersSettings() {
|
||||
|
||||
// Delete provider
|
||||
const deleteProvider = async (id: string, name: string) => {
|
||||
if (!confirm(`Delete "${name}" provider? This cannot be undone.`)) return;
|
||||
|
||||
try {
|
||||
setSaving(id);
|
||||
setIsDeleting(true);
|
||||
const response = await fetch(`/api/auth/providers/manage/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
@@ -244,6 +249,7 @@ export function AuthProvidersSettings() {
|
||||
if (data.success) {
|
||||
setProviders((prev) => prev.filter((p) => p.id !== id));
|
||||
toast.success("Provider deleted");
|
||||
setProviderToDelete(null);
|
||||
} else {
|
||||
toast.error("Failed to delete provider");
|
||||
}
|
||||
@@ -251,10 +257,19 @@ export function AuthProvidersSettings() {
|
||||
console.error("Error deleting provider:", error);
|
||||
toast.error("Failed to delete provider");
|
||||
} finally {
|
||||
setSaving(null);
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Open delete confirmation modal
|
||||
const openDeleteModal = (provider: AuthProvider) => {
|
||||
setProviderToDelete({
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
displayName: provider.displayName,
|
||||
});
|
||||
};
|
||||
|
||||
// Add new provider
|
||||
const addProvider = async () => {
|
||||
if (!newProvider.name || !newProvider.displayName || !newProvider.clientId || !newProvider.clientSecret) {
|
||||
@@ -802,7 +817,7 @@ export function AuthProvidersSettings() {
|
||||
setEditingProvider(provider);
|
||||
}
|
||||
}}
|
||||
onDelete={() => deleteProvider(provider.id, provider.displayName)}
|
||||
onDelete={() => openDeleteModal(provider)}
|
||||
saving={saving === provider.id}
|
||||
getIcon={getProviderIcon}
|
||||
editingProvider={editingProvider}
|
||||
@@ -843,7 +858,7 @@ export function AuthProvidersSettings() {
|
||||
setEditingProvider(provider);
|
||||
}
|
||||
}}
|
||||
onDelete={() => deleteProvider(provider.id, provider.displayName)}
|
||||
onDelete={() => openDeleteModal(provider)}
|
||||
saving={saving === provider.id}
|
||||
getIcon={getProviderIcon}
|
||||
editingProvider={editingProvider}
|
||||
@@ -876,6 +891,19 @@ export function AuthProvidersSettings() {
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<AuthProviderDeleteModal
|
||||
isOpen={!!providerToDelete}
|
||||
onClose={() => setProviderToDelete(null)}
|
||||
provider={providerToDelete}
|
||||
onConfirm={async () => {
|
||||
if (providerToDelete) {
|
||||
await deleteProvider(providerToDelete.id, providerToDelete.name);
|
||||
}
|
||||
}}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1047,36 +1075,10 @@ function EditProviderForm({
|
||||
{ pattern: "facebook.com", scopes: ["public_profile", "email"] },
|
||||
{ pattern: "twitter.com", scopes: ["tweet.read", "users.read"] },
|
||||
{ pattern: "linkedin.com", scopes: ["r_liteprofile", "r_emailaddress"] },
|
||||
{ pattern: "authentik", scopes: ["openid", "profile", "email"] },
|
||||
{ pattern: "keycloak", scopes: ["openid", "profile", "email"] },
|
||||
{ pattern: "auth0.com", scopes: ["openid", "profile", "email"] },
|
||||
{ pattern: "okta.com", scopes: ["openid", "profile", "email"] },
|
||||
{ pattern: "onelogin.com", scopes: ["openid", "profile", "email"] },
|
||||
{ pattern: "pingidentity.com", scopes: ["openid", "profile", "email"] },
|
||||
{ pattern: "azure.com", scopes: ["openid", "profile", "email", "User.Read"] },
|
||||
{ pattern: "aws.amazon.com", scopes: ["openid", "profile", "email"] },
|
||||
{ pattern: "slack.com", scopes: ["identity.basic", "identity.email", "identity.avatar"] },
|
||||
{ pattern: "bitbucket.org", scopes: ["account", "repository"] },
|
||||
{ pattern: "atlassian.com", scopes: ["read:jira-user", "read:jira-work"] },
|
||||
{ pattern: "salesforce.com", scopes: ["api", "refresh_token"] },
|
||||
{ pattern: "zendesk.com", scopes: ["read"] },
|
||||
{ pattern: "shopify.com", scopes: ["read_products", "read_customers"] },
|
||||
{ pattern: "stripe.com", scopes: ["read"] },
|
||||
{ pattern: "twilio.com", scopes: ["read"] },
|
||||
{ pattern: "sendgrid.com", scopes: ["mail.send"] },
|
||||
{ pattern: "mailchimp.com", scopes: ["read"] },
|
||||
{ pattern: "hubspot.com", scopes: ["contacts", "crm.objects.contacts.read"] },
|
||||
{ pattern: "zoom.us", scopes: ["user:read:admin"] },
|
||||
{ pattern: "teams.microsoft.com", scopes: ["openid", "profile", "email", "User.Read"] },
|
||||
{ pattern: "notion.so", scopes: ["read"] },
|
||||
{ pattern: "figma.com", scopes: ["files:read"] },
|
||||
{ pattern: "dropbox.com", scopes: ["files.content.read"] },
|
||||
{ pattern: "box.com", scopes: ["root_readwrite"] },
|
||||
{ pattern: "trello.com", scopes: ["read"] },
|
||||
{ pattern: "asana.com", scopes: ["default"] },
|
||||
{ pattern: "monday.com", scopes: ["read"] },
|
||||
{ pattern: "clickup.com", scopes: ["read"] },
|
||||
{ pattern: "linear.app", scopes: ["read"] },
|
||||
{ pattern: "kinde.com", scopes: ["openid", "profile", "email"] },
|
||||
{ pattern: "zitadel.com", scopes: ["openid", "profile", "email"] },
|
||||
];
|
||||
|
||||
// Procura por padrões conhecidos
|
||||
|
Reference in New Issue
Block a user