mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat/onboarding (#337)
* feat(onboarding): add cookie helper, types and mock data * feat(onboarding): add stepper, contextual preview and shell * feat(onboarding): add sso-gate step * feat(onboarding): add account-info step with real signup * feat(onboarding): add security step * feat(onboarding): add preferences step * fix(onboarding): validate avatar upload size/type and handle FileReader errors * feat(onboarding): add org-create step * feat(onboarding): add invite-members step * feat(onboarding): add notifier step reusing real provider list * feat(onboarding): add storage step reusing real provider list * feat(onboarding): add defaults step * feat(onboarding): add agent-create step * feat(onboarding): add agent-waiting step with simulated ping * feat(onboarding): add project-create step * feat(onboarding): add db-settings step with per-db repeat * feat(onboarding): add finish step with confetti * feat(onboarding): wire full step graph * feat(onboarding): add /welcome route * feat(onboarding): gate landing, auth and dashboard routes behind onboarding cookie * fix(onboarding): call next() in finish step so onFlowComplete fires and cookie is written * docs: add onboarding channel config + style refactor spec * docs: add conditional step skip when no agents to spec * docs(onboarding): add implementation plan for channel config flow and style refactor * feat(onboarding): extend OnboardingChannel with name and config fields * feat(onboarding): add two-phase config flow and org-combobox style to notifier step * fix(onboarding): prevent duplicate notifier providers and guard Continue button * feat(onboarding): add two-phase config flow and org-combobox style to storage step * style(onboarding): restyle sso-gate provider buttons to org-combobox card style * style(onboarding): restyle security method choice to org-combobox card style * style(onboarding): restyle theme toggles in preferences step to org-combobox card style * style(onboarding): restyle DB toggle buttons in project-create step to org-combobox card style * feat(onboarding): skip agent-waiting/project-create/db-settings when no agents created * feat(onboarding): update type system — meta, firstName/lastName, org.id, databases * feat(onboarding): add settings.onboarding column + markOnboardingDoneAction * feat(onboarding): add resolveOnboardingState server function * fix(onboarding): remove unused imports from onboarding-state.ts * style(onboarding): add branding to shell, fix progress bar full width * feat(onboarding): replace preview panel with stepper checklist * feat(onboarding): add step-login replacing sso-gate * fix(onboarding): step-login — conform to global mutation pattern, handle SSO errors * feat(onboarding): step-account-info — firstName/lastName, conditional password, findOrCreate * feat(onboarding): avatar API route (next/og) + AvatarPicker in preferences step * feat(onboarding): step-security reads passkeyEnabled from flowData.meta * feat(onboarding): step-org-create calls real createOrganizationAction * fix(onboarding): step-org-create — simplify error handling, throw on empty name * feat(onboarding): notifier + storage steps call real actions + findOrCreate on back * feat(onboarding): step-agent-create calls real createAgentAction + findOrCreate * feat(onboarding): step-agent-waiting polls real agent lastContact via useQuery * feat(onboarding): add step-agent-key with edgeKey generation via server action * feat(onboarding): step-project-create uses real action + real DBs from flowData * feat(onboarding): wire steps — add login + agent-key, remove sso-gate * feat(onboarding): page.tsx uses resolveOnboardingState — server-driven resume * chore(onboarding): delete mock/cookie/preview/sso-gate files — replaced by real implementation * feat(onboarding): add emailPasswordEnabled to meta, remove logoDataUrl from org * feat(onboarding): DB-driven resume — query notifiers/storages, check agent lastContact * feat(onboarding): agent-waiting step is now skippable * feat(onboarding): login — auto-advance if session, callbackURL on SSO, hide email/pw when disabled, empty state * fix(onboarding): step-login — add next to useEffect deps, default emailPasswordEnabled to false * fix(onboarding): verify session after passkey registration, fallback to signIn.passkey() * feat(onboarding): theme applied dynamically via setTheme, avatar saved to user.image * feat(onboarding): remove org logo upload — orgs have no logo * feat(onboarding): show Next button in shell when navigating backwards * docs: add onboarding refactor design spec * docs: add onboarding refactor implementation plan * refactor(onboarding): extract useCreateAgent and useDeleteAgent hooks * refactor(onboarding): extract useCreateOrg and useCreateProject hooks * fix(onboarding): trim project name, standardize error narrowing in org/project hooks * fix(onboarding): use narrowed createData.value in use-create-org * refactor(onboarding): extract useUpdateAccount hook * refactor(onboarding): extract useAddNotifier and useRemoveNotifier hooks * refactor(onboarding): extract useAddStorage and useRemoveStorage hooks * refactor(onboarding): extract useAgentStatus and useGenerateEdgeKey hooks * refactor(onboarding): extract useMarkOnboardingDone hook, remove .gitkeep files * docs: add onboarding theme persistence & progress bar fix spec * docs: add onboarding theme & progress bar implementation plan * fix(onboarding): persist theme to db on selection in StepPreferences * fix(onboarding): recalculate progress bar from full STEP_ORDER position * fix(theme): sync user theme from DB on all routes, not only dashboard * fix(theme): use current theme as fallback in preferences, prevent session override of localStorage * fix(onboarding): correct resume steps and skip agent-waiting flash when agent is connected * fix(onboarding): properly resolve onboarding state, remove redirect trap in step-defaults, block waiting flash * fix(onboarding): resume at db-settings instead of finish if no database exists for the project * fix(onboarding): redirect to agent-create if project exists but no agent is found * fix(onboarding): allow going from agent-create to project-create if a project already exists * fix(onboarding): skip intermediate agent steps when going back from project-create to agent-create * fix(onboarding): skip default step when going back from agent-create if no notifiers and storages exist * feat(onboarding): allow user to choose between passkey and password during registration if both are enabled * feat(onboarding): replace tabs with single form and multiple buttons for auth choice * style(onboarding): improve empty auth state UI with lucide icon and better layout * fix(onboarding): completely hide back button on the first step instead of disabling it * fix(onboarding): skip default step if no notifiers and storages exist when advancing from storage step * fix(onboarding): correctly show select placeholder when default value does not exist in the list * refactor(onboarding): remove duplicate generate-edge-key action and use core utility directly * feat(onboarding): compute edgeKey directly on the server side to avoid lazy loading * fix(onboarding): skip db-settings step if no database is found on the agent * fix(onboarding): do not skip db settings if agent is connected but no databases found * refactor(onboarding): skip db settings entirely if no database is selected in project create * feat(onboarding): auto-save database selections on project create instantly * style(onboarding): replace button opacity flash with specific spinner animation on db select * feat(onboarding): extend OnboardingDbSettings with retention, schedule, and policy fields * feat(onboarding): add applyOnboardingDbSettingsAction server action * feat(onboarding): add BackupScheduleSelector component * feat(onboarding): rewrite step-db-settings with three-level phase navigation and full policy persistence * fix(onboarding): wire applyMutation.mutateAsync everywhere and stabilize list keys * docs(sdd): append hotfix summary for applyMutation and list keys * fix(onboarding): address final review findings - atomicity, scheduling guard, apply-all recovery, client validation * add: onboarding * fix(migration): merge * fix(migration): apply for onboarding * fix(settings): make avatar selector, dicebear picker and tab bar responsive * chore: checkpoint before features reorganization * Task 2: Add migration script for features directory reorganization * refactor(agents): reorganize into actions/components/schemas/hooks * refactor(auth): reorganize into actions/components/schemas * refactor(channel): reorganize into components/schemas, nest notifications+storages under components * refactor(database): reorganize into actions/components/schemas * refactor(layout): move all components to components/ * refactor(logs): reorganize into components/types * refactor(migration): reorganize into actions/components * refactor(notifications): reorganize into components/types/utils * refactor(organizations): reorganize into actions/components/schemas/hooks/utils * refactor(profile): reorganize into actions/components/schemas * refactor(projects): reorganize into actions/components/schemas * refactor(settings): reorganize into actions/components/schemas/hooks * refactor(statistics): reorganize into components/utils * refactor(storages): reorganize into types/utils * refactor(theme): move all components to components/ * refactor(updates): reorganize into components/hooks * refactor(upload): move action to actions/ * refactor(users): reorganize into actions/components/schemas * fix: update broken imports after features reorganization (double-quote relative imports, channel subdir absolute paths) * fix: .gitignore * fix: onboarding * fix: removed dead files * feat: backfill onboarding flag for instances with existing data * fix: persist onboarding=true on boot when SKIP_ONBOARDING is set * feat: remove avatarMode/dicebearStyle from DB schema and types * feat: simplify resolveAvatarUrl — custom image first, gravatar fallback * feat: allow avatar upload regardless of mode * feat: remove avatarMode from layout and profile prop chains * feat: remove avatar mode admin tab and supporting files * feat: remove avatar mode selector from onboarding defaults step * fix: remove preferences onboarding step and fix routing * fix: remove stale preferences entry from onboarding checklist * feat: add avatar reset button and explanatory tooltip on profile page * fix: move avatar tooltip to top-right badge overlay on avatar * fix: use small destructive button for avatar removal * fix * Update step-defaults.tsx * fix: default dev password for keycloak refer to env * fix * fix(migration): refactoring * fix: api agent helpers.ts * Update .env.example --------- Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com> Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
This commit is contained in:
co-authored by
Théo LAGACHE
charles-gauthereau
parent
40d3228db6
commit
40247efddd
@@ -1,88 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Card} from "@/components/ui/card";
|
||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {truncateWords} from "@/utils/text";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||
import {
|
||||
EditChannelButton
|
||||
} from "@/features/channel/channel-edit-button";
|
||||
import {ChannelKind, getChannelIcon} from "@/features/channel/channels-helpers";
|
||||
import {
|
||||
DeleteChannelButton
|
||||
} from "@/features/channel/channel-delete-button";
|
||||
|
||||
export type ChannelCardProps = {
|
||||
data: NotificationChannelWith | StorageChannelWith;
|
||||
organization?: OrganizationWithMembers;
|
||||
organizations?: OrganizationWithMembers[];
|
||||
adminView?: boolean;
|
||||
kind?: ChannelKind;
|
||||
defaultStorageChannelId?: string | null | undefined
|
||||
|
||||
};
|
||||
|
||||
|
||||
export const ChannelCard = (props: ChannelCardProps) => {
|
||||
const {data, organization, kind, adminView, defaultStorageChannelId} = props;
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const isDefaultSystemStorage = defaultStorageChannelId === data.id;
|
||||
|
||||
const isOwned = data.organizationId ? true : !organization;
|
||||
const isLocalSystem = data.provider == "local";
|
||||
|
||||
return (
|
||||
<div className="block transition-all duration-200 rounded-xl">
|
||||
<Card className="flex flex-row justify-between p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="flex h-10 w-10 items-center justify-center rounded-md bg-secondary border border-border">
|
||||
{getChannelIcon(data.provider)}
|
||||
</div>
|
||||
<div className={`h-2 w-2 rounded-full ${data.enabled ? "bg-green-600" : "bg-muted"}`}/>
|
||||
</div>
|
||||
<div className="flex justify-start w-full">
|
||||
<div className="flex flex-col items-start md:flex-row md:items-center gap-2 ">
|
||||
<h3 className="font-medium text-foreground">{isMobile ? truncateWords(data.name, 2) : data.name}</h3>
|
||||
<Badge variant="secondary" className="text-xs font-mono">
|
||||
{data.provider}
|
||||
</Badge>
|
||||
{isDefaultSystemStorage && (
|
||||
<Badge variant="secondary" className="text-xs font-mono">
|
||||
default
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{kind && (
|
||||
<div className="flex items-center gap-2">
|
||||
{(isOwned) && (
|
||||
<>
|
||||
<EditChannelButton
|
||||
organizations={props.organizations}
|
||||
adminView={props.adminView}
|
||||
organization={organization}
|
||||
channel={data}
|
||||
kind={kind}
|
||||
/>
|
||||
{!isLocalSystem && (
|
||||
<DeleteChannelButton
|
||||
kind={kind}
|
||||
organizationId={organization?.id}
|
||||
channelId={data.id}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -12,11 +12,11 @@ import {useIsMobile} from "@/hooks/use-mobile";
|
||||
import {useEffect, useState} from "react";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||
import {ChannelForm} from "@/features/channel/channel-form";
|
||||
import {ChannelKind, getChannelTextBasedOnKind} from "@/features/channel/channels-helpers";
|
||||
import {ChannelForm} from "@/features/channel/components/channel-form";
|
||||
import {ChannelKind, getChannelTextBasedOnKind} from "@/features/channel/components/channels-helpers";
|
||||
import {
|
||||
ChannelOrganisationForm
|
||||
} from "@/features/organizations/channels-organization-form";
|
||||
} from "@/features/organizations/components/channels-organization-form";
|
||||
|
||||
|
||||
type ChannelAddModalProps = {
|
||||
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { NotificationChannelWith } from "@/db/schema/09_notification-channel";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { OrganizationWithMembers } from "@/db/schema/03_organization";
|
||||
import { truncateWords } from "@/utils/text";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { StorageChannelWith } from "@/db/schema/12_storage-channel";
|
||||
import { EditChannelButton } from "@/features/channel/components/channel-edit-button";
|
||||
import {
|
||||
ChannelKind,
|
||||
getChannelIcon,
|
||||
} from "@/features/channel/components/channels-helpers";
|
||||
import { DeleteChannelButton } from "@/features/channel/components/channel-delete-button";
|
||||
|
||||
export type ChannelCardProps = {
|
||||
data: NotificationChannelWith | StorageChannelWith;
|
||||
organization?: OrganizationWithMembers;
|
||||
organizations?: OrganizationWithMembers[];
|
||||
adminView?: boolean;
|
||||
kind?: ChannelKind;
|
||||
defaultStorageChannelId?: string | null | undefined;
|
||||
};
|
||||
|
||||
export const ChannelCard = (props: ChannelCardProps) => {
|
||||
const { data, organization, kind, defaultStorageChannelId } = props;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const isDefaultSystemStorage = defaultStorageChannelId === data.id;
|
||||
|
||||
const isOwned = data.organizationId ? true : !organization;
|
||||
const isLocalSystem = data.provider === "local";
|
||||
|
||||
return (
|
||||
<div className="block transition-all duration-200 rounded-xl">
|
||||
<Card className="flex flex-row justify-between p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-secondary border border-border">
|
||||
{getChannelIcon(data.provider)}
|
||||
</div>
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${data.enabled ? "bg-green-600" : "bg-muted"}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-start w-full">
|
||||
<div className="flex flex-col items-start md:flex-row md:items-center gap-2 ">
|
||||
<h3 className="font-medium text-foreground">
|
||||
{isMobile ? truncateWords(data.name, 2) : data.name}
|
||||
</h3>
|
||||
<Badge variant="secondary" className="text-xs font-mono">
|
||||
{data.provider}
|
||||
</Badge>
|
||||
{isDefaultSystemStorage && (
|
||||
<Badge variant="secondary" className="text-xs font-mono">
|
||||
default
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{kind && (
|
||||
<div className="flex items-center gap-2">
|
||||
{isOwned && (
|
||||
<>
|
||||
<EditChannelButton
|
||||
organizations={props.organizations}
|
||||
adminView={props.adminView}
|
||||
organization={organization}
|
||||
channel={data}
|
||||
kind={kind}
|
||||
/>
|
||||
{!isLocalSystem && (
|
||||
<DeleteChannelButton
|
||||
kind={kind}
|
||||
organizationId={organization?.id}
|
||||
channelId={data.id}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+3
-3
@@ -5,12 +5,12 @@ import {useRouter} from "next/navigation";
|
||||
import {Trash2} from "lucide-react";
|
||||
import {
|
||||
removeNotificationChannelAction,
|
||||
} from "@/features/channel/notifications/channel.action";
|
||||
} from "@/features/channel/components/notifications/channel.action";
|
||||
import {toast} from "sonner";
|
||||
import {ChannelKind, getChannelTextBasedOnKind} from "@/features/channel/channels-helpers";
|
||||
import {ChannelKind, getChannelTextBasedOnKind} from "@/features/channel/components/channels-helpers";
|
||||
import {
|
||||
removeStorageChannelAction
|
||||
} from "@/features/channel/storages/channel.action";
|
||||
} from "@/features/channel/components/storages/channel.action";
|
||||
|
||||
export type DeleteChannelButtonProps = {
|
||||
channelId: string;
|
||||
+4
-4
@@ -7,14 +7,14 @@ import {toast} from "sonner";
|
||||
import {useState} from "react";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||
import {ChannelKind} from "@/features/channel/channels-helpers";
|
||||
import {ChannelKind} from "@/features/channel/components/channels-helpers";
|
||||
import {
|
||||
updateNotificationChannelAction
|
||||
} from "@/features/channel/notifications/channel.action";
|
||||
import {ChannelAddEditModal} from "@/features/channel/channel-add-edit-modal";
|
||||
} from "@/features/channel/components/notifications/channel.action";
|
||||
import {ChannelAddEditModal} from "@/features/channel/components/channel-add-edit-modal";
|
||||
import {
|
||||
updateStorageChannelAction
|
||||
} from "@/features/channel/storages/channel.action";
|
||||
} from "@/features/channel/components/storages/channel.action";
|
||||
|
||||
export type EditChannelButtonProps = {
|
||||
channel: NotificationChannelWith | StorageChannelWith;
|
||||
+7
-7
@@ -8,7 +8,7 @@ import {Input} from "@/components/ui/input";
|
||||
|
||||
import {
|
||||
addStorageChannelAction, updateStorageChannelAction
|
||||
} from "@/features/channel/storages/channel.action";
|
||||
} from "@/features/channel/components/storages/channel.action";
|
||||
import {toast} from "sonner";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {Button} from "@/components/ui/button";
|
||||
@@ -20,19 +20,19 @@ import {ArrowLeft} from "lucide-react";
|
||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||
import {
|
||||
NotificationChannelFormSchema, NotificationChannelFormType, StorageChannelFormSchema, StorageChannelFormType
|
||||
} from "@/features/channel/channel-form.schema";
|
||||
} from "@/features/channel/schemas/channel-form.schema";
|
||||
import {
|
||||
ChannelKind,
|
||||
renderChannelForm
|
||||
} from "@/features/channel/channels-helpers";
|
||||
import {storageProviders} from "@/features/channel/channels-storage-helper";
|
||||
import {notificationProviders} from "@/features/channel/channels-notification-helper";
|
||||
} from "@/features/channel/components/channels-helpers";
|
||||
import {storageProviders} from "@/features/channel/components/channels-storage-helper";
|
||||
import {notificationProviders} from "@/features/channel/components/channels-notification-helper";
|
||||
import {
|
||||
ChannelTestButton
|
||||
} from "@/features/channel/channel-test-button";
|
||||
} from "@/features/channel/components/channel-test-button";
|
||||
import {
|
||||
addNotificationChannelAction, updateNotificationChannelAction
|
||||
} from "@/features/channel/notifications/channel.action";
|
||||
} from "@/features/channel/components/notifications/channel.action";
|
||||
|
||||
|
||||
type NotifierFormProps = {
|
||||
+5
-5
@@ -6,14 +6,14 @@ import {toast} from "sonner";
|
||||
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {dispatchNotification} from "@/features/notifications/notifications.dispatch";
|
||||
import {EventPayload} from "@/features/notifications/notifications.types";
|
||||
import {dispatchNotification} from "@/features/notifications/utils/notifications.dispatch";
|
||||
import {EventPayload} from "@/features/notifications/types";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {StorageChannel} from "@/db/schema/12_storage-channel";
|
||||
import {ChannelKind} from "@/features/channel/channels-helpers";
|
||||
import type {StorageInput} from "@/features/storages/storages.types";
|
||||
import {dispatchStorage} from "@/features/storages/storages.dispatch";
|
||||
import {ChannelKind} from "@/features/channel/components/channels-helpers";
|
||||
import type {StorageInput} from "@/features/storages/types";
|
||||
import {dispatchStorage} from "@/features/storages/utils/storages.dispatch";
|
||||
|
||||
|
||||
type NotifierTestChannelButtonProps = {
|
||||
+15
-15
@@ -1,49 +1,49 @@
|
||||
import {UseFormReturn} from "react-hook-form";
|
||||
import {
|
||||
NotifierSmtpForm
|
||||
} from "@/features/channel/notifications/smtp.form";
|
||||
} from "@/features/channel/components/notifications/smtp.form";
|
||||
import {
|
||||
NotifierSlackForm
|
||||
} from "@/features/channel/notifications/slack.form";
|
||||
} from "@/features/channel/components/notifications/slack.form";
|
||||
import {
|
||||
NotifierDiscordForm
|
||||
} from "@/features/channel/notifications/discord.form";
|
||||
} from "@/features/channel/components/notifications/discord.form";
|
||||
import {
|
||||
NotifierTelegramForm
|
||||
} from "@/features/channel/notifications/telegram.form";
|
||||
} from "@/features/channel/components/notifications/telegram.form";
|
||||
import {
|
||||
NotifierGotifyForm
|
||||
} from "@/features/channel/notifications/gotify.form";
|
||||
} from "@/features/channel/components/notifications/gotify.form";
|
||||
import {
|
||||
NotifierNtfyForm
|
||||
} from "@/features/channel/notifications/ntfy.form";
|
||||
} from "@/features/channel/components/notifications/ntfy.form";
|
||||
import {
|
||||
NotifierWebhookForm
|
||||
} from "@/features/channel/notifications/webhook.form";
|
||||
} from "@/features/channel/components/notifications/webhook.form";
|
||||
import {
|
||||
NotifierNextcloudForm
|
||||
} from "@/features/channel/notifications/nextcloud.form";
|
||||
} from "@/features/channel/components/notifications/nextcloud.form";
|
||||
import {
|
||||
NotifierPushoverForm
|
||||
} from "@/features/channel/notifications/pushover.form";
|
||||
} from "@/features/channel/components/notifications/pushover.form";
|
||||
import {
|
||||
notificationProviders,
|
||||
} from "@/features/channel/channels-notification-helper";
|
||||
} from "@/features/channel/components/channels-notification-helper";
|
||||
import {
|
||||
NotifierTeamsForm
|
||||
} from "@/features/channel/notifications/teams.form";
|
||||
import {storageProviders} from "@/features/channel/channels-storage-helper";
|
||||
} from "@/features/channel/components/notifications/teams.form";
|
||||
import {storageProviders} from "@/features/channel/components/channels-storage-helper";
|
||||
import {ForwardRefExoticComponent, JSX, RefAttributes, SVGProps} from "react";
|
||||
import {LucideProps} from "lucide-react";
|
||||
import {
|
||||
StorageS3Form
|
||||
} from "@/features/channel/storages/s3.form";
|
||||
} from "@/features/channel/components/storages/s3.form";
|
||||
import {
|
||||
StorageGoogleDriveForm
|
||||
} from "@/features/channel/storages/google-drive/google-drive.form";
|
||||
} from "@/features/channel/components/storages/google-drive/google-drive.form";
|
||||
import {
|
||||
StorageBlobForm
|
||||
} from "@/features/channel/storages/az-blob.form";
|
||||
} from "@/features/channel/components/storages/az-blob.form";
|
||||
|
||||
export type ChannelKind = "notification" | "storage";
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import {Mail} from "lucide-react";
|
||||
import type { SVGProps } from 'react';
|
||||
import {ProviderIconTypes} from "@/features/channel/channels-helpers";
|
||||
import {ProviderIconTypes} from "@/features/channel/components/channels-helpers";
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -5,12 +5,12 @@ import { useState } from "react";
|
||||
import { EmptyStatePlaceholder } from "@/components/common/empty-state-placeholder";
|
||||
import { OrganizationWithMembers } from "@/db/schema/03_organization";
|
||||
import { StorageChannelWith } from "@/db/schema/12_storage-channel";
|
||||
import { ChannelCard } from "@/features/channel/channel-card";
|
||||
import { ChannelAddEditModal } from "@/features/channel/channel-add-edit-modal";
|
||||
import { ChannelCard } from "@/features/channel/components/channel-card";
|
||||
import { ChannelAddEditModal } from "@/features/channel/components/channel-add-edit-modal";
|
||||
import {
|
||||
ChannelKind,
|
||||
getChannelTextBasedOnKind,
|
||||
} from "@/features/channel/channels-helpers";
|
||||
} from "@/features/channel/components/channels-helpers";
|
||||
|
||||
type ChannelsSectionProps = {
|
||||
channels: NotificationChannelWith[] | StorageChannelWith[];
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import {Server} from "lucide-react";
|
||||
import type {SVGProps} from "react";
|
||||
import {ProviderIconTypes} from "@/features/channel/channels-helpers";
|
||||
import {ProviderIconTypes} from "@/features/channel/components/channels-helpers";
|
||||
|
||||
export const storageProviders: ProviderIconTypes[] = [
|
||||
{value: "local", label: "Local", icon: Server},
|
||||
@@ -0,0 +1,200 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { ServerActionResult } from "@/types/action-type";
|
||||
import * as drizzleDb from "@/db";
|
||||
import { userAction } from "@/lib/safe-actions/actions";
|
||||
import { NotificationChannel } from "@/db/schema/09_notification-channel";
|
||||
import { db } from "@/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { withUpdatedAt } from "@/db/utils";
|
||||
import { NotificationChannelFormSchema } from "@/features/channel/schemas/channel-form.schema";
|
||||
|
||||
export const addNotificationChannelAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
organizationId: z.string().optional(),
|
||||
data: NotificationChannelFormSchema,
|
||||
}),
|
||||
)
|
||||
.action(
|
||||
async ({
|
||||
parsedInput,
|
||||
}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||
const { organizationId, data } = parsedInput;
|
||||
try {
|
||||
const [channel] = await db
|
||||
.insert(drizzleDb.schemas.notificationChannel)
|
||||
.values({
|
||||
provider: data.provider,
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
enabled: data.enabled ?? true,
|
||||
organizationId: organizationId ?? null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (organizationId) {
|
||||
await db
|
||||
.insert(drizzleDb.schemas.organizationNotificationChannel)
|
||||
.values({
|
||||
organizationId,
|
||||
notificationChannelId: channel.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: {
|
||||
...channel,
|
||||
config: channel.config as JSON,
|
||||
},
|
||||
actionSuccess: {
|
||||
message: "Notification channel has been successfully created.",
|
||||
messageParams: { notificationChannelId: channel.id },
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error;
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to create notification channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { notificationChannelId: "" },
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const removeNotificationChannelAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
organizationId: z.string().optional(),
|
||||
notificationChannelId: z.string(),
|
||||
}),
|
||||
)
|
||||
.action(
|
||||
async ({
|
||||
parsedInput,
|
||||
}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||
const { organizationId, notificationChannelId } = parsedInput;
|
||||
|
||||
try {
|
||||
if (organizationId) {
|
||||
await db
|
||||
.delete(drizzleDb.schemas.organizationNotificationChannel)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
drizzleDb.schemas.organizationNotificationChannel
|
||||
.organizationId,
|
||||
organizationId,
|
||||
),
|
||||
eq(
|
||||
drizzleDb.schemas.organizationNotificationChannel
|
||||
.notificationChannelId,
|
||||
notificationChannelId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const [deletedChannel] = await db
|
||||
.delete(drizzleDb.schemas.notificationChannel)
|
||||
.where(
|
||||
eq(drizzleDb.schemas.notificationChannel.id, notificationChannelId),
|
||||
)
|
||||
.returning();
|
||||
|
||||
if (!deletedChannel) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Notification channel not found.",
|
||||
status: 404,
|
||||
messageParams: { notificationChannelId: notificationChannelId },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: {
|
||||
...deletedChannel,
|
||||
config: deletedChannel.config as JSON,
|
||||
},
|
||||
actionSuccess: {
|
||||
message: "Notification channel has been successfully removed.",
|
||||
messageParams: { notificationChannelId: notificationChannelId },
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error;
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to remove notification channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { notificationChannelId: notificationChannelId },
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const updateNotificationChannelAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
data: NotificationChannelFormSchema,
|
||||
}),
|
||||
)
|
||||
.action(
|
||||
async ({
|
||||
parsedInput,
|
||||
}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||
const { id, data } = parsedInput;
|
||||
|
||||
try {
|
||||
const [channel] = await db
|
||||
.update(drizzleDb.schemas.notificationChannel)
|
||||
.set(
|
||||
withUpdatedAt({
|
||||
provider: data.provider,
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
enabled: data.enabled ?? true,
|
||||
}),
|
||||
)
|
||||
.where(eq(drizzleDb.schemas.notificationChannel.id, id))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: {
|
||||
...channel,
|
||||
config: channel.config as JSON,
|
||||
},
|
||||
actionSuccess: {
|
||||
message: `Notification channel "${channel.name}" has been successfully updated.`,
|
||||
messageParams: { id: channel.id },
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error;
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update notification channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { id: "" },
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type {EventPayload, DispatchResult} from '@/features/notifications/notifications.types';
|
||||
import type {EventPayload, DispatchResult} from '@/features/notifications/types';
|
||||
|
||||
export async function sendDiscord(
|
||||
config: { discordWebhook: string },
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type {EventPayload, DispatchResult} from '@/features/notifications/notifications.types';
|
||||
import type {EventPayload, DispatchResult} from '@/features/notifications/types';
|
||||
|
||||
export async function sendGotify(
|
||||
config: { gotifyServerUrl: string; gotifyAppToken: string },
|
||||
+9
-9
@@ -1,15 +1,15 @@
|
||||
"use server"
|
||||
import type {ProviderKind, EventPayload, DispatchResult} from '@/features/notifications/notifications.types';
|
||||
import type {ProviderKind, EventPayload, DispatchResult} from '@/features/notifications/types';
|
||||
import {sendSlack} from './slack';
|
||||
import {sendSmtp} from './smtp';
|
||||
import {sendDiscord} from "@/features/channel/notifications/discord";
|
||||
import {sendTelegram} from "@/features/channel/notifications/telegram";
|
||||
import {sendGotify} from "@/features/channel/notifications/gotify";
|
||||
import {sendNtfy} from "@/features/channel/notifications/ntfy";
|
||||
import {sendWebhook} from "@/features/channel/notifications/webhook";
|
||||
import {sendNextcloud} from "@/features/channel/notifications/nextcloud";
|
||||
import {sendPushover} from "@/features/channel/notifications/pushover";
|
||||
import {sendTeams} from "@/features/channel/notifications/teams"
|
||||
import {sendDiscord} from "@/features/channel/components/notifications/discord";
|
||||
import {sendTelegram} from "@/features/channel/components/notifications/telegram";
|
||||
import {sendGotify} from "@/features/channel/components/notifications/gotify";
|
||||
import {sendNtfy} from "@/features/channel/components/notifications/ntfy";
|
||||
import {sendWebhook} from "@/features/channel/components/notifications/webhook";
|
||||
import {sendNextcloud} from "@/features/channel/components/notifications/nextcloud";
|
||||
import {sendPushover} from "@/features/channel/components/notifications/pushover";
|
||||
import {sendTeams} from "@/features/channel/components/notifications/teams"
|
||||
|
||||
const handlers: Record<
|
||||
ProviderKind,
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { createHmac, randomBytes } from "crypto";
|
||||
import type { EventPayload, DispatchResult } from '@/features/notifications/notifications.types';
|
||||
import type { EventPayload, DispatchResult } from '@/features/notifications/types';
|
||||
|
||||
type NextcloudConfig = {
|
||||
nextcloudUrl: string;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type {EventPayload, DispatchResult} from '@/features/notifications/notifications.types';
|
||||
import type {EventPayload, DispatchResult} from '@/features/notifications/types';
|
||||
|
||||
const getPriority = (level?: string): number => {
|
||||
switch (level) {
|
||||
+1
-1
@@ -4,7 +4,7 @@ import {Input} from "@/components/ui/input";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
import {PUSHOVER_PRIORITIES} from "@/features/channel/notifications/pushover.schema";
|
||||
import {PUSHOVER_PRIORITIES} from "@/features/channel/components/notifications/pushover.schema";
|
||||
|
||||
type NotifierPushoverFormProps = {
|
||||
form: UseFormReturn<any, any, any>;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type {EventPayload, DispatchResult} from '@/features/notifications/notifications.types';
|
||||
import type {EventPayload, DispatchResult} from '@/features/notifications/types';
|
||||
|
||||
export async function sendPushover(
|
||||
config: {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type {EventPayload, DispatchResult} from '@/features/notifications/notifications.types';
|
||||
import type {EventPayload, DispatchResult} from '@/features/notifications/types';
|
||||
|
||||
export async function sendSlack(
|
||||
config: { slackWebhook: string },
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
import { render } from "@react-email/render";
|
||||
import nodemailer from "nodemailer";
|
||||
import type { EventPayload, DispatchResult } from '@/features/notifications/notifications.types';
|
||||
import type { EventPayload, DispatchResult } from '@/features/notifications/types';
|
||||
import EmailNotification from "@/components/emails/email-notification";
|
||||
|
||||
export async function sendSmtp(
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { EventPayload, DispatchResult } from '@/features/notifications/notifications.types';
|
||||
import type { EventPayload, DispatchResult } from '@/features/notifications/types';
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { EventPayload, DispatchResult } from '@/features/notifications/notifications.types';
|
||||
import type { EventPayload, DispatchResult } from '@/features/notifications/types';
|
||||
|
||||
export async function sendTelegram(
|
||||
config: {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { EventPayload, DispatchResult } from '@/features/notifications/notifications.types';
|
||||
import type { EventPayload, DispatchResult } from '@/features/notifications/types';
|
||||
|
||||
type WebhookConfig = {
|
||||
webhookUrl: string;
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {
|
||||
StorageMetaData,
|
||||
StorageResult,
|
||||
StorageUploadInput
|
||||
} from '@/features/storages/storages.types';
|
||||
} from '@/features/storages/types';
|
||||
import {Readable} from "node:stream";
|
||||
|
||||
type BlobConfig = {
|
||||
@@ -0,0 +1,189 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { ServerActionResult } from "@/types/action-type";
|
||||
import * as drizzleDb from "@/db";
|
||||
import { userAction } from "@/lib/safe-actions/actions";
|
||||
import { db } from "@/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { withUpdatedAt } from "@/db/utils";
|
||||
import { StorageChannelFormSchema } from "@/features/channel/schemas/channel-form.schema";
|
||||
import { StorageChannel } from "@/db/schema/12_storage-channel";
|
||||
|
||||
export const addStorageChannelAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
organizationId: z.string().optional(),
|
||||
data: StorageChannelFormSchema,
|
||||
}),
|
||||
)
|
||||
.action(
|
||||
async ({ parsedInput }): Promise<ServerActionResult<StorageChannel>> => {
|
||||
const { organizationId, data } = parsedInput;
|
||||
|
||||
try {
|
||||
const [channel] = await db
|
||||
.insert(drizzleDb.schemas.storageChannel)
|
||||
.values({
|
||||
provider: data.provider,
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
enabled: data.enabled ?? true,
|
||||
organizationId: organizationId ?? null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (organizationId) {
|
||||
await db.insert(drizzleDb.schemas.organizationStorageChannel).values({
|
||||
organizationId,
|
||||
storageChannelId: channel.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: {
|
||||
...channel,
|
||||
config: channel.config as JSON,
|
||||
},
|
||||
actionSuccess: {
|
||||
message: "Storage channel has been successfully created.",
|
||||
messageParams: { id: channel.id },
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error;
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to create storage channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { id: "" },
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const removeStorageChannelAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
organizationId: z.string().optional(),
|
||||
id: z.string(),
|
||||
}),
|
||||
)
|
||||
.action(
|
||||
async ({ parsedInput }): Promise<ServerActionResult<StorageChannel>> => {
|
||||
const { organizationId, id } = parsedInput;
|
||||
|
||||
try {
|
||||
if (organizationId) {
|
||||
await db
|
||||
.delete(drizzleDb.schemas.organizationStorageChannel)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
drizzleDb.schemas.organizationStorageChannel.organizationId,
|
||||
organizationId,
|
||||
),
|
||||
eq(
|
||||
drizzleDb.schemas.organizationStorageChannel.storageChannelId,
|
||||
id,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const [deletedChannel] = await db
|
||||
.delete(drizzleDb.schemas.storageChannel)
|
||||
.where(eq(drizzleDb.schemas.storageChannel.id, id))
|
||||
.returning();
|
||||
|
||||
if (!deletedChannel) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Storage channel not found.",
|
||||
status: 404,
|
||||
messageParams: { id: id },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: {
|
||||
...deletedChannel,
|
||||
config: deletedChannel.config as JSON,
|
||||
},
|
||||
actionSuccess: {
|
||||
message: "Storage channel has been successfully removed.",
|
||||
messageParams: { id: id },
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error;
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to remove storage channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { id: id },
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const updateStorageChannelAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
data: StorageChannelFormSchema,
|
||||
}),
|
||||
)
|
||||
.action(
|
||||
async ({ parsedInput }): Promise<ServerActionResult<StorageChannel>> => {
|
||||
const { id, data } = parsedInput;
|
||||
|
||||
try {
|
||||
const [channel] = await db
|
||||
.update(drizzleDb.schemas.storageChannel)
|
||||
.set(
|
||||
withUpdatedAt({
|
||||
provider: data.provider,
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
enabled: data.enabled ?? true,
|
||||
}),
|
||||
)
|
||||
.where(eq(drizzleDb.schemas.storageChannel.id, id))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: {
|
||||
...channel,
|
||||
config: channel.config as JSON,
|
||||
},
|
||||
actionSuccess: {
|
||||
message: `Storage channel "${channel.name}" has been successfully updated.`,
|
||||
messageParams: { id: channel.id },
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error;
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update storage channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { id: "" },
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
+1
-1
@@ -8,7 +8,7 @@ import {Button} from "@/components/ui/button";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import {
|
||||
googleDriveRefreshTokenAction
|
||||
} from "@/features/channel/storages/google-drive/google-drive-refresh.action";
|
||||
} from "@/features/channel/components/storages/google-drive/google-drive-refresh.action";
|
||||
import {toast} from "sonner";
|
||||
|
||||
type StorageGoogleDriveFormProps = {
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import {drive_v3, google} from "googleapis";
|
||||
import {GoogleDriveConfig} from "@/features/channel/storages/google-drive/types";
|
||||
import {GoogleDriveConfig} from "@/features/channel/components/storages/google-drive/types";
|
||||
import Drive = drive_v3.Drive;
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
|
||||
+4
-4
@@ -6,15 +6,15 @@ import {
|
||||
StorageMetaData,
|
||||
StorageResult,
|
||||
StorageUploadInput
|
||||
} from '@/features/storages/storages.types';
|
||||
import {GoogleDriveConfig} from "@/features/channel/storages/google-drive/types";
|
||||
} from '@/features/storages/types';
|
||||
import {GoogleDriveConfig} from "@/features/channel/components/storages/google-drive/types";
|
||||
import {
|
||||
ensureFolderPath,
|
||||
findFileByName,
|
||||
getGoogleDriveClient, resolveFilePath
|
||||
} from "@/features/channel/storages/google-drive/helpers";
|
||||
} from "@/features/channel/components/storages/google-drive/helpers";
|
||||
import {Readable} from "node:stream";
|
||||
import {generateFileUrl} from "@/features/storages/storages.helpers";
|
||||
import {generateFileUrl} from "@/features/storages/utils/storages.helpers";
|
||||
|
||||
export async function uploadGoogleDrive(
|
||||
config: GoogleDriveConfig,
|
||||
+4
-4
@@ -2,18 +2,18 @@ import {
|
||||
StorageProviderKind,
|
||||
StorageInput,
|
||||
StorageResult,
|
||||
} from '@/features/storages/storages.types';
|
||||
} from '@/features/storages/types';
|
||||
|
||||
import {uploadLocal, getLocal, deleteLocal, pingLocal, copyLocal} from './local';
|
||||
import {copyS3, deleteS3, getS3, pingS3, uploadS3} from "@/features/channel/storages/s3";
|
||||
import {copyBlob, deleteBlob, getBlob, pingBlob, uploadBlob} from "@/features/channel/storages/az-blob";
|
||||
import {copyS3, deleteS3, getS3, pingS3, uploadS3} from "@/features/channel/components/storages/s3";
|
||||
import {copyBlob, deleteBlob, getBlob, pingBlob, uploadBlob} from "@/features/channel/components/storages/az-blob";
|
||||
import {
|
||||
copyGoogleDrive,
|
||||
deleteGoogleDrive,
|
||||
getGoogleDrive,
|
||||
pingGoogleDrive,
|
||||
uploadGoogleDrive
|
||||
} from "@/features/channel/storages/google-drive";
|
||||
} from "@/features/channel/components/storages/google-drive";
|
||||
|
||||
type ProviderHandler = {
|
||||
upload: (config: any, input: StorageInput & { action: 'upload' }) => Promise<StorageResult>;
|
||||
+2
-2
@@ -8,9 +8,9 @@ import {
|
||||
StorageMetaData,
|
||||
StorageResult,
|
||||
StorageUploadInput,
|
||||
} from '@/features/storages/storages.types';
|
||||
} from '@/features/storages/types';
|
||||
import fs from "node:fs";
|
||||
import { generateFileUrl } from "@/features/storages/storages.helpers";
|
||||
import { generateFileUrl } from "@/features/storages/utils/storages.helpers";
|
||||
import { Readable } from "node:stream";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {
|
||||
StorageMetaData,
|
||||
StorageResult,
|
||||
StorageUploadInput
|
||||
} from '@/features/storages/storages.types';
|
||||
} from '@/features/storages/types';
|
||||
import {Readable} from "node:stream";
|
||||
|
||||
type S3Config = {
|
||||
@@ -1,172 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||
import {db} from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {
|
||||
NotificationChannelFormSchema
|
||||
} from "@/features/channel/channel-form.schema";
|
||||
|
||||
|
||||
export const addNotificationChannelAction = userAction.schema(
|
||||
z.object({
|
||||
organizationId: z.string().optional(),
|
||||
data: NotificationChannelFormSchema
|
||||
})
|
||||
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||
const {organizationId, data} = parsedInput;
|
||||
try {
|
||||
const [channel] = await db
|
||||
.insert(drizzleDb.schemas.notificationChannel)
|
||||
.values({
|
||||
provider: data.provider,
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
enabled: data.enabled ?? true,
|
||||
organizationId: organizationId ?? null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (organizationId) {
|
||||
await db.insert(drizzleDb.schemas.organizationNotificationChannel).values({
|
||||
organizationId,
|
||||
notificationChannelId: channel.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: {
|
||||
...channel,
|
||||
config: channel.config as JSON
|
||||
},
|
||||
actionSuccess: {
|
||||
message: "Notification channel has been successfully created.",
|
||||
messageParams: {notificationChannelId: channel.id},
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error;
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to create notification channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {notificationChannelId: ""},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const removeNotificationChannelAction = userAction.schema(
|
||||
z.object({
|
||||
organizationId: z.string().optional(),
|
||||
notificationChannelId: z.string(),
|
||||
})
|
||||
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||
const {organizationId, notificationChannelId} = parsedInput;
|
||||
|
||||
try {
|
||||
if (organizationId) {
|
||||
await db
|
||||
.delete(drizzleDb.schemas.organizationNotificationChannel)
|
||||
.where(
|
||||
and(
|
||||
eq(drizzleDb.schemas.organizationNotificationChannel.organizationId, organizationId),
|
||||
eq(drizzleDb.schemas.organizationNotificationChannel.notificationChannelId, notificationChannelId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [deletedChannel] = await db
|
||||
.delete(drizzleDb.schemas.notificationChannel)
|
||||
.where(eq(drizzleDb.schemas.notificationChannel.id, notificationChannelId))
|
||||
.returning();
|
||||
|
||||
if (!deletedChannel) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Notification channel not found.",
|
||||
status: 404,
|
||||
messageParams: {notificationChannelId: notificationChannelId},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: {
|
||||
...deletedChannel,
|
||||
config: deletedChannel.config as JSON
|
||||
},
|
||||
actionSuccess: {
|
||||
message: "Notification channel has been successfully removed.",
|
||||
messageParams: {notificationChannelId: notificationChannelId},
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error;
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to remove notification channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {notificationChannelId: notificationChannelId},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const updateNotificationChannelAction = userAction.schema(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
data: NotificationChannelFormSchema
|
||||
})
|
||||
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||
const {id, data} = parsedInput;
|
||||
|
||||
try {
|
||||
const [channel] = await db
|
||||
.update(drizzleDb.schemas.notificationChannel)
|
||||
.set(withUpdatedAt({
|
||||
provider: data.provider,
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
enabled: data.enabled ?? true,
|
||||
}))
|
||||
.where(eq(drizzleDb.schemas.notificationChannel.id, id))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: {
|
||||
...channel,
|
||||
config: channel.config as JSON
|
||||
},
|
||||
actionSuccess: {
|
||||
message: `Notification channel "${channel.name}" has been successfully updated.`,
|
||||
messageParams: {id: channel.id},
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error;
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update notification channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {id: ""},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
+14
-14
@@ -1,18 +1,18 @@
|
||||
import {z} from "zod";
|
||||
import {SlackChannelConfigSchema} from "@/features/channel/notifications/slack.schema";
|
||||
import {SmtpChannelConfigSchema} from "@/features/channel/notifications/smtp.schema";
|
||||
import {DiscordChannelConfigSchema} from "@/features/channel/notifications/discord.schema";
|
||||
import {TelegramChannelConfigSchema} from "@/features/channel/notifications/telegram.schema";
|
||||
import {GotifyChannelConfigSchema} from "@/features/channel/notifications/gotify.schema";
|
||||
import {NtfyChannelConfigSchema} from "@/features/channel/notifications/ntfy.schema";
|
||||
import {WebhookChannelConfigSchema} from "@/features/channel/notifications/webhook.schema";
|
||||
import {NextcloudChannelConfigSchema} from "@/features/channel/notifications/nextcloud.schema";
|
||||
import {PushoverChannelConfigSchema} from "@/features/channel/notifications/pushover.schema";
|
||||
import {S3ChannelConfigSchema} from "@/features/channel/storages/s3.schema";
|
||||
import {GoogleDriveChannelConfigSchema} from "@/features/channel/storages/google-drive/google-drive.schema";
|
||||
import {LocalChannelConfigSchema} from "@/features/channel/storages/local.schema";
|
||||
import {TeamsChannelConfigSchema} from "@/features/channel/notifications/teams.schema";
|
||||
import {BlobChannelConfigSchema} from "@/features/channel/storages/az-blob.schema";
|
||||
import {SlackChannelConfigSchema} from "@/features/channel/components/notifications/slack.schema";
|
||||
import {SmtpChannelConfigSchema} from "@/features/channel/components/notifications/smtp.schema";
|
||||
import {DiscordChannelConfigSchema} from "@/features/channel/components/notifications/discord.schema";
|
||||
import {TelegramChannelConfigSchema} from "@/features/channel/components/notifications/telegram.schema";
|
||||
import {GotifyChannelConfigSchema} from "@/features/channel/components/notifications/gotify.schema";
|
||||
import {NtfyChannelConfigSchema} from "@/features/channel/components/notifications/ntfy.schema";
|
||||
import {WebhookChannelConfigSchema} from "@/features/channel/components/notifications/webhook.schema";
|
||||
import {NextcloudChannelConfigSchema} from "@/features/channel/components/notifications/nextcloud.schema";
|
||||
import {PushoverChannelConfigSchema} from "@/features/channel/components/notifications/pushover.schema";
|
||||
import {S3ChannelConfigSchema} from "@/features/channel/components/storages/s3.schema";
|
||||
import {GoogleDriveChannelConfigSchema} from "@/features/channel/components/storages/google-drive/google-drive.schema";
|
||||
import {LocalChannelConfigSchema} from "@/features/channel/components/storages/local.schema";
|
||||
import {TeamsChannelConfigSchema} from "@/features/channel/components/notifications/teams.schema";
|
||||
import {BlobChannelConfigSchema} from "@/features/channel/components/storages/az-blob.schema";
|
||||
|
||||
|
||||
const BaseChannelFormSchema = z.object({
|
||||
@@ -1,173 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {db} from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {
|
||||
StorageChannelFormSchema
|
||||
} from "@/features/channel/channel-form.schema";
|
||||
import {StorageChannel} from "@/db/schema/12_storage-channel";
|
||||
|
||||
|
||||
export const addStorageChannelAction = userAction.schema(
|
||||
z.object({
|
||||
organizationId: z.string().optional(),
|
||||
data: StorageChannelFormSchema
|
||||
})
|
||||
).action(async ({parsedInput}): Promise<ServerActionResult<StorageChannel>> => {
|
||||
const {organizationId, data} = parsedInput;
|
||||
|
||||
try {
|
||||
const [channel] = await db
|
||||
.insert(drizzleDb.schemas.storageChannel)
|
||||
.values({
|
||||
provider: data.provider,
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
enabled: data.enabled ?? true,
|
||||
organizationId: organizationId ?? null
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (organizationId) {
|
||||
await db.insert(drizzleDb.schemas.organizationStorageChannel).values({
|
||||
organizationId,
|
||||
storageChannelId: channel.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: {
|
||||
...channel,
|
||||
config: channel.config as JSON
|
||||
},
|
||||
actionSuccess: {
|
||||
message: "Storage channel has been successfully created.",
|
||||
messageParams: {id: channel.id},
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error;
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to create storage channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {id: ""},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const removeStorageChannelAction = userAction.schema(
|
||||
z.object({
|
||||
organizationId: z.string().optional(),
|
||||
id: z.string(),
|
||||
})
|
||||
).action(async ({parsedInput}): Promise<ServerActionResult<StorageChannel>> => {
|
||||
const {organizationId, id} = parsedInput;
|
||||
|
||||
try {
|
||||
if (organizationId) {
|
||||
await db
|
||||
.delete(drizzleDb.schemas.organizationStorageChannel)
|
||||
.where(
|
||||
and(
|
||||
eq(drizzleDb.schemas.organizationStorageChannel.organizationId, organizationId),
|
||||
eq(drizzleDb.schemas.organizationStorageChannel.storageChannelId, id)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [deletedChannel] = await db
|
||||
.delete(drizzleDb.schemas.storageChannel)
|
||||
.where(eq(drizzleDb.schemas.storageChannel.id, id))
|
||||
.returning();
|
||||
|
||||
if (!deletedChannel) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Storage channel not found.",
|
||||
status: 404,
|
||||
messageParams: {id: id},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: {
|
||||
...deletedChannel,
|
||||
config: deletedChannel.config as JSON
|
||||
},
|
||||
actionSuccess: {
|
||||
message: "Storage channel has been successfully removed.",
|
||||
messageParams: {id: id},
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error;
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to remove storage channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {id: id},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const updateStorageChannelAction = userAction.schema(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
data: StorageChannelFormSchema
|
||||
})
|
||||
).action(async ({parsedInput}): Promise<ServerActionResult<StorageChannel>> => {
|
||||
const {id, data} = parsedInput;
|
||||
|
||||
try {
|
||||
const [channel] = await db
|
||||
.update(drizzleDb.schemas.storageChannel)
|
||||
.set(withUpdatedAt({
|
||||
provider: data.provider,
|
||||
name: data.name,
|
||||
config: data.config,
|
||||
enabled: data.enabled ?? true,
|
||||
}))
|
||||
.where(eq(drizzleDb.schemas.storageChannel.id, id))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: {
|
||||
...channel,
|
||||
config: channel.config as JSON
|
||||
},
|
||||
actionSuccess: {
|
||||
message: `Storage channel "${channel.name}" has been successfully updated.`,
|
||||
messageParams: {id: channel.id},
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error;
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update storage channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {id: ""},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user