mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix
This commit is contained in:
@@ -1,89 +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 { 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 {
|
||||
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";
|
||||
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
|
||||
|
||||
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 { data, organization, kind, defaultStorageChannelId } = props;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const isDefaultSystemStorage = defaultStorageChannelId === data.id;
|
||||
const isDefaultSystemStorage = defaultStorageChannelId === data.id;
|
||||
|
||||
const isOwned = data.organizationId ? true : !organization;
|
||||
const isLocalSystem = data.provider == "local";
|
||||
const isSystemChannel = data.organizationId === null;
|
||||
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 && !isSystemChannel && (
|
||||
<DeleteChannelButton
|
||||
kind={kind}
|
||||
organizationId={organization?.id}
|
||||
channelId={data.id}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,190 +1,200 @@
|
||||
"use server";
|
||||
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
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";
|
||||
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(
|
||||
export const addNotificationChannelAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
organizationId: z.string().optional(),
|
||||
data: NotificationChannelFormSchema
|
||||
})
|
||||
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||
const {organizationId, data} = parsedInput;
|
||||
try {
|
||||
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();
|
||||
.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,
|
||||
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},
|
||||
},
|
||||
success: true,
|
||||
value: {
|
||||
...channel,
|
||||
config: channel.config as JSON,
|
||||
},
|
||||
actionSuccess: {
|
||||
message: "Notification channel has been successfully created.",
|
||||
messageParams: { notificationChannelId: channel.id },
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
} 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: ""},
|
||||
},
|
||||
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(
|
||||
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 {
|
||||
const existing = await db.query.notificationChannel.findFirst({
|
||||
where: eq(drizzleDb.schemas.notificationChannel.id, notificationChannelId),
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: { message: "Notification channel not found.", status: 404, messageParams: { notificationChannelId } },
|
||||
};
|
||||
}
|
||||
|
||||
if (existing.organizationId === null) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: { message: "System notification channels cannot be deleted.", status: 403, messageParams: { notificationChannelId } },
|
||||
};
|
||||
}
|
||||
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)
|
||||
)
|
||||
);
|
||||
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();
|
||||
.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: 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},
|
||||
},
|
||||
success: true,
|
||||
value: {
|
||||
...deletedChannel,
|
||||
config: deletedChannel.config as JSON,
|
||||
},
|
||||
actionSuccess: {
|
||||
message: "Notification channel has been successfully removed.",
|
||||
messageParams: { notificationChannelId: notificationChannelId },
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
} 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},
|
||||
},
|
||||
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(
|
||||
export const updateNotificationChannelAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
data: NotificationChannelFormSchema
|
||||
})
|
||||
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||
const {id, data} = parsedInput;
|
||||
id: z.string(),
|
||||
data: NotificationChannelFormSchema,
|
||||
}),
|
||||
)
|
||||
.action(
|
||||
async ({
|
||||
parsedInput,
|
||||
}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||
const { id, data } = parsedInput;
|
||||
|
||||
try {
|
||||
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();
|
||||
.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},
|
||||
},
|
||||
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) {
|
||||
} 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: ""},
|
||||
},
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update notification channel.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: { id: "" },
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -31,9 +31,6 @@ export const OnboardingChecklist = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-0 p-6 h-full">
|
||||
<p className="text-xs font-medium text-zinc-500 uppercase tracking-wider mb-4">
|
||||
Progress
|
||||
</p>
|
||||
<div className="flex flex-col relative">
|
||||
{CHECKLIST_STEPS.map((step, i) => {
|
||||
const isCompleted = i < currentIndex;
|
||||
|
||||
+15
-5
@@ -160,7 +160,7 @@ export const auth = betterAuth({
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
maxRequests: 100,
|
||||
timeWindow: 1000 * 60 * 60, // 100 requests / hour
|
||||
timeWindow: 1000 * 60 * 60,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -170,7 +170,7 @@ export const auth = betterAuth({
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
maxRequests: 1000,
|
||||
timeWindow: 1000 * 60 * 60, // 1000 requests / hour
|
||||
timeWindow: 1000 * 60 * 60,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -180,7 +180,7 @@ export const auth = betterAuth({
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
maxRequests: 10_000,
|
||||
timeWindow: 1000 * 60 * 60, // 10k requests / hour
|
||||
timeWindow: 1000 * 60 * 60,
|
||||
},
|
||||
},
|
||||
]),
|
||||
@@ -436,7 +436,7 @@ export const auth = betterAuth({
|
||||
},
|
||||
user: {
|
||||
update: {
|
||||
async before(user, context) {
|
||||
async before(user) {
|
||||
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
|
||||
if (user.password || user.lastChangedPasswordAt) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
@@ -450,7 +450,7 @@ export const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
create: {
|
||||
async before(user, context) {
|
||||
async before(user) {
|
||||
const userCount = (
|
||||
await db.select({ count: count() }).from(drizzleDb.schemas.user)
|
||||
)[0].count;
|
||||
@@ -461,6 +461,16 @@ export const auth = betterAuth({
|
||||
});
|
||||
}
|
||||
|
||||
if (env.SKIP_ONBOARDING === "false" && userCount > 0) {
|
||||
const settings = await db.query.setting.findFirst();
|
||||
if (!settings?.onboarding) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message:
|
||||
"Registration is disabled: onboarding not yet completed.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const role = userCount === 0 ? "superadmin" : "pending";
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user