Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ComponentProps, FormEvent, useCallback, useMemo } from 'react'
import { Button, FormField, Input2, Modal } from '@pluralsh/design-system'

import { GqlError } from 'components/utils/Alert'
import { ModalMountTransition } from 'components/utils/ModalMountTransition'
import { Body2P } from 'components/utils/typography/Text'
import { useTheme } from 'styled-components'
Expand All @@ -17,10 +18,7 @@ import { appendConnection, updateCache } from 'utils/graphql'

import { sinkTypeToIcon } from './NotificationSinksColumns'

const hookUrlMatch = [
[SinkType.Slack, /^https:\/\/[^/]*?slack/],
[SinkType.Teams, /^https:\/\/[^/]*?office/],
] as const satisfies [SinkType, RegExp][]
const slackHookUrlRegex = /^https:\/\/[^/]*?slack/

type ModalBaseProps = {
mode: 'edit' | 'create'
Expand All @@ -39,35 +37,36 @@ function UpsertNotificationSinkModal({
...props
}: ModalProps) {
const sink = mode === 'edit' ? props.sink : undefined
const sinkName = sink?.name
const sinkType = sink?.type
const slackUrl = sink?.configuration.slack?.url
const teamsUrl = sink?.configuration.teams?.url
const theme = useTheme()
const initialState = useMemo(
() => ({
name: '',
hookUrl: '',
...(mode === 'edit'
? {
name: sink?.name,
hookUrl:
sink?.configuration.slack?.url || sink?.configuration.teams?.url,
name: sinkName,
hookUrl: slackUrl || teamsUrl,
}
: {}),
}),
[
mode,
sink?.configuration.slack?.url,
sink?.configuration.teams?.url,
sink?.name,
]
[mode, sinkName, slackUrl, teamsUrl]
)
const { state, update, hasUpdates } = useUpdateState<{
name: string
hookUrl: string
}>(initialState)
const hookType = hookUrlMatch.find(([_, regex]) =>
regex.test(state.hookUrl)
)?.[0]
const hookType =
mode === 'edit' && sinkType
? sinkType
: slackHookUrlRegex.test(state.hookUrl)
? SinkType.Slack
: SinkType.Teams

const [mutation, { loading }] = useUpsertNotificationSinkMutation({
const [mutation, { loading, error }] = useUpsertNotificationSinkMutation({
onCompleted: () => onClose?.(),
update: (cache, { data }) =>
updateCache(cache, {
Expand All @@ -81,7 +80,7 @@ function UpsertNotificationSinkModal({
}),
})

const allowSubmit = hookType && state.name && state.hookUrl && hasUpdates
const allowSubmit = state.name && state.hookUrl && hasUpdates

const onSubmit = useCallback(
(e: FormEvent) => {
Expand Down Expand Up @@ -166,6 +165,7 @@ function UpsertNotificationSinkModal({
</InlineLink>{' '}
webhook url to send this event alert to your team.
</Body2P>
{error && <GqlError error={error} />}
<FormField label={mode === 'edit' ? `Webhook url` : 'Add sink'}>
<div css={{ display: 'flex', gap: theme.spacing.xxsmall }}>
{mode !== 'edit' && (
Expand All @@ -178,7 +178,7 @@ function UpsertNotificationSinkModal({
)}
<Input2
value={state.hookUrl}
endIcon={sinkTypeToIcon[hookType || '']}
endIcon={state.hookUrl ? sinkTypeToIcon[hookType] : undefined}
onChange={(e) => update({ hookUrl: e.target.value })}
placeholder="https://hooks.provider.com/..."
css={{ flex: '1 1 100%' }}
Expand Down
52 changes: 50 additions & 2 deletions lib/console/schema/notification_sink.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ defmodule Console.Schema.NotificationSink do
end

@valid ~w(type name)a
@slack_webhook_hosts ~w(slack.com slack-gov.com)
@teams_webhook_hosts ~w(office.com office365.com powerautomate.com powerplatform.com logic.azure.com)

def changeset(model, attrs \\ %{}) do
model
Expand All @@ -57,8 +59,8 @@ defmodule Console.Schema.NotificationSink do
defp config_changeset(model, attrs) do
model
|> cast(attrs, [])
|> cast_embed(:slack, with: &url_changeset/2)
|> cast_embed(:teams, with: &url_changeset/2)
|> cast_embed(:slack, with: &slack_changeset/2)
|> cast_embed(:teams, with: &teams_changeset/2)
|> cast_embed(:plural, with: &plural_changeset/2)
end

Expand All @@ -68,6 +70,52 @@ defmodule Console.Schema.NotificationSink do
|> validate_required([:url])
end

defp slack_changeset(model, attrs) do
model
|> url_changeset(attrs)
|> validate_change(
:url,
&validate_webhook_url(&1, &2, @slack_webhook_hosts, "must be a valid Slack webhook URL")
)
end

defp teams_changeset(model, attrs) do
model
|> url_changeset(attrs)
|> validate_change(
:url,
&validate_webhook_url(
&1,
&2,
@teams_webhook_hosts,
"must be a valid Microsoft Teams webhook URL"
)
)
end

defp validate_webhook_url(:url, url, hosts, message) when is_binary(url) do
with {:ok, %URI{scheme: "https", host: host, userinfo: nil} = uri} when is_binary(host) <-
URI.new(url),
true <- valid_port?(uri.port),
true <- host_matches?(host, hosts) do
[]
else
_ -> [url: message]
end
end

defp validate_webhook_url(:url, _, _, message), do: [url: message]

defp valid_port?(port) when is_integer(port), do: port in 1..65_535
defp valid_port?(nil), do: true
defp valid_port?(_), do: false

defp host_matches?(host, hosts) do
host = String.downcase(host)

Enum.any?(hosts, &(host == &1 || String.ends_with?(host, ".#{&1}")))
end

defp plural_changeset(model, attrs) do
model
|> cast(attrs, ~w(priority urgent)a)
Expand Down
Loading