mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix
This commit is contained in:
@@ -1,19 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {Card} from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
import { NotificationChannelWith } from "@/db/schema/09_notification-channel";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import { OrganizationWithMembers } from "@/db/schema/03_organization";
|
||||||
import {truncateWords} from "@/utils/text";
|
import { truncateWords } from "@/utils/text";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
import { StorageChannelWith } from "@/db/schema/12_storage-channel";
|
||||||
|
import { EditChannelButton } from "@/features/channel/components/channel-edit-button";
|
||||||
import {
|
import {
|
||||||
EditChannelButton
|
ChannelKind,
|
||||||
} from "@/features/channel/components/channel-edit-button";
|
getChannelIcon,
|
||||||
import {ChannelKind, getChannelIcon} from "@/features/channel/components/channels-helpers";
|
} from "@/features/channel/components/channels-helpers";
|
||||||
import {
|
import { DeleteChannelButton } from "@/features/channel/components/channel-delete-button";
|
||||||
DeleteChannelButton
|
|
||||||
} from "@/features/channel/components/channel-delete-button";
|
|
||||||
|
|
||||||
export type ChannelCardProps = {
|
export type ChannelCardProps = {
|
||||||
data: NotificationChannelWith | StorageChannelWith;
|
data: NotificationChannelWith | StorageChannelWith;
|
||||||
@@ -21,34 +20,34 @@ export type ChannelCardProps = {
|
|||||||
organizations?: OrganizationWithMembers[];
|
organizations?: OrganizationWithMembers[];
|
||||||
adminView?: boolean;
|
adminView?: boolean;
|
||||||
kind?: ChannelKind;
|
kind?: ChannelKind;
|
||||||
defaultStorageChannelId?: string | null | undefined
|
defaultStorageChannelId?: string | null | undefined;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const ChannelCard = (props: ChannelCardProps) => {
|
export const ChannelCard = (props: ChannelCardProps) => {
|
||||||
const {data, organization, kind, adminView, defaultStorageChannelId} = props;
|
const { data, organization, kind, defaultStorageChannelId } = props;
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const isDefaultSystemStorage = defaultStorageChannelId === data.id;
|
const isDefaultSystemStorage = defaultStorageChannelId === data.id;
|
||||||
|
|
||||||
const isOwned = data.organizationId ? true : !organization;
|
const isOwned = data.organizationId ? true : !organization;
|
||||||
const isLocalSystem = data.provider == "local";
|
const isLocalSystem = data.provider === "local";
|
||||||
const isSystemChannel = data.organizationId === null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block transition-all duration-200 rounded-xl">
|
<div className="block transition-all duration-200 rounded-xl">
|
||||||
<Card className="flex flex-row justify-between p-4">
|
<Card className="flex flex-row justify-between p-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-secondary border border-border">
|
||||||
className="flex h-10 w-10 items-center justify-center rounded-md bg-secondary border border-border">
|
|
||||||
{getChannelIcon(data.provider)}
|
{getChannelIcon(data.provider)}
|
||||||
</div>
|
</div>
|
||||||
<div className={`h-2 w-2 rounded-full ${data.enabled ? "bg-green-600" : "bg-muted"}`}/>
|
<div
|
||||||
|
className={`h-2 w-2 rounded-full ${data.enabled ? "bg-green-600" : "bg-muted"}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-start w-full">
|
<div className="flex justify-start w-full">
|
||||||
<div className="flex flex-col items-start md:flex-row md:items-center gap-2 ">
|
<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>
|
<h3 className="font-medium text-foreground">
|
||||||
|
{isMobile ? truncateWords(data.name, 2) : data.name}
|
||||||
|
</h3>
|
||||||
<Badge variant="secondary" className="text-xs font-mono">
|
<Badge variant="secondary" className="text-xs font-mono">
|
||||||
{data.provider}
|
{data.provider}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -61,7 +60,7 @@ export const ChannelCard = (props: ChannelCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
{kind && (
|
{kind && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{(isOwned) && (
|
{isOwned && (
|
||||||
<>
|
<>
|
||||||
<EditChannelButton
|
<EditChannelButton
|
||||||
organizations={props.organizations}
|
organizations={props.organizations}
|
||||||
@@ -70,7 +69,7 @@ export const ChannelCard = (props: ChannelCardProps) => {
|
|||||||
channel={data}
|
channel={data}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
/>
|
/>
|
||||||
{!isLocalSystem && !isSystemChannel && (
|
{!isLocalSystem && (
|
||||||
<DeleteChannelButton
|
<DeleteChannelButton
|
||||||
kind={kind}
|
kind={kind}
|
||||||
organizationId={organization?.id}
|
organizationId={organization?.id}
|
||||||
@@ -85,5 +84,3 @@ export const ChannelCard = (props: ChannelCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
import { ServerActionResult } from "@/types/action-type";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {userAction} from "@/lib/safe-actions/actions";
|
import { userAction } from "@/lib/safe-actions/actions";
|
||||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
import { NotificationChannel } from "@/db/schema/09_notification-channel";
|
||||||
import {db} from "@/db";
|
import { db } from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import { withUpdatedAt } from "@/db/utils";
|
||||||
import {
|
import { NotificationChannelFormSchema } from "@/features/channel/channel-form.schema";
|
||||||
NotificationChannelFormSchema
|
|
||||||
} from "@/features/channel/schemas/channel-form.schema";
|
|
||||||
|
|
||||||
|
export const addNotificationChannelAction = userAction
|
||||||
export const addNotificationChannelAction = userAction.schema(
|
.schema(
|
||||||
z.object({
|
z.object({
|
||||||
organizationId: z.string().optional(),
|
organizationId: z.string().optional(),
|
||||||
data: NotificationChannelFormSchema
|
data: NotificationChannelFormSchema,
|
||||||
})
|
}),
|
||||||
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
)
|
||||||
const {organizationId, data} = parsedInput;
|
.action(
|
||||||
|
async ({
|
||||||
|
parsedInput,
|
||||||
|
}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||||
|
const { organizationId, data } = parsedInput;
|
||||||
try {
|
try {
|
||||||
const [channel] = await db
|
const [channel] = await db
|
||||||
.insert(drizzleDb.schemas.notificationChannel)
|
.insert(drizzleDb.schemas.notificationChannel)
|
||||||
@@ -33,7 +35,9 @@ export const addNotificationChannelAction = userAction.schema(
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
await db.insert(drizzleDb.schemas.organizationNotificationChannel).values({
|
await db
|
||||||
|
.insert(drizzleDb.schemas.organizationNotificationChannel)
|
||||||
|
.values({
|
||||||
organizationId,
|
organizationId,
|
||||||
notificationChannelId: channel.id,
|
notificationChannelId: channel.id,
|
||||||
});
|
});
|
||||||
@@ -43,11 +47,11 @@ export const addNotificationChannelAction = userAction.schema(
|
|||||||
success: true,
|
success: true,
|
||||||
value: {
|
value: {
|
||||||
...channel,
|
...channel,
|
||||||
config: channel.config as JSON
|
config: channel.config as JSON,
|
||||||
},
|
},
|
||||||
actionSuccess: {
|
actionSuccess: {
|
||||||
message: "Notification channel has been successfully created.",
|
message: "Notification channel has been successfully created.",
|
||||||
messageParams: {notificationChannelId: channel.id},
|
messageParams: { notificationChannelId: channel.id },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
@@ -58,53 +62,51 @@ export const addNotificationChannelAction = userAction.schema(
|
|||||||
message: "Failed to create notification channel.",
|
message: "Failed to create notification channel.",
|
||||||
status: 500,
|
status: 500,
|
||||||
cause: error instanceof Error ? error.message : "Unknown error",
|
cause: error instanceof Error ? error.message : "Unknown error",
|
||||||
messageParams: {notificationChannelId: ""},
|
messageParams: { notificationChannelId: "" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const removeNotificationChannelAction = userAction.schema(
|
export const removeNotificationChannelAction = userAction
|
||||||
|
.schema(
|
||||||
z.object({
|
z.object({
|
||||||
organizationId: z.string().optional(),
|
organizationId: z.string().optional(),
|
||||||
notificationChannelId: z.string(),
|
notificationChannelId: z.string(),
|
||||||
})
|
}),
|
||||||
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
)
|
||||||
const {organizationId, notificationChannelId} = parsedInput;
|
.action(
|
||||||
|
async ({
|
||||||
|
parsedInput,
|
||||||
|
}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||||
|
const { organizationId, notificationChannelId } = parsedInput;
|
||||||
|
|
||||||
try {
|
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 } },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
await db
|
await db
|
||||||
.delete(drizzleDb.schemas.organizationNotificationChannel)
|
.delete(drizzleDb.schemas.organizationNotificationChannel)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(drizzleDb.schemas.organizationNotificationChannel.organizationId, organizationId),
|
eq(
|
||||||
eq(drizzleDb.schemas.organizationNotificationChannel.notificationChannelId, notificationChannelId)
|
drizzleDb.schemas.organizationNotificationChannel
|
||||||
)
|
.organizationId,
|
||||||
|
organizationId,
|
||||||
|
),
|
||||||
|
eq(
|
||||||
|
drizzleDb.schemas.organizationNotificationChannel
|
||||||
|
.notificationChannelId,
|
||||||
|
notificationChannelId,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [deletedChannel] = await db
|
const [deletedChannel] = await db
|
||||||
.delete(drizzleDb.schemas.notificationChannel)
|
.delete(drizzleDb.schemas.notificationChannel)
|
||||||
.where(eq(drizzleDb.schemas.notificationChannel.id, notificationChannelId))
|
.where(
|
||||||
|
eq(drizzleDb.schemas.notificationChannel.id, notificationChannelId),
|
||||||
|
)
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (!deletedChannel) {
|
if (!deletedChannel) {
|
||||||
@@ -113,7 +115,7 @@ export const removeNotificationChannelAction = userAction.schema(
|
|||||||
actionError: {
|
actionError: {
|
||||||
message: "Notification channel not found.",
|
message: "Notification channel not found.",
|
||||||
status: 404,
|
status: 404,
|
||||||
messageParams: {notificationChannelId: notificationChannelId},
|
messageParams: { notificationChannelId: notificationChannelId },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -122,11 +124,11 @@ export const removeNotificationChannelAction = userAction.schema(
|
|||||||
success: true,
|
success: true,
|
||||||
value: {
|
value: {
|
||||||
...deletedChannel,
|
...deletedChannel,
|
||||||
config: deletedChannel.config as JSON
|
config: deletedChannel.config as JSON,
|
||||||
},
|
},
|
||||||
actionSuccess: {
|
actionSuccess: {
|
||||||
message: "Notification channel has been successfully removed.",
|
message: "Notification channel has been successfully removed.",
|
||||||
messageParams: {notificationChannelId: notificationChannelId},
|
messageParams: { notificationChannelId: notificationChannelId },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
@@ -137,30 +139,37 @@ export const removeNotificationChannelAction = userAction.schema(
|
|||||||
message: "Failed to remove notification channel.",
|
message: "Failed to remove notification channel.",
|
||||||
status: 500,
|
status: 500,
|
||||||
cause: error instanceof Error ? error.message : "Unknown error",
|
cause: error instanceof Error ? error.message : "Unknown error",
|
||||||
messageParams: {notificationChannelId: notificationChannelId},
|
messageParams: { notificationChannelId: notificationChannelId },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateNotificationChannelAction = userAction
|
||||||
export const updateNotificationChannelAction = userAction.schema(
|
.schema(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
data: NotificationChannelFormSchema
|
data: NotificationChannelFormSchema,
|
||||||
})
|
}),
|
||||||
).action(async ({parsedInput}): Promise<ServerActionResult<NotificationChannel>> => {
|
)
|
||||||
const {id, data} = parsedInput;
|
.action(
|
||||||
|
async ({
|
||||||
|
parsedInput,
|
||||||
|
}): Promise<ServerActionResult<NotificationChannel>> => {
|
||||||
|
const { id, data } = parsedInput;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [channel] = await db
|
const [channel] = await db
|
||||||
.update(drizzleDb.schemas.notificationChannel)
|
.update(drizzleDb.schemas.notificationChannel)
|
||||||
.set(withUpdatedAt({
|
.set(
|
||||||
|
withUpdatedAt({
|
||||||
provider: data.provider,
|
provider: data.provider,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
config: data.config,
|
config: data.config,
|
||||||
enabled: data.enabled ?? true,
|
enabled: data.enabled ?? true,
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.where(eq(drizzleDb.schemas.notificationChannel.id, id))
|
.where(eq(drizzleDb.schemas.notificationChannel.id, id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -168,11 +177,11 @@ export const updateNotificationChannelAction = userAction.schema(
|
|||||||
success: true,
|
success: true,
|
||||||
value: {
|
value: {
|
||||||
...channel,
|
...channel,
|
||||||
config: channel.config as JSON
|
config: channel.config as JSON,
|
||||||
},
|
},
|
||||||
actionSuccess: {
|
actionSuccess: {
|
||||||
message: `Notification channel "${channel.name}" has been successfully updated.`,
|
message: `Notification channel "${channel.name}" has been successfully updated.`,
|
||||||
messageParams: {id: channel.id},
|
messageParams: { id: channel.id },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
@@ -183,8 +192,9 @@ export const updateNotificationChannelAction = userAction.schema(
|
|||||||
message: "Failed to update notification channel.",
|
message: "Failed to update notification channel.",
|
||||||
status: 500,
|
status: 500,
|
||||||
cause: error instanceof Error ? error.message : "Unknown error",
|
cause: error instanceof Error ? error.message : "Unknown error",
|
||||||
messageParams: {id: ""},
|
messageParams: { id: "" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ export const OnboardingChecklist = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-0 p-6 h-full">
|
<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">
|
<div className="flex flex-col relative">
|
||||||
{CHECKLIST_STEPS.map((step, i) => {
|
{CHECKLIST_STEPS.map((step, i) => {
|
||||||
const isCompleted = i < currentIndex;
|
const isCompleted = i < currentIndex;
|
||||||
|
|||||||
+15
-5
@@ -160,7 +160,7 @@ export const auth = betterAuth({
|
|||||||
rateLimit: {
|
rateLimit: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxRequests: 100,
|
maxRequests: 100,
|
||||||
timeWindow: 1000 * 60 * 60, // 100 requests / hour
|
timeWindow: 1000 * 60 * 60,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -170,7 +170,7 @@ export const auth = betterAuth({
|
|||||||
rateLimit: {
|
rateLimit: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxRequests: 1000,
|
maxRequests: 1000,
|
||||||
timeWindow: 1000 * 60 * 60, // 1000 requests / hour
|
timeWindow: 1000 * 60 * 60,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -180,7 +180,7 @@ export const auth = betterAuth({
|
|||||||
rateLimit: {
|
rateLimit: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxRequests: 10_000,
|
maxRequests: 10_000,
|
||||||
timeWindow: 1000 * 60 * 60, // 10k requests / hour
|
timeWindow: 1000 * 60 * 60,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -436,7 +436,7 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
update: {
|
update: {
|
||||||
async before(user, context) {
|
async before(user) {
|
||||||
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
|
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
|
||||||
if (user.password || user.lastChangedPasswordAt) {
|
if (user.password || user.lastChangedPasswordAt) {
|
||||||
throw new APIError("FORBIDDEN", {
|
throw new APIError("FORBIDDEN", {
|
||||||
@@ -450,7 +450,7 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
async before(user, context) {
|
async before(user) {
|
||||||
const userCount = (
|
const userCount = (
|
||||||
await db.select({ count: count() }).from(drizzleDb.schemas.user)
|
await db.select({ count: count() }).from(drizzleDb.schemas.user)
|
||||||
)[0].count;
|
)[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";
|
const role = userCount === 0 ? "superadmin" : "pending";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user