mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
chore: checkpoint before features reorganization
This commit is contained in:
+1
-5
@@ -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"
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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 "Connect a storage" step first.
|
Go back and configure storages in the "Connect an external storage" step first.
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" } },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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}`);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user