This commit is contained in:
Théo LAGACHE
2026-06-24 16:24:35 +02:00
parent 7a9784244b
commit 8cf164d229
4 changed files with 246 additions and 232 deletions
@@ -7,13 +7,12 @@ 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>
); );
}; };
@@ -8,17 +8,19 @@ 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>> => { )
.action(
async ({
parsedInput,
}): Promise<ServerActionResult<NotificationChannel>> => {
const { organizationId, data } = parsedInput; const { organizationId, data } = parsedInput;
try { try {
const [channel] = await db const [channel] = await db
@@ -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,7 +47,7 @@ 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.",
@@ -62,49 +66,47 @@ export const addNotificationChannelAction = userAction.schema(
}, },
}; };
} }
}); },
);
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>> => { )
.action(
async ({
parsedInput,
}): Promise<ServerActionResult<NotificationChannel>> => {
const { organizationId, notificationChannelId } = parsedInput; 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) {
@@ -122,7 +124,7 @@ 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.",
@@ -141,26 +143,33 @@ export const removeNotificationChannelAction = userAction.schema(
}, },
}; };
} }
}); },
);
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>> => { )
.action(
async ({
parsedInput,
}): Promise<ServerActionResult<NotificationChannel>> => {
const { id, data } = parsedInput; 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,7 +177,7 @@ 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.`,
@@ -187,4 +196,5 @@ export const updateNotificationChannelAction = userAction.schema(
}, },
}; };
} }
}); },
);
@@ -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
View File
@@ -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 {