mirror of
https://github.com/amir20/dozzle.git
synced 2026-06-23 04:10:12 +00:00
feat: supports dozzle cloud (#4381)
This commit is contained in:
Vendored
+3
@@ -92,6 +92,7 @@ declare module 'vue' {
|
||||
'Mdi:bell': typeof import('~icons/mdi/bell')['default']
|
||||
'Mdi:bellOutline': typeof import('~icons/mdi/bell-outline')['default']
|
||||
'Mdi:check': typeof import('~icons/mdi/check')['default']
|
||||
'Mdi:checkCircle': typeof import('~icons/mdi/check-circle')['default']
|
||||
'Mdi:chevronDoubleDown': typeof import('~icons/mdi/chevron-double-down')['default']
|
||||
'Mdi:chevronDown': typeof import('~icons/mdi/chevron-down')['default']
|
||||
'Mdi:chevronLeft': typeof import('~icons/mdi/chevron-left')['default']
|
||||
@@ -99,6 +100,7 @@ declare module 'vue' {
|
||||
'Mdi:clockOutline': typeof import('~icons/mdi/clock-outline')['default']
|
||||
'Mdi:close': typeof import('~icons/mdi/close')['default']
|
||||
'Mdi:cloud': typeof import('~icons/mdi/cloud')['default']
|
||||
'Mdi:cloudOutline': typeof import('~icons/mdi/cloud-outline')['default']
|
||||
'Mdi:cog': typeof import('~icons/mdi/cog')['default']
|
||||
'Mdi:docker': typeof import('~icons/mdi/docker')['default']
|
||||
'Mdi:gauge': typeof import('~icons/mdi/gauge')['default']
|
||||
@@ -108,6 +110,7 @@ declare module 'vue' {
|
||||
'Mdi:key': typeof import('~icons/mdi/key')['default']
|
||||
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
'Mdi:lightningBolt': typeof import('~icons/mdi/lightning-bolt')['default']
|
||||
'Mdi:linkVariant': typeof import('~icons/mdi/link-variant')['default']
|
||||
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
|
||||
'Mdi:packageVariantClosed': typeof import('~icons/mdi/package-variant-closed')['default']
|
||||
'Mdi:pencilOutline': typeof import('~icons/mdi/pencil-outline')['default']
|
||||
|
||||
@@ -53,7 +53,7 @@ const { destination } = defineProps<{
|
||||
|
||||
const showDrawer = useDrawer();
|
||||
const deleteMutation = useMutation(DeleteDispatcherDocument);
|
||||
const dispatchersQuery = useQuery({ query: GetDispatchersDocument, pause: true });
|
||||
const dispatchersQuery = useQuery({ query: GetDispatchersDocument });
|
||||
const alertsQuery = useQuery({ query: GetNotificationRulesDocument, pause: true });
|
||||
|
||||
function editDestination() {
|
||||
@@ -62,6 +62,7 @@ function editDestination() {
|
||||
{
|
||||
destination,
|
||||
onCreated: () => dispatchersQuery.executeQuery({ requestPolicy: "network-only" }),
|
||||
existingDispatchers: dispatchersQuery.data.value?.dispatchers ?? [],
|
||||
},
|
||||
"md",
|
||||
);
|
||||
|
||||
@@ -11,20 +11,6 @@
|
||||
<p class="text-base-content/60">{{ $t("notifications.destination-form.description") }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.name") }}</legend>
|
||||
<input
|
||||
ref="nameInput"
|
||||
v-model="name"
|
||||
type="text"
|
||||
class="input focus:input-primary w-full text-base"
|
||||
required
|
||||
:class="{ 'input-primary': name.trim().length > 0 }"
|
||||
:placeholder="$t('notifications.destination-form.name-placeholder')"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<!-- Type Selection -->
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.type") }}</legend>
|
||||
@@ -44,22 +30,69 @@
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
class="card card-border border-base-content/20 cursor-pointer transition-colors"
|
||||
:class="type === 'cloud' ? 'border-primary bg-primary/10' : ''"
|
||||
class="card card-border border-base-content/20 transition-colors"
|
||||
:class="[
|
||||
type === 'cloud' ? 'border-primary bg-primary/10' : '',
|
||||
hasExistingCloudDestination && type !== 'cloud' ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
|
||||
]"
|
||||
>
|
||||
<div class="card-body flex-row items-center gap-3 p-4">
|
||||
<input type="radio" v-model="type" value="cloud" class="radio radio-primary" />
|
||||
<input
|
||||
type="radio"
|
||||
v-model="type"
|
||||
value="cloud"
|
||||
class="radio radio-primary"
|
||||
:disabled="hasExistingCloudDestination && type !== 'cloud'"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-semibold">{{ $t("notifications.destination-form.cloud-title") }}</div>
|
||||
<div class="text-base-content/60 text-sm">
|
||||
{{ $t("notifications.destination-form.cloud-description") }}
|
||||
</div>
|
||||
<div v-if="hasExistingCloudDestination && type !== 'cloud'" class="text-warning mt-1 text-xs">
|
||||
{{ $t("notifications.destination-form.cloud-exists") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Name (only for webhook type) -->
|
||||
<fieldset v-if="type === 'webhook'" class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.name") }}</legend>
|
||||
<input
|
||||
ref="nameInput"
|
||||
v-model="name"
|
||||
type="text"
|
||||
class="input focus:input-primary w-full text-base"
|
||||
required
|
||||
:class="{ 'input-primary': name.trim().length > 0 }"
|
||||
:placeholder="$t('notifications.destination-form.name-placeholder')"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<!-- Cloud linked success (when editing cloud with apiKey) -->
|
||||
<fieldset v-if="type === 'cloud' && destination?.apiKey" class="fieldset">
|
||||
<div class="alert alert-success">
|
||||
<mdi:check-circle class="text-xl" />
|
||||
<span>{{ $t("notifications.destination-form.cloud-linked") }}</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Link Dozzle Cloud (only for cloud type, when creating or not linked) -->
|
||||
<div v-else-if="type === 'cloud'" class="card card-border border-primary/30 bg-primary/5">
|
||||
<div class="card-body items-center text-center">
|
||||
<mdi:cloud-outline class="text-primary text-4xl" />
|
||||
<h3 class="card-title">{{ $t("notifications.destination-form.link-cloud") }}</h3>
|
||||
<p class="text-base-content/60 text-sm">{{ $t("notifications.destination-form.cloud-description") }}</p>
|
||||
<a :href="cloudLinkUrl" class="btn btn-primary btn-lg mt-2">
|
||||
<mdi:link-variant class="text-lg" />
|
||||
{{ $t("notifications.destination-form.link-cloud-button") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Webhook URL (only for webhook type) -->
|
||||
<fieldset v-if="type === 'webhook'" class="fieldset">
|
||||
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.webhook-url") }}</legend>
|
||||
@@ -122,7 +155,12 @@
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-2 pt-4">
|
||||
<button class="btn" @click="testDestination" :disabled="!canTest || !isValidUrl || isTesting">
|
||||
<button
|
||||
v-if="type === 'webhook'"
|
||||
class="btn"
|
||||
@click="testDestination"
|
||||
:disabled="!canTest || !isValidUrl || isTesting"
|
||||
>
|
||||
<span v-if="isTesting" class="loading loading-spinner loading-sm"></span>
|
||||
{{ $t("notifications.destination-form.test") }}
|
||||
</button>
|
||||
@@ -195,12 +233,24 @@ const PAYLOAD_TEMPLATES: Record<PayloadFormat, string> = {
|
||||
}`,
|
||||
};
|
||||
|
||||
const { close, onCreated, destination } = defineProps<{
|
||||
const {
|
||||
close,
|
||||
onCreated,
|
||||
destination,
|
||||
existingDispatchers = [],
|
||||
} = defineProps<{
|
||||
close?: () => void;
|
||||
onCreated?: () => void;
|
||||
destination?: Dispatcher;
|
||||
existingDispatchers?: Dispatcher[];
|
||||
}>();
|
||||
|
||||
const hasExistingCloudDestination = computed(() => {
|
||||
// When editing, exclude the current destination from the check
|
||||
const others = isEditing ? existingDispatchers.filter((d) => d.id !== destination!.id) : existingDispatchers;
|
||||
return others.some((d) => d.type === "cloud");
|
||||
});
|
||||
|
||||
const createMutation = useMutation(CreateDispatcherDocument);
|
||||
const updateMutation = useMutation(UpdateDispatcherDocument);
|
||||
const testMutation = useMutation(TestWebhookDocument);
|
||||
@@ -219,6 +269,11 @@ const isSaving = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const testResult = ref<TestWebhookResult | null>(null);
|
||||
|
||||
const cloudLinkUrl = computed(() => {
|
||||
const callbackUrl = `${window.location.origin}${withBase("/")}`;
|
||||
return `${__CLOUD_URL__}/link?appUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
});
|
||||
|
||||
function selectPayloadFormat(format: PayloadFormat) {
|
||||
payloadFormat.value = format;
|
||||
template.value = PAYLOAD_TEMPLATES[format];
|
||||
@@ -242,8 +297,11 @@ const isValidUrl = computed(() => {
|
||||
|
||||
const canSave = computed(() => {
|
||||
if (isSaving.value) return false;
|
||||
if (!name.value.trim()) return false;
|
||||
if (type.value === "webhook" && !isValidUrl.value) return false;
|
||||
if (type.value === "cloud") return false;
|
||||
if (type.value === "webhook") {
|
||||
if (!name.value.trim()) return false;
|
||||
if (!isValidUrl.value) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -283,8 +341,8 @@ async function saveDestination() {
|
||||
const input = {
|
||||
name: name.value.trim(),
|
||||
type: type.value,
|
||||
url: type.value === "webhook" ? webhookUrl.value.trim() : undefined,
|
||||
template: type.value === "webhook" && template.value.trim() ? template.value.trim() : undefined,
|
||||
url: webhookUrl.value.trim(),
|
||||
template: template.value.trim() || undefined,
|
||||
};
|
||||
|
||||
const result = isEditing
|
||||
|
||||
@@ -24,6 +24,7 @@ query GetDispatchers {
|
||||
type
|
||||
url
|
||||
template
|
||||
apiKey
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,11 +70,35 @@ import DestinationForm from "@/components/Notification/DestinationForm.vue";
|
||||
import DestinationCard from "@/components/Notification/DestinationCard.vue";
|
||||
|
||||
const showDrawer = useDrawer();
|
||||
const route = useRoute();
|
||||
|
||||
// GraphQL queries
|
||||
const alertsQuery = useQuery({ query: GetNotificationRulesDocument });
|
||||
const dispatchersQuery = useQuery({ query: GetDispatchersDocument });
|
||||
|
||||
// Handle newCloudLink query param
|
||||
watch(
|
||||
() => [route.query.newCloudLink, dispatchersQuery.data.value],
|
||||
([newCloudLink, data]) => {
|
||||
if (newCloudLink && data) {
|
||||
const id = Number(newCloudLink);
|
||||
const destination = dispatchers.value.find((d) => d.id === id);
|
||||
if (destination) {
|
||||
showDrawer(
|
||||
DestinationForm,
|
||||
{
|
||||
destination,
|
||||
onCreated: () => dispatchersQuery.executeQuery({ requestPolicy: "network-only" }),
|
||||
existingDispatchers: dispatchers.value,
|
||||
},
|
||||
"md",
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Computed data from queries
|
||||
const alerts = computed(() => alertsQuery.data.value?.notificationRules ?? []);
|
||||
const dispatchers = computed(() => dispatchersQuery.data.value?.dispatchers ?? []);
|
||||
@@ -98,7 +122,10 @@ function openCreateAlert() {
|
||||
function openAddDestination() {
|
||||
showDrawer(
|
||||
DestinationForm,
|
||||
{ onCreated: () => dispatchersQuery.executeQuery({ requestPolicy: "network-only" }) },
|
||||
{
|
||||
onCreated: () => dispatchersQuery.executeQuery({ requestPolicy: "network-only" }),
|
||||
existingDispatchers: dispatchers.value,
|
||||
},
|
||||
"md",
|
||||
);
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -4,3 +4,5 @@ declare module "*.vue" {
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare const __CLOUD_URL__: string;
|
||||
|
||||
@@ -36,6 +36,7 @@ export type Container = {
|
||||
|
||||
export type Dispatcher = {
|
||||
__typename?: 'Dispatcher';
|
||||
apiKey?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['Int']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
template?: Maybe<Scalars['String']['output']>;
|
||||
@@ -44,6 +45,7 @@ export type Dispatcher = {
|
||||
};
|
||||
|
||||
export type DispatcherInput = {
|
||||
apiKey?: InputMaybe<Scalars['String']['input']>;
|
||||
name: Scalars['String']['input'];
|
||||
template?: InputMaybe<Scalars['String']['input']>;
|
||||
type: Scalars['String']['input'];
|
||||
@@ -218,7 +220,7 @@ export type GetNotificationRulesQuery = { __typename?: 'Query', notificationRule
|
||||
export type GetDispatchersQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetDispatchersQuery = { __typename?: 'Query', dispatchers: Array<{ __typename?: 'Dispatcher', id: number, name: string, type: string, url?: string | null, template?: string | null }> };
|
||||
export type GetDispatchersQuery = { __typename?: 'Query', dispatchers: Array<{ __typename?: 'Dispatcher', id: number, name: string, type: string, url?: string | null, template?: string | null, apiKey?: string | null }> };
|
||||
|
||||
export type CreateNotificationRuleMutationVariables = Exact<{
|
||||
input: NotificationRuleInput;
|
||||
@@ -293,7 +295,7 @@ export type GetReleasesQuery = { __typename?: 'Query', releases: Array<{ __typen
|
||||
|
||||
|
||||
export const GetNotificationRulesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetNotificationRules"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notificationRules"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"containerExpression"}},{"kind":"Field","name":{"kind":"Name","value":"logExpression"}},{"kind":"Field","name":{"kind":"Name","value":"triggerCount"}},{"kind":"Field","name":{"kind":"Name","value":"triggeredContainers"}},{"kind":"Field","name":{"kind":"Name","value":"lastTriggeredAt"}},{"kind":"Field","name":{"kind":"Name","value":"dispatcher"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]}}]} as unknown as DocumentNode<GetNotificationRulesQuery, GetNotificationRulesQueryVariables>;
|
||||
export const GetDispatchersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDispatchers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dispatchers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"template"}}]}}]}}]} as unknown as DocumentNode<GetDispatchersQuery, GetDispatchersQueryVariables>;
|
||||
export const GetDispatchersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDispatchers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dispatchers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"template"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}}]}}]} as unknown as DocumentNode<GetDispatchersQuery, GetDispatchersQueryVariables>;
|
||||
export const CreateNotificationRuleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNotificationRule"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationRuleInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createNotificationRule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"containerExpression"}},{"kind":"Field","name":{"kind":"Name","value":"logExpression"}},{"kind":"Field","name":{"kind":"Name","value":"dispatcher"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}}]}}]} as unknown as DocumentNode<CreateNotificationRuleMutation, CreateNotificationRuleMutationVariables>;
|
||||
export const UpdateNotificationRuleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateNotificationRule"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationRuleUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateNotificationRule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"containerExpression"}},{"kind":"Field","name":{"kind":"Name","value":"logExpression"}},{"kind":"Field","name":{"kind":"Name","value":"dispatcher"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}}]}}]} as unknown as DocumentNode<UpdateNotificationRuleMutation, UpdateNotificationRuleMutationVariables>;
|
||||
export const ReplaceNotificationRuleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ReplaceNotificationRule"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationRuleInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"replaceNotificationRule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"containerExpression"}},{"kind":"Field","name":{"kind":"Name","value":"logExpression"}},{"kind":"Field","name":{"kind":"Name","value":"dispatcher"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}}]}}]} as unknown as DocumentNode<ReplaceNotificationRuleMutation, ReplaceNotificationRuleMutationVariables>;
|
||||
|
||||
@@ -14,8 +14,7 @@ github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43
|
||||
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||
github.com/air-verse/air v1.64.3 h1:bCLjOVQUvHd7e8FjQjYNDQsSSeXa6/S2s7uYhAVogdk=
|
||||
github.com/air-verse/air v1.64.3/go.mod h1:SOwv08NTvKextanAY819AK0FK6XzQO5/tC6s1R+rk8o=
|
||||
github.com/air-verse/air v1.64.4 h1:P0alz5Jia5NucZew1HYZy69lzPWZHFjLTCKo0VhxME8=
|
||||
github.com/air-verse/air v1.64.4/go.mod h1:OaJZSfZqf7wyjS2oP/CcEVyIt0JmZuPh5x1gdtklmmY=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
||||
|
||||
+56
-1
@@ -68,6 +68,7 @@ type ComplexityRoot struct {
|
||||
}
|
||||
|
||||
Dispatcher struct {
|
||||
APIKey func(childComplexity int) int
|
||||
ID func(childComplexity int) int
|
||||
Name func(childComplexity int) int
|
||||
Template func(childComplexity int) int
|
||||
@@ -264,6 +265,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
||||
|
||||
return e.complexity.Container.State(childComplexity), true
|
||||
|
||||
case "Dispatcher.apiKey":
|
||||
if e.complexity.Dispatcher.APIKey == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.Dispatcher.APIKey(childComplexity), true
|
||||
case "Dispatcher.id":
|
||||
if e.complexity.Dispatcher.ID == nil {
|
||||
break
|
||||
@@ -1437,6 +1444,35 @@ func (ec *executionContext) fieldContext_Dispatcher_template(_ context.Context,
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Dispatcher_apiKey(ctx context.Context, field graphql.CollectedField, obj *model.Dispatcher) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
ec.OperationContext,
|
||||
field,
|
||||
ec.fieldContext_Dispatcher_apiKey,
|
||||
func(ctx context.Context) (any, error) {
|
||||
return obj.APIKey, nil
|
||||
},
|
||||
nil,
|
||||
ec.marshalOString2ᚖstring,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_Dispatcher_apiKey(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "Dispatcher",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type String does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _LogEvent_id(ctx context.Context, field graphql.CollectedField, obj *container.LogEvent) (ret graphql.Marshaler) {
|
||||
return graphql.ResolveField(
|
||||
ctx,
|
||||
@@ -1899,6 +1935,8 @@ func (ec *executionContext) fieldContext_Mutation_createDispatcher(ctx context.C
|
||||
return ec.fieldContext_Dispatcher_url(ctx, field)
|
||||
case "template":
|
||||
return ec.fieldContext_Dispatcher_template(ctx, field)
|
||||
case "apiKey":
|
||||
return ec.fieldContext_Dispatcher_apiKey(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type Dispatcher", field.Name)
|
||||
},
|
||||
@@ -1952,6 +1990,8 @@ func (ec *executionContext) fieldContext_Mutation_updateDispatcher(ctx context.C
|
||||
return ec.fieldContext_Dispatcher_url(ctx, field)
|
||||
case "template":
|
||||
return ec.fieldContext_Dispatcher_template(ctx, field)
|
||||
case "apiKey":
|
||||
return ec.fieldContext_Dispatcher_apiKey(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type Dispatcher", field.Name)
|
||||
},
|
||||
@@ -2234,6 +2274,8 @@ func (ec *executionContext) fieldContext_NotificationRule_dispatcher(_ context.C
|
||||
return ec.fieldContext_Dispatcher_url(ctx, field)
|
||||
case "template":
|
||||
return ec.fieldContext_Dispatcher_template(ctx, field)
|
||||
case "apiKey":
|
||||
return ec.fieldContext_Dispatcher_apiKey(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type Dispatcher", field.Name)
|
||||
},
|
||||
@@ -2715,6 +2757,8 @@ func (ec *executionContext) fieldContext_Query_dispatchers(_ context.Context, fi
|
||||
return ec.fieldContext_Dispatcher_url(ctx, field)
|
||||
case "template":
|
||||
return ec.fieldContext_Dispatcher_template(ctx, field)
|
||||
case "apiKey":
|
||||
return ec.fieldContext_Dispatcher_apiKey(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type Dispatcher", field.Name)
|
||||
},
|
||||
@@ -2757,6 +2801,8 @@ func (ec *executionContext) fieldContext_Query_dispatcher(ctx context.Context, f
|
||||
return ec.fieldContext_Dispatcher_url(ctx, field)
|
||||
case "template":
|
||||
return ec.fieldContext_Dispatcher_template(ctx, field)
|
||||
case "apiKey":
|
||||
return ec.fieldContext_Dispatcher_apiKey(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type Dispatcher", field.Name)
|
||||
},
|
||||
@@ -4764,7 +4810,7 @@ func (ec *executionContext) unmarshalInputDispatcherInput(ctx context.Context, o
|
||||
asMap[k] = v
|
||||
}
|
||||
|
||||
fieldsInOrder := [...]string{"name", "type", "url", "template"}
|
||||
fieldsInOrder := [...]string{"name", "type", "url", "template", "apiKey"}
|
||||
for _, k := range fieldsInOrder {
|
||||
v, ok := asMap[k]
|
||||
if !ok {
|
||||
@@ -4799,6 +4845,13 @@ func (ec *executionContext) unmarshalInputDispatcherInput(ctx context.Context, o
|
||||
return it, err
|
||||
}
|
||||
it.Template = data
|
||||
case "apiKey":
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("apiKey"))
|
||||
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
it.APIKey = data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5129,6 +5182,8 @@ func (ec *executionContext) _Dispatcher(ctx context.Context, sel ast.SelectionSe
|
||||
out.Values[i] = ec._Dispatcher_url(ctx, field, obj)
|
||||
case "template":
|
||||
out.Values[i] = ec._Dispatcher_template(ctx, field, obj)
|
||||
case "apiKey":
|
||||
out.Values[i] = ec._Dispatcher_apiKey(ctx, field, obj)
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
|
||||
@@ -44,12 +44,17 @@ func dispatcherConfigToDispatcher(d *notification.DispatcherConfig) *model.Dispa
|
||||
if d.Template != "" {
|
||||
template = &d.Template
|
||||
}
|
||||
var apiKey *string
|
||||
if d.APIKey != "" {
|
||||
apiKey = &d.APIKey
|
||||
}
|
||||
return &model.Dispatcher{
|
||||
ID: int32(d.ID),
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: url,
|
||||
Template: template,
|
||||
APIKey: apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ type Dispatcher struct {
|
||||
Type string `json:"type"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Template *string `json:"template,omitempty"`
|
||||
APIKey *string `json:"apiKey,omitempty"`
|
||||
}
|
||||
|
||||
type DispatcherInput struct {
|
||||
@@ -21,6 +22,7 @@ type DispatcherInput struct {
|
||||
Type string `json:"type"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Template *string `json:"template,omitempty"`
|
||||
APIKey *string `json:"apiKey,omitempty"`
|
||||
}
|
||||
|
||||
type Mutation struct {
|
||||
|
||||
@@ -47,6 +47,7 @@ type Dispatcher {
|
||||
type: String!
|
||||
url: String
|
||||
template: String
|
||||
apiKey: String
|
||||
}
|
||||
|
||||
type PreviewResult {
|
||||
@@ -92,6 +93,7 @@ input DispatcherInput {
|
||||
type: String!
|
||||
url: String
|
||||
template: String
|
||||
apiKey: String
|
||||
}
|
||||
|
||||
input PreviewInput {
|
||||
|
||||
@@ -131,6 +131,16 @@ func (r *mutationResolver) CreateDispatcher(ctx context.Context, input model.Dis
|
||||
return nil, &Error{Message: err.Error()}
|
||||
}
|
||||
d = webhook
|
||||
case "cloud":
|
||||
apiKey := ""
|
||||
if input.APIKey != nil {
|
||||
apiKey = *input.APIKey
|
||||
}
|
||||
cloud, err := dispatcher.NewCloudDispatcher(input.Name, apiKey)
|
||||
if err != nil {
|
||||
return nil, &Error{Message: err.Error()}
|
||||
}
|
||||
d = cloud
|
||||
default:
|
||||
return nil, &Error{Message: "unknown dispatcher type"}
|
||||
}
|
||||
@@ -164,6 +174,16 @@ func (r *mutationResolver) UpdateDispatcher(ctx context.Context, id int32, input
|
||||
return nil, &Error{Message: err.Error()}
|
||||
}
|
||||
d = webhook
|
||||
case "cloud":
|
||||
apiKey := ""
|
||||
if input.APIKey != nil {
|
||||
apiKey = *input.APIKey
|
||||
}
|
||||
cloud, err := dispatcher.NewCloudDispatcher(input.Name, apiKey)
|
||||
if err != nil {
|
||||
return nil, &Error{Message: err.Error()}
|
||||
}
|
||||
d = cloud
|
||||
default:
|
||||
return nil, &Error{Message: "unknown dispatcher type"}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package dispatcher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// CloudDispatcher sends notifications to Dozzle Cloud
|
||||
type CloudDispatcher struct {
|
||||
Name string
|
||||
URL string
|
||||
APIKey string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewCloudDispatcher creates a new cloud dispatcher
|
||||
func NewCloudDispatcher(name string, apiKey string) (*CloudDispatcher, error) {
|
||||
url := os.Getenv("DOLIGENCE_URL")
|
||||
if url == "" {
|
||||
url = "https://doligence.dozzle.dev"
|
||||
}
|
||||
url = url + "/api/events"
|
||||
|
||||
if apiKey == "" {
|
||||
return nil, fmt.Errorf("API key is required for cloud dispatcher")
|
||||
}
|
||||
|
||||
return &CloudDispatcher{
|
||||
Name: name,
|
||||
URL: url,
|
||||
APIKey: apiKey,
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Send sends a notification to Dozzle Cloud
|
||||
func (c *CloudDispatcher) Send(ctx context.Context, notification types.Notification) error {
|
||||
payload, err := json.Marshal(notification)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.URL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-API-Key", c.APIKey)
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send to cloud: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
limitedReader := io.LimitReader(resp.Body, 1024*1024)
|
||||
responseBody, _ := io.ReadAll(limitedReader)
|
||||
log.Debug().
|
||||
Str("cloud", c.Name).
|
||||
Str("url", c.URL).
|
||||
Int("status_code", resp.StatusCode).
|
||||
Str("payload", string(payload)).
|
||||
Str("response_body", string(responseBody)).
|
||||
Msg("cloud returned non-success status code")
|
||||
return fmt.Errorf("cloud returned status code %d: %s", resp.StatusCode, string(responseBody))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -290,6 +290,13 @@ func (m *Manager) Dispatchers() []DispatcherConfig {
|
||||
URL: v.URL,
|
||||
Template: v.TemplateText,
|
||||
})
|
||||
case *dispatcher.CloudDispatcher:
|
||||
result = append(result, DispatcherConfig{
|
||||
ID: id,
|
||||
Name: v.Name,
|
||||
Type: "cloud",
|
||||
APIKey: v.APIKey,
|
||||
})
|
||||
}
|
||||
return true
|
||||
})
|
||||
@@ -410,53 +417,32 @@ func (m *Manager) LoadConfig(r io.Reader) error {
|
||||
return fmt.Errorf("failed to decode config: %w", err)
|
||||
}
|
||||
|
||||
// Find max IDs to initialize counters
|
||||
var maxSubID, maxDispatcherID int
|
||||
for _, sub := range config.Subscriptions {
|
||||
if sub.ID > maxSubID {
|
||||
maxSubID = sub.ID
|
||||
}
|
||||
}
|
||||
for _, d := range config.Dispatchers {
|
||||
if d.ID > maxDispatcherID {
|
||||
maxDispatcherID = d.ID
|
||||
}
|
||||
}
|
||||
m.subscriptionCounter.Store(int32(maxSubID))
|
||||
m.dispatcherCounter.Store(int32(maxDispatcherID))
|
||||
|
||||
// Load subscriptions
|
||||
for _, sub := range config.Subscriptions {
|
||||
if err := m.loadSubscription(sub); err != nil {
|
||||
return fmt.Errorf("failed to add subscription %s: %w", sub.Name, err)
|
||||
// Convert to types for HandleNotificationConfig
|
||||
subscriptions := make([]types.SubscriptionConfig, len(config.Subscriptions))
|
||||
for i, sub := range config.Subscriptions {
|
||||
subscriptions[i] = types.SubscriptionConfig{
|
||||
ID: sub.ID,
|
||||
Name: sub.Name,
|
||||
Enabled: sub.Enabled,
|
||||
DispatcherID: sub.DispatcherID,
|
||||
LogExpression: sub.LogExpression,
|
||||
ContainerExpression: sub.ContainerExpression,
|
||||
}
|
||||
}
|
||||
|
||||
// Load dispatchers
|
||||
for _, dispatcherConfig := range config.Dispatchers {
|
||||
var d dispatcher.Dispatcher
|
||||
switch dispatcherConfig.Type {
|
||||
case "webhook":
|
||||
webhook, err := dispatcher.NewWebhookDispatcher(dispatcherConfig.Name, dispatcherConfig.URL, dispatcherConfig.Template)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create webhook dispatcher %s: %w", dispatcherConfig.Name, err)
|
||||
}
|
||||
d = webhook
|
||||
default:
|
||||
return fmt.Errorf("unknown dispatcher type: %s", dispatcherConfig.Type)
|
||||
}
|
||||
m.dispatchers.Store(dispatcherConfig.ID, d)
|
||||
log.Debug().Int("id", dispatcherConfig.ID).Msg("Loaded dispatcher")
|
||||
}
|
||||
|
||||
// Update listener to start streams for loaded subscriptions
|
||||
if m.listener != nil {
|
||||
if err := m.listener.UpdateStreams(); err != nil {
|
||||
return fmt.Errorf("failed to update listener streams: %w", err)
|
||||
dispatchers := make([]types.DispatcherConfig, len(config.Dispatchers))
|
||||
for i, d := range config.Dispatchers {
|
||||
dispatchers[i] = types.DispatcherConfig{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Type: d.Type,
|
||||
URL: d.URL,
|
||||
Template: d.Template,
|
||||
APIKey: d.APIKey,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return m.HandleNotificationConfig(subscriptions, dispatchers)
|
||||
}
|
||||
|
||||
// HandleNotificationConfig implements agent.NotificationConfigHandler interface
|
||||
@@ -505,20 +491,20 @@ func (m *Manager) HandleNotificationConfig(subscriptions []types.SubscriptionCon
|
||||
}
|
||||
|
||||
// Load dispatchers
|
||||
for _, dispatcherConfig := range dispatchers {
|
||||
var d dispatcher.Dispatcher
|
||||
switch dispatcherConfig.Type {
|
||||
case "webhook":
|
||||
webhook, err := dispatcher.NewWebhookDispatcher(dispatcherConfig.Name, dispatcherConfig.URL, dispatcherConfig.Template)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create webhook dispatcher %s: %w", dispatcherConfig.Name, err)
|
||||
}
|
||||
d = webhook
|
||||
default:
|
||||
return fmt.Errorf("unknown dispatcher type: %s", dispatcherConfig.Type)
|
||||
for _, dc := range dispatchers {
|
||||
d, err := createDispatcher(DispatcherConfig{
|
||||
ID: dc.ID,
|
||||
Name: dc.Name,
|
||||
Type: dc.Type,
|
||||
URL: dc.URL,
|
||||
Template: dc.Template,
|
||||
APIKey: dc.APIKey,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create dispatcher %s: %w", dc.Name, err)
|
||||
}
|
||||
m.dispatchers.Store(dispatcherConfig.ID, d)
|
||||
log.Debug().Int("id", dispatcherConfig.ID).Msg("Loaded dispatcher from state sync")
|
||||
m.dispatchers.Store(dc.ID, d)
|
||||
log.Debug().Int("id", dc.ID).Msg("Loaded dispatcher from state sync")
|
||||
}
|
||||
|
||||
// Update listener to start/stop streams based on new subscriptions
|
||||
@@ -532,6 +518,18 @@ func (m *Manager) HandleNotificationConfig(subscriptions []types.SubscriptionCon
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDispatcher creates a dispatcher from a DispatcherConfig
|
||||
func createDispatcher(config DispatcherConfig) (dispatcher.Dispatcher, error) {
|
||||
switch config.Type {
|
||||
case "webhook":
|
||||
return dispatcher.NewWebhookDispatcher(config.Name, config.URL, config.Template)
|
||||
case "cloud":
|
||||
return dispatcher.NewCloudDispatcher(config.Name, config.APIKey)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown dispatcher type: %s", config.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// loadSubscription loads a subscription with its existing ID (used when loading from config)
|
||||
func (m *Manager) loadSubscription(sub *Subscription) error {
|
||||
// Compile container expression if provided
|
||||
|
||||
@@ -114,9 +114,10 @@ func (s *Subscription) AddTriggeredContainer(id string) {
|
||||
type DispatcherConfig struct {
|
||||
ID int `json:"id" yaml:"id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"` // "webhook", etc.
|
||||
Type string `json:"type" yaml:"type"` // "webhook", "cloud"
|
||||
URL string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
Template string `json:"template,omitempty" yaml:"template,omitempty"` // Go template for custom payload format
|
||||
APIKey string `json:"apiKey,omitempty" yaml:"apiKey,omitempty"` // API key for cloud dispatcher
|
||||
}
|
||||
|
||||
// Config represents the persisted notification configuration
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/notification/dispatcher"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type exchangeTokenResponse struct {
|
||||
Key string `json:"key"`
|
||||
Prefix string `json:"prefix"`
|
||||
ExpiresAt *string `json:"expiresAt,omitempty"`
|
||||
}
|
||||
|
||||
func (h *handler) cloudCallback(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
http.Error(w, "missing token parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cloudURL := os.Getenv("DOLIGENCE_URL")
|
||||
if cloudURL == "" {
|
||||
cloudURL = "https://doligence.dozzle.dev"
|
||||
}
|
||||
|
||||
exchangeURL := fmt.Sprintf("%s/api/exchange-token", cloudURL)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
req, err := http.NewRequestWithContext(r.Context(), http.MethodPost, exchangeURL, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create request")
|
||||
http.Error(w, "failed to create request", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
q := req.URL.Query()
|
||||
q.Set("token", token)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to exchange token")
|
||||
http.Error(w, "failed to exchange token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||
log.Error().Int("status", resp.StatusCode).Str("body", string(body)).Msg("Token exchange failed")
|
||||
http.Error(w, "token exchange failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var tokenResp exchangeTokenResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to decode token response")
|
||||
http.Error(w, "failed to decode token response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenResp.Key == "" {
|
||||
log.Error().Msg("Empty key received")
|
||||
http.Error(w, "empty key received", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
name := "Dozzle Cloud"
|
||||
|
||||
cloudDispatcher, err := dispatcher.NewCloudDispatcher(name, tokenResp.Key)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create cloud dispatcher")
|
||||
http.Error(w, "failed to create cloud dispatcher", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
id := h.hostService.AddDispatcher(cloudDispatcher)
|
||||
|
||||
base := h.config.Base
|
||||
if base == "/" {
|
||||
base = ""
|
||||
}
|
||||
redirectURL := fmt.Sprintf("%s/notifications?newCloudLink=%d", base, id)
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
}
|
||||
@@ -170,6 +170,9 @@ func createRouter(h *handler) *chi.Mux {
|
||||
r.Post("/token", h.createToken)
|
||||
r.Delete("/token", h.deleteToken)
|
||||
}
|
||||
|
||||
// Cloud callback (public, handles OAuth-style code exchange)
|
||||
r.Get("/cloud/callback", h.cloudCallback)
|
||||
})
|
||||
|
||||
r.Get("/healthcheck", h.healthcheck)
|
||||
|
||||
@@ -224,6 +224,8 @@ notifications:
|
||||
cloud-description: Push, email, and dashboard
|
||||
webhook-url: Webhook URL
|
||||
webhook-url-placeholder: https://hooks.foo.com/services/...
|
||||
api-key: API Key
|
||||
api-key-placeholder: Enter your Dozzle Cloud API key
|
||||
payload-format: Payload Format
|
||||
format-slack: Slack
|
||||
format-discord: Discord
|
||||
@@ -236,3 +238,7 @@ notifications:
|
||||
cancel: Cancel
|
||||
save: Save
|
||||
add: Add Destination
|
||||
cloud-exists: Only one Dozzle Cloud destination can be configured
|
||||
link-cloud: Link Account
|
||||
link-cloud-button: Link Dozzle Cloud
|
||||
cloud-linked: Your Dozzle Cloud account is linked and ready to receive notifications.
|
||||
|
||||
@@ -49,4 +49,5 @@ type DispatcherConfig struct {
|
||||
Type string
|
||||
URL string
|
||||
Template string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import svgLoader from "vite-svg-loader";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig(() => ({
|
||||
define: {
|
||||
__CLOUD_URL__: JSON.stringify(process.env.CLOUD_URL || "https://cloud.dozzle.dev"),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@/": `${path.resolve(__dirname, "assets")}/`,
|
||||
|
||||
Reference in New Issue
Block a user