chore: checkpoint before features reorganization

This commit is contained in:
Théo LAGACHE
2026-06-23 16:00:22 +02:00
parent 7c9b38b1eb
commit 87fe60f82a
34 changed files with 930 additions and 933 deletions
+1 -5
View File
@@ -1,11 +1,7 @@
{ {
"extends": [ "extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended"],
"next/core-web-vitals",
"plugin:tailwindcss/recommended"
],
"rules": { "rules": {
"react/no-unescaped-entities": "off", "react/no-unescaped-entities": "off",
"react-hooks/rules-of-hooks": "off" "react-hooks/rules-of-hooks": "off"
} }
} }
-51
View File
@@ -1,51 +0,0 @@
import { ImageResponse } from "next/og";
import { NextRequest, NextResponse } from "next/server";
import { getSettings } from "@/db/services/setting";
export const runtime = "nodejs";
const AVATAR_COLORS = [
"#4f46e5",
"#7c3aed",
"#e11d48",
"#ea580c",
"#d97706",
"#059669",
"#0891b2",
"#52525b",
];
export async function GET(request: NextRequest) {
const settings = await getSettings();
if (settings?.avatarMode && settings.avatarMode !== "internal") {
return new NextResponse(null, { status: 404 });
}
const { searchParams } = new URL(request.url);
const initials = (searchParams.get("initials") ?? "?")
.slice(0, 2)
.toUpperCase();
const color = searchParams.get("color") ?? AVATAR_COLORS[0];
return new ImageResponse(
<div
style={{
width: 80,
height: 80,
borderRadius: "50%",
backgroundColor: color,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "white",
fontSize: 28,
fontWeight: 700,
fontFamily: "sans-serif",
letterSpacing: "-0.5px",
}}
>
{initials}
</div>,
{ width: 80, height: 80 },
);
}
+26 -8
View File
@@ -10,8 +10,14 @@ import { AuthProviderConfig } from "@/lib/auth/config";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig[] }) { export function SocialAuthButtons({
const socialProviders = providers.filter((p) => p.isActive && p.type !== "credential"); providers,
}: {
providers: AuthProviderConfig[];
}) {
const socialProviders = providers.filter(
(p) => p.isActive && p.type !== "credential",
);
const router = useRouter(); const router = useRouter();
const [isLoading, setIsLoading] = useState<string | null>(null); const [isLoading, setIsLoading] = useState<string | null>(null);
@@ -27,7 +33,9 @@ export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig
router.push("/dashboard"); router.push("/dashboard");
}, },
onError() { onError() {
toast.error("An error occurred during passkey authentication. Please try again."); toast.error(
"An error occurred during passkey authentication. Please try again.",
);
}, },
}, },
}); });
@@ -37,7 +45,6 @@ export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig
providerType: "oidc", providerType: "oidc",
callbackURL: "/dashboard", callbackURL: "/dashboard",
}); });
console.log(result);
} else { } else {
result = await authClient.signIn.social({ result = await authClient.signIn.social({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -47,12 +54,16 @@ export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig
} }
if (result?.error) { if (result?.error) {
toast.error("An error occurred while signing in with the provider. Please try again."); toast.error(
"An error occurred while signing in with the provider. Please try again.",
);
} else if (provider.id !== "passkey") { } else if (provider.id !== "passkey") {
toast.success("Redirecting to provider..."); toast.success("Redirecting to provider...");
} }
} catch (err) { } catch (err) {
toast.error("An error occurred while signing in with the provider. Please try again."); toast.error(
"An error occurred while signing in with the provider. Please try again.",
);
} finally { } finally {
setIsLoading(null); setIsLoading(null);
} }
@@ -63,10 +74,17 @@ export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig
return ( return (
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
{socialProviders.map((provider) => ( {socialProviders.map((provider) => (
<Button key={provider.id} variant="outline" className="w-full gap-2" onClick={() => handleSocialSignIn(provider)} disabled={!!isLoading}> <Button
key={provider.id}
variant="outline"
className="w-full gap-2"
onClick={() => handleSocialSignIn(provider)}
disabled={!!isLoading}
>
{isLoading === provider.id ? ( {isLoading === provider.id ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
) : provider.icon.startsWith("/") || provider.icon.startsWith("http") ? ( ) : provider.icon.startsWith("/") ||
provider.icon.startsWith("http") ? (
<Image <Image
src={provider.icon} src={provider.icon}
alt={provider.id || "icon"} alt={provider.id || "icon"}
+1 -1
View File
@@ -37,7 +37,7 @@ export const LoggedInButtonClient = ({ user, sessions, currentSession, accounts,
> >
<SidebarMenuButton type="button" className="h-auto justify-between py-2" data-testid="profile-dropdown"> <SidebarMenuButton type="button" className="h-auto justify-between py-2" data-testid="profile-dropdown">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Avatar className="size-6"> <Avatar key={avatarUrl ?? "fallback"} className="size-6">
<AvatarFallback>{(user.name?.[0] ?? user.email?.[0] ?? "?").toUpperCase()}</AvatarFallback> <AvatarFallback>{(user.name?.[0] ?? user.email?.[0] ?? "?").toUpperCase()}</AvatarFallback>
{avatarUrl && <AvatarImage src={avatarUrl} />} {avatarUrl && <AvatarImage src={avatarUrl} />}
</Avatar> </Avatar>
@@ -27,7 +27,7 @@ export const StorageSection = ({ initial, storages, onSave, isPending }: Storage
} }
noChannelsMessage={ noChannelsMessage={
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Go back and configure storages in the &quot;Connect a storage&quot; step first. Go back and configure storages in the &quot;Connect an external storage&quot; step first.
</p> </p>
} }
/> />
+16 -1
View File
@@ -107,7 +107,9 @@ export async function resolveOnboardingState(): Promise<ResolvedOnboardingState>
organizationId: n.organizationId ?? null, organizationId: n.organizationId ?? null,
})); }));
const storages = storageChannels.map((s) => ({ const storages = storageChannels
.filter((s) => s.provider !== "local")
.map((s) => ({
id: s.id, id: s.id,
provider: s.provider, provider: s.provider,
label: s.provider, label: s.provider,
@@ -168,6 +170,10 @@ export async function resolveOnboardingState(): Promise<ResolvedOnboardingState>
const hasAgents = agents && agents.length > 0; const hasAgents = agents && agents.length > 0;
const defaultsConfigured =
!!settings?.defaultNotificationChannelId &&
!!settings?.defaultStorageChannelId;
if (project) { if (project) {
if (!hasAgents) { if (!hasAgents) {
if (notifiers.length === 0) { if (notifiers.length === 0) {
@@ -178,9 +184,13 @@ export async function resolveOnboardingState(): Promise<ResolvedOnboardingState>
meta.resumeStepId = "storage"; meta.resumeStepId = "storage";
return { stepId: "storage", flowData: fullData }; return { stepId: "storage", flowData: fullData };
} }
if (defaultsConfigured) {
meta.resumeStepId = "agent-create"; meta.resumeStepId = "agent-create";
return { stepId: "agent-create", flowData: fullData }; return { stepId: "agent-create", flowData: fullData };
} }
meta.resumeStepId = "defaults";
return { stepId: "defaults", flowData: fullData };
}
const agentHasPinged = !!agents[0]?.lastContact; const agentHasPinged = !!agents[0]?.lastContact;
if (!agentHasPinged) { if (!agentHasPinged) {
@@ -215,6 +225,11 @@ export async function resolveOnboardingState(): Promise<ResolvedOnboardingState>
return { stepId: "storage", flowData: fullData }; return { stepId: "storage", flowData: fullData };
} }
if (defaultsConfigured) {
meta.resumeStepId = "agent-create"; meta.resumeStepId = "agent-create";
return { stepId: "agent-create", flowData: fullData }; return { stepId: "agent-create", flowData: fullData };
} }
meta.resumeStepId = "defaults";
return { stepId: "defaults", flowData: fullData };
}
+5 -1
View File
@@ -105,7 +105,11 @@ export const onboardingSteps: OnboardingStep[] = [
id: "agent-key", id: "agent-key",
component: StepAgentKey, component: StepAgentKey,
isSkippable: false, isSkippable: false,
nextStep: "agent-waiting", nextStep: (ctx: any) => {
const agents = (ctx.flowData?.agents as any[]) || [];
const isConnected = agents.some((a) => a.connected);
return isConnected ? "project-create" : "agent-waiting";
},
}, },
{ {
id: "agent-waiting", id: "agent-waiting",
@@ -100,7 +100,9 @@ export const StepDbSettings = () => {
storagePolicies: settings.storagePolicies, storagePolicies: settings.storagePolicies,
}); });
succeededIds.push(targetId); succeededIds.push(targetId);
} catch {} } catch (err) {
console.error("Failed to apply settings to database", targetId, err);
}
} }
if (succeededIds.length > 0) { if (succeededIds.length > 0) {
@@ -2,6 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import { useOnboarding } from "@onboardjs/react"; import { useOnboarding } from "@onboardjs/react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@@ -46,7 +47,9 @@ export const StepDefaults = () => {
); );
const selectNotifier = async (value: string) => { const selectNotifier = async (value: string) => {
const prev = notifierId;
setNotifierId(value); setNotifierId(value);
try {
await updateNotificationSettingsAction({ await updateNotificationSettingsAction({
name: "system", name: "system",
data: { notificationChannelId: value }, data: { notificationChannelId: value },
@@ -54,13 +57,19 @@ export const StepDefaults = () => {
await updateContext({ await updateContext({
flowData: { flowData: {
...state?.context.flowData, ...state?.context.flowData,
defaults: { notifierId: value, storageId, avatarMode }, defaults: { notifierId: value, storageId, avatarMode, dicebearStyle },
}, },
}); });
} catch {
setNotifierId(prev);
toast.error("Failed to save default notifier");
}
}; };
const selectStorage = async (value: string) => { const selectStorage = async (value: string) => {
const prev = storageId;
setStorageId(value); setStorageId(value);
try {
await updateStorageSettingsAction({ await updateStorageSettingsAction({
name: "system", name: "system",
data: { storageChannelId: value, encryption: false }, data: { storageChannelId: value, encryption: false },
@@ -68,13 +77,19 @@ export const StepDefaults = () => {
await updateContext({ await updateContext({
flowData: { flowData: {
...state?.context.flowData, ...state?.context.flowData,
defaults: { notifierId, storageId: value, avatarMode }, defaults: { notifierId, storageId: value, avatarMode, dicebearStyle },
}, },
}); });
} catch {
setStorageId(prev);
toast.error("Failed to save default storage");
}
}; };
const selectAvatarMode = async (mode: AvatarMode) => { const selectAvatarMode = async (mode: AvatarMode) => {
const prev = avatarMode;
setAvatarMode(mode); setAvatarMode(mode);
try {
await updateAvatarModeAction({ await updateAvatarModeAction({
name: "system", name: "system",
avatarMode: mode, avatarMode: mode,
@@ -86,10 +101,16 @@ export const StepDefaults = () => {
defaults: { notifierId, storageId, avatarMode: mode, dicebearStyle }, defaults: { notifierId, storageId, avatarMode: mode, dicebearStyle },
}, },
}); });
} catch {
setAvatarMode(prev);
toast.error("Failed to save avatar mode");
}
}; };
const selectDicebearStyle = async (style: string) => { const selectDicebearStyle = async (style: string) => {
const prev = dicebearStyle;
setDicebearStyle(style); setDicebearStyle(style);
try {
await updateAvatarModeAction({ await updateAvatarModeAction({
name: "system", name: "system",
avatarMode: "dicebear", avatarMode: "dicebear",
@@ -101,6 +122,10 @@ export const StepDefaults = () => {
defaults: { notifierId, storageId, avatarMode, dicebearStyle: style }, defaults: { notifierId, storageId, avatarMode, dicebearStyle: style },
}, },
}); });
} catch {
setDicebearStyle(prev);
toast.error("Failed to save avatar style");
}
}; };
const onContinue = async () => { const onContinue = async () => {
@@ -218,7 +243,7 @@ export const StepDefaults = () => {
<AvatarModeSelector value={avatarMode} onChange={selectAvatarMode} /> <AvatarModeSelector value={avatarMode} onChange={selectAvatarMode} />
{avatarMode === "dicebear" && ( {avatarMode === "dicebear" && (
<DicebearStylePicker value={dicebearStyle} onChange={selectDicebearStyle} /> <DicebearStylePicker value={dicebearStyle} onChange={selectDicebearStyle} maxHeight="max-h-56" />
)} )}
<Button type="button" onClick={onContinue}> <Button type="button" onClick={onContinue}>
+1 -1
View File
@@ -47,7 +47,7 @@ export const StepLogin = () => {
if (session?.user) { if (session?.user) {
router.push("/welcome"); router.push("/welcome");
} }
}, [session?.user?.id]); }, [session?.user, router]);
const form = useZodForm({ schema: LoginSchema }); const form = useZodForm({ schema: LoginSchema });
@@ -2,51 +2,21 @@
import { useOnboarding } from "@onboardjs/react"; import { useOnboarding } from "@onboardjs/react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { authClient } from "@/lib/auth/auth-client"; import { authClient } from "@/lib/auth/auth-client";
import type { OnboardingAccountData } from "@/features/onboarding/types";
import { ThemeKey, ThemeSelector } from "@/components/common/theme-selector"; import { ThemeKey, ThemeSelector } from "@/components/common/theme-selector";
const AVATAR_COLORS = [
{ name: "indigo", hex: "#4f46e5" },
{ name: "violet", hex: "#7c3aed" },
{ name: "rose", hex: "#e11d48" },
{ name: "orange", hex: "#ea580c" },
{ name: "amber", hex: "#d97706" },
{ name: "emerald", hex: "#059669" },
{ name: "cyan", hex: "#0891b2" },
{ name: "zinc", hex: "#52525b" },
];
export const StepPreferences = () => { export const StepPreferences = () => {
const { next, updateContext, state } = useOnboarding(); const { next, updateContext, state } = useOnboarding();
const { theme: currentTheme, setTheme } = useTheme(); const { theme: currentTheme, setTheme } = useTheme();
const preferences = state?.context.flowData.preferences ?? { const preferences = state?.context.flowData.preferences ?? {
theme: (currentTheme ?? "system") as ThemeKey, theme: (currentTheme ?? "system") as ThemeKey,
}; };
const account = state?.context.flowData.account as
| OnboardingAccountData
| undefined;
const initials = account
? `${account.firstName[0] ?? ""}${account.lastName[0] ?? ""}`.toUpperCase()
: "PB";
const selectedAvatarUrl = preferences.avatarUrl;
const selectAvatar = async (color: string) => {
const url = `/api/avatar?initials=${initials}&color=${encodeURIComponent(color)}`;
await updateContext({
flowData: {
...state?.context.flowData,
preferences: { ...preferences, avatarUrl: url },
},
});
};
const selectTheme = async (theme: ThemeKey) => { const selectTheme = async (theme: ThemeKey) => {
setTheme(theme); setTheme(theme);
try {
await authClient.updateUser({ theme }); await authClient.updateUser({ theme });
await updateContext({ await updateContext({
flowData: { flowData: {
@@ -54,12 +24,13 @@ export const StepPreferences = () => {
preferences: { ...preferences, theme }, preferences: { ...preferences, theme },
}, },
}); });
} catch {
toast.error("Failed to save theme preference");
setTheme(preferences.theme as ThemeKey);
}
}; };
const onContinue = async () => { const onContinue = async () => {
if (selectedAvatarUrl) {
await authClient.updateUser({ image: selectedAvatarUrl });
}
await next(); await next();
}; };
@@ -77,37 +48,6 @@ export const StepPreferences = () => {
<ThemeSelector value={preferences.theme} onSelect={selectTheme} /> <ThemeSelector value={preferences.theme} onSelect={selectTheme} />
</div> </div>
<div className="flex flex-col gap-2">
<p className="text-sm font-medium">Avatar</p>
<div className="flex flex-wrap gap-2">
{AVATAR_COLORS.map((c) => {
const url = `/api/avatar?initials=${initials}&color=${encodeURIComponent(c.hex)}`;
const isSelected = selectedAvatarUrl === url;
return (
<button
key={c.name}
type="button"
onClick={() => selectAvatar(c.hex)}
className={cn(
"rounded-full overflow-hidden transition-all shrink-0",
isSelected
? "ring-2 ring-primary ring-offset-2 ring-offset-background"
: "opacity-70 hover:opacity-100",
)}
>
<img
src={url}
alt={c.name}
width={40}
height={40}
className="size-10 rounded-full"
/>
</button>
);
})}
</div>
</div>
<Button type="button" onClick={onContinue}> <Button type="button" onClick={onContinue}>
Continue Continue
</Button> </Button>
@@ -135,9 +135,9 @@ export const StepStorage = () => {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div> <div>
<h1 className="text-2xl font-semibold">Connect a storage</h1> <h1 className="text-2xl font-semibold">Connect an external storage</h1>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
Optional choose where backups and files are stored. Optional link an S3-compatible bucket or other external storage to keep your backups safe and off-instance.
</p> </p>
</div> </div>
-1
View File
@@ -23,7 +23,6 @@ export type OnboardingSecurityData = {
export type OnboardingPreferencesData = { export type OnboardingPreferencesData = {
theme: "light" | "dark"; theme: "light" | "dark";
avatarUrl?: string;
}; };
export type OnboardingOrgData = { export type OnboardingOrgData = {
+11 -10
View File
@@ -31,7 +31,6 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
const inner = result?.data; const inner = result?.data;
if (inner?.success) { if (inner?.success) {
const updateUser = await updateImageUserAction(inner.value ?? ""); const updateUser = await updateImageUserAction(inner.value ?? "");
const dataUser = updateUser?.data; const dataUser = updateUser?.data;
@@ -45,8 +44,6 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
} else { } else {
toast.error(inner?.actionError?.message); toast.error(inner?.actionError?.message);
} }
}, },
}); });
@@ -70,14 +67,16 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
submitImage.mutate(file); submitImage.mutate(file);
}; };
return ( return (
<div className="relative "> <div className="relative ">
<Avatar
<Avatar className="w-24 h-24 lg:w-32 lg:h-32 border-4 border-muted/20"> key={src}
<AvatarImage className="object-cover" src={src}/> className="w-24 h-24 lg:w-32 lg:h-32 border-4 border-muted/20"
<AvatarFallback className="text-3xl">{(user.name?.charAt(0) ?? user.email?.charAt(0) ?? "?").toUpperCase()}</AvatarFallback> >
{src && <AvatarImage className="object-cover" src={src} />}
<AvatarFallback className="text-3xl">
{(user.name?.charAt(0) ?? user.email?.charAt(0) ?? "?").toUpperCase()}
</AvatarFallback>
</Avatar> </Avatar>
{canUpload && ( {canUpload && (
@@ -87,7 +86,9 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
fileInput.type = "file"; fileInput.type = "file";
fileInput.accept = ".jpg,.jpeg,.png,.webp"; fileInput.accept = ".jpg,.jpeg,.png,.webp";
fileInput.onchange = (e: Event) => fileInput.onchange = (e: Event) =>
handleImageUpload(e as unknown as React.ChangeEvent<HTMLInputElement>); handleImageUpload(
e as unknown as React.ChangeEvent<HTMLInputElement>,
);
fileInput.click(); fileInput.click();
}} }}
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-30 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32" className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-30 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32"
@@ -28,9 +28,9 @@ export const AvatarModeSelector = ({ value, onChange, disabled }: Props) => (
key={mode.value} key={mode.value}
type="button" type="button"
disabled={disabled} disabled={disabled}
onClick={() => onChange(mode.value)} onClick={() => { if (!isActive) onChange(mode.value); }}
className={cn( className={cn(
"flex flex-col gap-2 rounded-xl border-2 p-3 text-left transition-all hover:bg-accent/50 disabled:opacity-50", "flex flex-col gap-3 rounded-xl border-2 p-4 text-left transition-all hover:bg-accent/50 disabled:opacity-50",
isActive ? "border-primary bg-primary/5" : "border-muted/40", isActive ? "border-primary bg-primary/5" : "border-muted/40",
)} )}
> >
+1 -1
View File
@@ -42,7 +42,7 @@ export const SettingsAvatarSection = ({ settings }: Props) => {
}; };
return ( return (
<div className="flex flex-col gap-6 max-w-2xl"> <div className="flex flex-col gap-6">
<div> <div>
<h2 className="text-lg font-semibold">Avatar</h2> <h2 className="text-lg font-semibold">Avatar</h2>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
@@ -10,9 +10,10 @@ type Props = {
value: string; value: string;
onChange: (style: string) => void; onChange: (style: string) => void;
disabled?: boolean; disabled?: boolean;
maxHeight?: string;
}; };
export const DicebearStylePicker = ({ value, onChange, disabled }: Props) => { export const DicebearStylePicker = ({ value, onChange, disabled, maxHeight = "max-h-96" }: Props) => {
const dicebearState = useDiceBearStyles(); const dicebearState = useDiceBearStyles();
return ( return (
@@ -43,7 +44,7 @@ export const DicebearStylePicker = ({ value, onChange, disabled }: Props) => {
)} )}
{dicebearState.status === "success" && ( {dicebearState.status === "success" && (
<div className="max-h-52 overflow-y-auto scrollbar-hide"> <div className={cn("overflow-y-auto scrollbar-hide", maxHeight)}>
<div className="grid grid-cols-3 sm:grid-cols-5 gap-2"> <div className="grid grid-cols-3 sm:grid-cols-5 gap-2">
{dicebearState.styles.map((style) => { {dicebearState.styles.map((style) => {
const isActive = value === style; const isActive = value === style;
@@ -52,7 +53,7 @@ export const DicebearStylePicker = ({ value, onChange, disabled }: Props) => {
key={style} key={style}
type="button" type="button"
disabled={disabled} disabled={disabled}
onClick={() => onChange(style)} onClick={() => { if (!isActive) onChange(style); }}
title={style} title={style}
className={cn( className={cn(
"flex flex-col items-center gap-1 rounded-lg border-2 p-1.5 transition-all hover:bg-accent/50 disabled:opacity-50", "flex flex-col items-center gap-1 rounded-lg border-2 p-1.5 transition-all hover:bg-accent/50 disabled:opacity-50",
+22 -40
View File
@@ -8,17 +8,14 @@ import {SettingsEmailSection} from "@/features/settings/email-section";
import { SettingsStorageSection } from "@/features/settings/storage-section"; import { SettingsStorageSection } from "@/features/settings/storage-section";
import { StorageChannelWith } from "@/db/schema/12_storage-channel"; import { StorageChannelWith } from "@/db/schema/12_storage-channel";
import { AlarmClock, MailboxIcon, Save, UserCircle } from "lucide-react"; import { AlarmClock, MailboxIcon, Save, UserCircle } from "lucide-react";
import { import { SettingsNotificationSection } from "@/features/settings/notification-section";
SettingsNotificationSection
} from "@/features/settings/notification-section";
import { NotificationChannelWith } from "@/db/schema/09_notification-channel"; import { NotificationChannelWith } from "@/db/schema/09_notification-channel";
import { SettingsAvatarSection } from "@/features/settings/avatar-section"; import { SettingsAvatarSection } from "@/features/settings/avatar-section";
export type SettingsTabsProps = { export type SettingsTabsProps = {
settings: Setting settings: Setting;
storageChannels: StorageChannelWith[], storageChannels: StorageChannelWith[];
notificationChannels: NotificationChannelWith[]; notificationChannels: NotificationChannelWith[];
}; };
export const SettingsTabs = ({ settings, storageChannels, notificationChannels }: SettingsTabsProps) => { export const SettingsTabs = ({ settings, storageChannels, notificationChannels }: SettingsTabsProps) => {
@@ -27,7 +24,6 @@ export const SettingsTabs = ({settings, storageChannels, notificationChannels}:
const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "email"); const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "email");
useEffect(() => { useEffect(() => {
const newTab = searchParams.get("tab") ?? "email"; const newTab = searchParams.get("tab") ?? "email";
setTab(newTab); setTab(newTab);
@@ -37,65 +33,51 @@ export const SettingsTabs = ({settings, storageChannels, notificationChannels}:
router.push(`?tab=${value}`); router.push(`?tab=${value}`);
}; };
const tabs = [ const tabs = [
{ {
name: 'System Email', name: "System Email",
value: 'email', value: "email",
icon: MailboxIcon, icon: MailboxIcon,
content: ( content: <SettingsEmailSection settings={settings} />,
<SettingsEmailSection settings={settings}/>
)
}, },
{ {
name: 'Storage', name: "Storage",
value: 'storage', value: "storage",
icon: Save, icon: Save,
content: ( content: <SettingsStorageSection storageChannels={storageChannels} settings={settings} />,
<SettingsStorageSection storageChannels={storageChannels} settings={settings}/>
)
}, },
{ {
name: 'Notification', name: "Notification",
value: 'notification', value: "notification",
icon: AlarmClock, icon: AlarmClock,
content: ( content: <SettingsNotificationSection notificationChannels={notificationChannels} settings={settings} />,
<SettingsNotificationSection notificationChannels={notificationChannels} settings={settings}/>
)
}, },
{ {
name: 'Avatar', name: "Avatar",
value: 'avatar', value: "avatar",
icon: UserCircle, icon: UserCircle,
content: ( content: <SettingsAvatarSection settings={settings} />,
<SettingsAvatarSection settings={settings}/> },
) ];
}
]
return ( return (
<div className="h-full mt-3"> <div className="h-full mt-3">
<Tabs className="h-full gap-4" value={tab} onValueChange={handleChangeTab}> <Tabs className="h-full gap-4" value={tab} onValueChange={handleChangeTab}>
<TabsList className="w-full sm:w-fit"> <TabsList className="w-full">
{tabs.map(({ icon: Icon, name, value }) => ( {tabs.map(({ icon: Icon, name, value }) => (
<TabsTrigger key={value} value={value} className='flex items-center gap-1 px-2.5 sm:px-3 flex-1 sm:flex-none'> <TabsTrigger key={value} value={value} className="w-full flex items-center gap-1.5">
<Icon className="size-4 shrink-0" /> <Icon className="size-4 shrink-0" />
<span className="hidden sm:inline">{name}</span> <span className="hidden sm:inline">{name}</span>
</TabsTrigger> </TabsTrigger>
))} ))}
</TabsList> </TabsList>
{tabs.map(tab => ( {tabs.map((t) => (
<TabsContent key={tab.value} value={tab.value} className="h-full"> <TabsContent key={t.value} value={t.value} className="h-full">
{tab.content} {t.content}
</TabsContent> </TabsContent>
))} ))}
</Tabs> </Tabs>
</div> </div>
); );
}; };
+80 -26
View File
@@ -15,7 +15,8 @@ type ParsedVersion = {
function parseVersion(version: string): ParsedVersion { function parseVersion(version: string): ParsedVersion {
const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/); const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/);
if (!match) return { if (!match)
return {
major: 0, major: 0,
minor: 0, minor: 0,
patch: 0, patch: 0,
@@ -30,7 +31,10 @@ function parseVersion(version: string): ParsedVersion {
}; };
} }
const findLatestVersion = (currentVersion: string, releases: GitHubRelease[]): GitHubRelease | null => { const findLatestVersion = (
currentVersion: string,
releases: GitHubRelease[],
): GitHubRelease | null => {
const cleanCurrentVersion = currentVersion.replace(/^v/, ""); const cleanCurrentVersion = currentVersion.replace(/^v/, "");
const isStable = /^\d+\.\d+\.\d+$/.test(cleanCurrentVersion); const isStable = /^\d+\.\d+\.\d+$/.test(cleanCurrentVersion);
const isRc = /^\d+\.\d+\.\d+-rc\.\d+$/.test(cleanCurrentVersion); const isRc = /^\d+\.\d+\.\d+-rc\.\d+$/.test(cleanCurrentVersion);
@@ -38,69 +42,119 @@ const findLatestVersion = (currentVersion: string, releases: GitHubRelease[]): G
const currentParsedVersion = parseVersion(cleanCurrentVersion); const currentParsedVersion = parseVersion(cleanCurrentVersion);
if (isRc) { if (isRc) {
const latestStableVersion = releases.find(r => !r.prerelease); const latestStableVersion = releases.find((r) => !r.prerelease);
const latestRcVersion = releases.find(r => r.prerelease && (r.tag_name.includes("rc") || (r.name && r.name.includes("rc")))); const latestRcVersion = releases.find(
(r) =>
r.prerelease &&
(r.tag_name.includes("rc") || (r.name && r.name.includes("rc"))),
);
if (latestStableVersion) { if (latestStableVersion) {
const versionStr = latestStableVersion.tag_name || latestStableVersion.name; const versionStr =
latestStableVersion.tag_name || latestStableVersion.name;
const latestStableParsedVersion = parseVersion(versionStr); const latestStableParsedVersion = parseVersion(versionStr);
if (latestStableParsedVersion.major > currentParsedVersion.major) return latestStableVersion; if (latestStableParsedVersion.major > currentParsedVersion.major)
if (latestStableParsedVersion.major === currentParsedVersion.major && latestStableParsedVersion.minor > currentParsedVersion.minor) return latestStableVersion; return latestStableVersion;
if (latestStableParsedVersion.major === currentParsedVersion.major && latestStableParsedVersion.minor === currentParsedVersion.minor && latestStableParsedVersion.patch >= currentParsedVersion.patch) return latestStableVersion; if (
latestStableParsedVersion.major === currentParsedVersion.major &&
latestStableParsedVersion.minor > currentParsedVersion.minor
)
return latestStableVersion;
if (
latestStableParsedVersion.major === currentParsedVersion.major &&
latestStableParsedVersion.minor === currentParsedVersion.minor &&
latestStableParsedVersion.patch >= currentParsedVersion.patch
)
return latestStableVersion;
} }
if (latestRcVersion) { if (latestRcVersion) {
const versionStr = latestRcVersion.tag_name || latestRcVersion.name; const versionStr = latestRcVersion.tag_name || latestRcVersion.name;
const latestRcParsedVersion = parseVersion(versionStr); const latestRcParsedVersion = parseVersion(versionStr);
if (latestRcParsedVersion.major > currentParsedVersion.major) return latestRcVersion; if (latestRcParsedVersion.major > currentParsedVersion.major)
if (latestRcParsedVersion.major === currentParsedVersion.major && latestRcParsedVersion.minor > currentParsedVersion.minor) return latestRcVersion; return latestRcVersion;
if (latestRcParsedVersion.major === currentParsedVersion.major && latestRcParsedVersion.minor === currentParsedVersion.minor && latestRcParsedVersion.patch > currentParsedVersion.patch) return latestRcVersion; if (
if (latestRcParsedVersion.major === currentParsedVersion.major && latestRcParsedVersion.minor === currentParsedVersion.minor && latestRcParsedVersion.patch === currentParsedVersion.patch && latestRcParsedVersion.major === currentParsedVersion.major &&
latestRcParsedVersion.rc !== undefined && currentParsedVersion.rc !== undefined && latestRcParsedVersion.rc > currentParsedVersion.rc) return latestRcVersion; latestRcParsedVersion.minor > currentParsedVersion.minor
)
return latestRcVersion;
if (
latestRcParsedVersion.major === currentParsedVersion.major &&
latestRcParsedVersion.minor === currentParsedVersion.minor &&
latestRcParsedVersion.patch > currentParsedVersion.patch
)
return latestRcVersion;
if (
latestRcParsedVersion.major === currentParsedVersion.major &&
latestRcParsedVersion.minor === currentParsedVersion.minor &&
latestRcParsedVersion.patch === currentParsedVersion.patch &&
latestRcParsedVersion.rc !== undefined &&
currentParsedVersion.rc !== undefined &&
latestRcParsedVersion.rc > currentParsedVersion.rc
)
return latestRcVersion;
} }
} else if (isStable) { } else if (isStable) {
const latestStableVersion = releases.find(r => !r.prerelease); const latestStableVersion = releases.find((r) => !r.prerelease);
if (latestStableVersion) { if (latestStableVersion) {
const versionStr = latestStableVersion.tag_name || latestStableVersion.name; const versionStr =
latestStableVersion.tag_name || latestStableVersion.name;
const latestStableParsedVersion = parseVersion(versionStr); const latestStableParsedVersion = parseVersion(versionStr);
if (latestStableParsedVersion.major > currentParsedVersion.major) return latestStableVersion; if (latestStableParsedVersion.major > currentParsedVersion.major)
if (latestStableParsedVersion.major === currentParsedVersion.major && latestStableParsedVersion.minor > currentParsedVersion.minor) return latestStableVersion; return latestStableVersion;
if (latestStableParsedVersion.major === currentParsedVersion.major && latestStableParsedVersion.minor === currentParsedVersion.minor && latestStableParsedVersion.patch > currentParsedVersion.patch) return latestStableVersion; if (
latestStableParsedVersion.major === currentParsedVersion.major &&
latestStableParsedVersion.minor > currentParsedVersion.minor
)
return latestStableVersion;
if (
latestStableParsedVersion.major === currentParsedVersion.major &&
latestStableParsedVersion.minor === currentParsedVersion.minor &&
latestStableParsedVersion.patch > currentParsedVersion.patch
)
return latestStableVersion;
} }
} }
return null; return null;
}; };
export const getNewRelease = async (currentVersion: string): Promise<GitHubRelease | null> => { export const getNewRelease = async (
currentVersion: string,
): Promise<GitHubRelease | null> => {
try { try {
const response = await fetch("https://api.github.com/repos/Portabase/portabase/releases"); const response = await fetch(
"https://api.github.com/repos/Portabase/portabase/releases",
);
if (!response.ok) { if (!response.ok) {
return null; return null;
} }
const releases: GitHubRelease[] = await response.json(); const releases: GitHubRelease[] = await response.json();
return findLatestVersion(currentVersion, releases); return findLatestVersion(currentVersion, releases);
} catch (error) { } catch (error) {
console.error("Failed to fetch latest release", error); console.error("Failed to fetch latest release", error);
return null; return null;
} }
}; };
export const getNewAgentRelease = async (currentVersion: string): Promise<GitHubRelease | null> => { export const getNewAgentRelease = async (
currentVersion: string,
): Promise<GitHubRelease | null> => {
try { try {
const response = await fetch("https://api.github.com/repos/Portabase/agent-rust/releases", { const response = await fetch(
next: { revalidate: 3600 } "https://api.github.com/repos/Portabase/agent-rust/releases",
}); {
next: { revalidate: 3600 },
},
);
if (!response.ok) { if (!response.ok) {
return null; return null;
} }
const releases: GitHubRelease[] = await response.json(); const releases: GitHubRelease[] = await response.json();
return findLatestVersion(currentVersion, releases); return findLatestVersion(currentVersion, releases);
} catch (error) { } catch (error) {
console.error("Failed to fetch latest agent release", error); console.error("Failed to fetch latest agent release", error);
return null; return null;
+10 -8
View File
@@ -14,8 +14,10 @@ function getLocalTimestamp() {
day: "2-digit", day: "2-digit",
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
second: "2-digit" second: "2-digit",
}).format(date).replace(" ", "T"); })
.format(date)
.replace(" ", "T");
return `,"time":"${formatted}"`; return `,"time":"${formatted}"`;
} }
@@ -29,8 +31,8 @@ export const logger: Logger = pino({
formatters: { formatters: {
level(label) { level(label) {
return { level: label.toUpperCase() }; return { level: label.toUpperCase() };
} },
} },
} }
: { : {
timestamp: getLocalTimestamp, timestamp: getLocalTimestamp,
@@ -40,8 +42,8 @@ export const logger: Logger = pino({
colorize: true, colorize: true,
translateTime: "yyyy-mm-dd HH:MM:ss", translateTime: "yyyy-mm-dd HH:MM:ss",
ignore: "pid,hostname", ignore: "pid,hostname",
levelFirst: true levelFirst: true,
} },
} },
}) }),
}); });
+9 -6
View File
@@ -1,5 +1,5 @@
import { db } from "@/db"; import { db } from "@/db";
import {and, eq, isNotNull, isNull} from "drizzle-orm"; import { and, eq, isNotNull } from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import { withUpdatedAt } from "@/db/utils"; import { withUpdatedAt } from "@/db/utils";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
@@ -11,18 +11,21 @@ export const backupCleanTask = async () => {
const backups = await db.query.backup.findMany({ const backups = await db.query.backup.findMany({
where: and( where: and(
isNotNull(drizzleDb.schemas.backup.deletedAt), isNotNull(drizzleDb.schemas.backup.deletedAt),
eq(drizzleDb.schemas.backup.status, "ongoing") eq(drizzleDb.schemas.backup.status, "ongoing"),
) ),
}); });
log.debug(`Backups to clean: ${backups.length}`); log.debug(`Backups to clean: ${backups.length}`);
for (const backup of backups) { for (const backup of backups) {
await db.update(drizzleDb.schemas.backup).set(withUpdatedAt({ await db
.update(drizzleDb.schemas.backup)
.set(
withUpdatedAt({
status: "failed", status: "failed",
})) }),
)
.where(eq(drizzleDb.schemas.backup.id, backup.id)); .where(eq(drizzleDb.schemas.backup.id, backup.id));
} }
} catch (e: any) { } catch (e: any) {
log.error({ name: "backupCleanTask", error: e }, `Backup cleanup failed`); log.error({ name: "backupCleanTask", error: e }, `Backup cleanup failed`);
throw e; throw e;
+1 -2
View File
@@ -32,9 +32,8 @@ export const retentionCleanTask = async () => {
export async function enforceRetention( export async function enforceRetention(
databaseId: string, databaseId: string,
policy: typeof retentionPolicy.$inferSelect policy: typeof retentionPolicy.$inferSelect,
) { ) {
;
log.info({ name: "enforceRetention" }, `Retention started for ${databaseId}`); log.info({ name: "enforceRetention" }, `Retention started for ${databaseId}`);
switch (policy.type) { switch (policy.type) {
+27 -20
View File
@@ -1,4 +1,4 @@
"use server" "use server";
import { action } from "@/lib/safe-actions/actions"; import { action } from "@/lib/safe-actions/actions";
import { z } from "zod"; import { z } from "zod";
import { ServerActionResult } from "@/types/action-type"; import { ServerActionResult } from "@/types/action-type";
@@ -10,22 +10,22 @@ import {withUpdatedAt} from "@/db/utils";
import type { StorageInput } from "@/features/storages/storages.types"; import type { StorageInput } from "@/features/storages/storages.types";
import { dispatchStorage } from "@/features/storages/storages.dispatch"; import { dispatchStorage } from "@/features/storages/storages.dispatch";
export const deleteBackupCronAction = action export const deleteBackupCronAction = action
.schema( .schema(
z.object({ z.object({
backupId: z.string(), backupId: z.string(),
databaseId: z.string(), databaseId: z.string(),
}) }),
) )
.action(async ({ parsedInput }): Promise<ServerActionResult<Backup>> => { .action(async ({ parsedInput }): Promise<ServerActionResult<Backup>> => {
try { try {
const backup = await db.query.backup.findFirst({ const backup = await db.query.backup.findFirst({
where: and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)) where: and(
eq(drizzleDb.schemas.backup.id, parsedInput.backupId),
eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId),
),
}); });
if (!backup) { if (!backup) {
return { return {
success: false, success: false,
@@ -34,23 +34,25 @@ export const deleteBackupCronAction = action
status: 404, status: 404,
messageParams: { backupId: parsedInput.backupId }, messageParams: { backupId: parsedInput.backupId },
}, },
};
} }
}
const backupStorages = await db.query.backupStorage.findMany({ const backupStorages = await db.query.backupStorage.findMany({
where: eq(drizzleDb.schemas.backupStorage.backupId, parsedInput.backupId), where: eq(
drizzleDb.schemas.backupStorage.backupId,
parsedInput.backupId,
),
}); });
for (const backupStorage of backupStorages) { for (const backupStorage of backupStorages) {
await db await db
.update(drizzleDb.schemas.backupStorage) .update(drizzleDb.schemas.backupStorage)
.set(withUpdatedAt({ .set(
withUpdatedAt({
deletedAt: new Date(), deletedAt: new Date(),
})) }),
.where(eq(drizzleDb.schemas.backupStorage.id, backupStorage.id)) )
.where(eq(drizzleDb.schemas.backupStorage.id, backupStorage.id));
if (backupStorage.status != "success" || !backupStorage.path) { if (backupStorage.status != "success" || !backupStorage.path) {
continue; continue;
@@ -68,12 +70,18 @@ export const deleteBackupCronAction = action
await db await db
.update(drizzleDb.schemas.backup) .update(drizzleDb.schemas.backup)
.set(withUpdatedAt({ .set(
withUpdatedAt({
deletedAt: new Date(), deletedAt: new Date(),
status: backup.status == "ongoing" ? "failed" : backup.status status: backup.status == "ongoing" ? "failed" : backup.status,
}),
})) )
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId))) .where(
and(
eq(drizzleDb.schemas.backup.id, parsedInput.backupId),
eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId),
),
);
return { return {
success: true, success: true,
@@ -81,7 +89,6 @@ export const deleteBackupCronAction = action
message: `Backup deleted successfully (${parsedInput.backupId}).`, message: `Backup deleted successfully (${parsedInput.backupId}).`,
}, },
}; };
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return { return {
+3 -3
View File
@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx" import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs));
} }
+5 -2
View File
@@ -2,7 +2,10 @@ import {NextResponse} from "next/server";
export const errorHandler = (error: any) => { export const errorHandler = (error: any) => {
return new NextResponse( return new NextResponse(
JSON.stringify({ message: 'An error occurred while processing your request.', status: 500 }), JSON.stringify({
{ status: 500, headers: { 'Content-Type': 'application/json' } } message: "An error occurred while processing your request.",
status: 500,
}),
{ status: 500, headers: { "Content-Type": "application/json" } },
); );
}; };
+7 -5
View File
@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from 'next/server'; import { logger } from "@/lib/logger";
import { NextRequest, NextResponse } from "next/server";
const LOG_LEVELS = ["debug", "info", "warn", "error"] as const; const LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
type LogLevel = typeof LOG_LEVELS[number]; type LogLevel = (typeof LOG_LEVELS)[number];
const raw = (process.env.LOG_LEVEL ?? "info") as LogLevel; const raw = (process.env.LOG_LEVEL ?? "info") as LogLevel;
const _idx = LOG_LEVELS.indexOf(raw); const _idx = LOG_LEVELS.indexOf(raw);
@@ -10,12 +11,13 @@ const LEVEL_INDEX = _idx === -1 ? 1 : _idx;
const STATUS_PATH_RE = /\/api\/agent\/[^/]+\/status\/?$/; const STATUS_PATH_RE = /\/api\/agent\/[^/]+\/status\/?$/;
export function loggingMiddleware(request: NextRequest) { export function loggingMiddleware(request: NextRequest) {
if (request.url.includes('/api')) { if (request.url.includes("/api")) {
const isStatusPing = STATUS_PATH_RE.test(request.nextUrl.pathname); const isStatusPing = STATUS_PATH_RE.test(request.nextUrl.pathname);
// Status pings are debug-level; all other API calls are info-level.
const requiredIndex = isStatusPing ? 0 : 1; const requiredIndex = isStatusPing ? 0 : 1;
if (LEVEL_INDEX <= requiredIndex) { if (LEVEL_INDEX <= requiredIndex) {
console.log(`[API] Received ${request.method} request : ${request.url} at ${new Date()}`); logger.info(
`[API] Received ${request.method} request : ${request.url} at ${new Date()}`,
);
} }
} }
return NextResponse.next(); return NextResponse.next();
+4 -1
View File
@@ -15,7 +15,10 @@ export type ActionErrorMessage = {
messageParams?: Record<string, string | number>; messageParams?: Record<string, string | number>;
}; };
export type ActionSuccessMessage = { message?: string; messageParams?: Record<string, string | number> }; export type ActionSuccessMessage = {
message?: string;
messageParams?: Record<string, string | number>;
};
export type ActionResult<T> = SafeActionResult< export type ActionResult<T> = SafeActionResult<
string, string,
+8 -9
View File
@@ -1,13 +1,12 @@
export type SignUpUser = { export type SignUpUser = {
name: string name: string;
email: string email: string;
password: string password: string;
callbackURL?: string callbackURL?: string;
role?: string role?: string;
theme: string theme: string;
emailVerified?: boolean emailVerified?: boolean;
} };
export type BetterAuthError = { export type BetterAuthError = {
code?: string; code?: string;
+13 -11
View File
@@ -1,31 +1,33 @@
import {MemberWithUser, Organization, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization"; import {
MemberWithUser,
Organization,
OrganizationWithMembersAndUsers,
} from "@/db/schema/03_organization";
import { OrganizationMember } from "@/db/schema/04_member"; import { OrganizationMember } from "@/db/schema/04_member";
import { OrganizationInvitation } from "@/db/schema/05_invitation"; import { OrganizationInvitation } from "@/db/schema/05_invitation";
import { User } from "@/db/schema/02_user"; import { User } from "@/db/schema/02_user";
export function buildOrganizationWithMembers( export function buildOrganizationWithMembers(
rows: { rows: {
organization: Organization; organization: Organization;
member: OrganizationMember | null; member: OrganizationMember | null;
invitation: OrganizationInvitation | null; invitation: OrganizationInvitation | null;
user: User | null; user: User | null;
}[] }[],
): OrganizationWithMembersAndUsers | null { ): OrganizationWithMembersAndUsers | null {
if (rows.length === 0) return null; if (rows.length === 0) return null;
const org = rows[0].organization; const org = rows[0].organization;
const invitations: OrganizationInvitation[] = rows const invitations: OrganizationInvitation[] = rows
.filter(r => r.invitation) .filter((r) => r.invitation)
.map(r => ({ .map((r) => ({
...r.invitation!, ...r.invitation!,
})); }));
const members: MemberWithUser[] = rows const members: MemberWithUser[] = rows
.filter(r => r.member && r.user) .filter((r) => r.member && r.user)
.map(r => ({ .map((r) => ({
...r.member!, ...r.member!,
user: r.user!, user: r.user!,
})); }));
@@ -37,7 +39,6 @@ export function buildOrganizationWithMembers(
}; };
} }
export function getFileExtension(dbType: string) { export function getFileExtension(dbType: string) {
switch (dbType) { switch (dbType) {
case "postgresql": case "postgresql":
@@ -49,7 +50,9 @@ export function getFileExtension(dbType: string) {
} }
} }
export function getFileHeadersBasedOnDbms(dbType: string): Record<string, string[]> { export function getFileHeadersBasedOnDbms(
dbType: string,
): Record<string, string[]> {
switch (dbType) { switch (dbType) {
case "postgresql": case "postgresql":
return { return {
@@ -84,6 +87,5 @@ export function getFileHeadersBasedOnDbms(dbType: string): Record<string, string
}; };
default: default:
throw new Error(`Unsupported database type: ${dbType}`); throw new Error(`Unsupported database type: ${dbType}`);
} }
} }
-2
View File
@@ -5,10 +5,8 @@ import {eq} from "drizzle-orm";
import { StorageProviderKind } from "@/features/storages/storages.types"; import { StorageProviderKind } from "@/features/storages/storages.types";
import { withUpdatedAt } from "@/db/utils"; import { withUpdatedAt } from "@/db/utils";
export async function createSettingsIfNotExist() { export async function createSettingsIfNotExist() {
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
const systemSettingsValues = { const systemSettingsValues = {
name: "system", name: "system",
smtpPassword: env.SMTP_PASSWORD ?? null, smtpPassword: env.SMTP_PASSWORD ?? null,
+3 -10
View File
@@ -1,6 +1,6 @@
import { db } from "@/db"; import { db } from "@/db";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {count, eq} from "drizzle-orm"; import { eq } from "drizzle-orm";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
import type { SignUpUser } from "@/types/auth"; import type { SignUpUser } from "@/types/auth";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
@@ -9,16 +9,13 @@ import {createUserDb} from "@/db/services/user";
const log = logger.child({ module: "init/user" }); const log = logger.child({ module: "init/user" });
export async function createDefaultUser() { export async function createDefaultUser() {
const hasRequiredEnv = const hasRequiredEnv =
env.AUTH_DEFAULT_USER && env.AUTH_DEFAULT_USER &&
env.AUTH_DEFAULT_PASSWORD && env.AUTH_DEFAULT_PASSWORD &&
env.AUTH_DEFAULT_USER_NAME; env.AUTH_DEFAULT_USER_NAME;
if (!hasRequiredEnv) { if (!hasRequiredEnv) {
log.info( log.info("Default admin creation skipped: missing environment variables.");
"Default admin creation skipped: missing environment variables.",
);
return; return;
} }
@@ -44,7 +41,6 @@ export async function createDefaultUser() {
emailVerified: true, emailVerified: true,
}; };
const newUser = await createUserDb(userData); const newUser = await createUserDb(userData);
if (newUser) { if (newUser) {
@@ -62,10 +58,7 @@ export async function createDefaultUser() {
role: "owner", role: "owner",
}); });
} else { } else {
log.warn( log.warn("Default organization not found. Cannot assign member.");
"Default organization not found. Cannot assign member.",
);
} }
} }
} }