mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix
This commit is contained in:
+5
-2
@@ -1,4 +1,5 @@
|
|||||||
import BackButton from "@/components/common/back-button";
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
export default async function NotFound() {
|
export default async function NotFound() {
|
||||||
return (
|
return (
|
||||||
@@ -12,7 +13,9 @@ export default async function NotFound() {
|
|||||||
The content you are trying to view is not available.
|
The content you are trying to view is not available.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<BackButton>Go home</BackButton>
|
<Button asChild>
|
||||||
|
<Link href="/">Go home</Link>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
type BackButtonProps = React.ComponentProps<typeof Button> & {
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function BackButton({ children, ...props }: BackButtonProps) {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button onClick={() => router.back()} aria-label={children?.toString()} {...props}>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { and, eq, inArray, ne } from "drizzle-orm";
|
||||||
import { member } from "@/db/schema/04_member";
|
import { member } from "@/db/schema/04_member";
|
||||||
import { organization } from "@/db/schema/03_organization";
|
import { organization } from "@/db/schema/03_organization";
|
||||||
|
|
||||||
@@ -15,3 +15,15 @@ export async function getUserOrganization(userId: string) {
|
|||||||
where: eq(organization.id, memberRow.organizationId),
|
where: eq(organization.id, memberRow.organizationId),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserOwnOrganization(userId: string) {
|
||||||
|
const memberRows = await db.query.member.findMany({
|
||||||
|
columns: { organizationId: true },
|
||||||
|
where: eq(member.userId, userId),
|
||||||
|
});
|
||||||
|
if (!memberRows.length) return null;
|
||||||
|
const orgIds = memberRows.map((r) => r.organizationId);
|
||||||
|
return db.query.organization.findFirst({
|
||||||
|
where: and(inArray(organization.id, orgIds), ne(organization.slug, "default")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useOnboarding } from "@onboardjs/react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
createOrganizationAction,
|
createOrganizationAction,
|
||||||
getMyOrganizationAction,
|
|
||||||
updateOrganizationAction,
|
updateOrganizationAction,
|
||||||
} from "@/features/organizations/organization.action";
|
} from "@/features/organizations/organization.action";
|
||||||
import { slugify } from "@/utils/slugify";
|
import { slugify } from "@/utils/slugify";
|
||||||
@@ -18,18 +17,10 @@ export const useCreateOrg = () => {
|
|||||||
const trimmed = name.trim();
|
const trimmed = name.trim();
|
||||||
if (!trimmed) throw new Error("Organisation name is required");
|
if (!trimmed) throw new Error("Organisation name is required");
|
||||||
|
|
||||||
let existingOrg = state?.context.flowData.org as
|
const existingOrg = state?.context.flowData.org as
|
||||||
| { id: string; name: string }
|
| { id: string; name: string }
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (!existingOrg) {
|
|
||||||
const fetchResult = await getMyOrganizationAction({});
|
|
||||||
const fetchData = fetchResult?.data;
|
|
||||||
if (fetchData?.success && fetchData.value) {
|
|
||||||
existingOrg = { id: fetchData.value.id, name: fetchData.value.name };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingOrg) {
|
if (existingOrg) {
|
||||||
const result = await updateOrganizationAction({
|
const result = await updateOrganizationAction({
|
||||||
organizationId: existingOrg.id,
|
organizationId: existingOrg.id,
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import { member } from "@/db/schema/04_member";
|
|||||||
import { currentUser } from "@/lib/auth/current-user";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
import { getSettings } from "@/db/services/setting";
|
import { getSettings } from "@/db/services/setting";
|
||||||
import { hasUsers } from "@/db/services/user";
|
import { hasUsers } from "@/db/services/user";
|
||||||
import { getUserOrganization } from "@/db/services/organization";
|
import {
|
||||||
|
getUserOrganization,
|
||||||
|
getUserOwnOrganization,
|
||||||
|
} from "@/db/services/organization";
|
||||||
import { getOrganizationProject } from "@/db/services/project";
|
import { getOrganizationProject } from "@/db/services/project";
|
||||||
import { getOrganizationAgents } from "@/db/services/agent";
|
import { getOrganizationAgents } from "@/db/services/agent";
|
||||||
import { getDatabasesSettings } from "@/db/services/database";
|
import { getDatabasesSettings } from "@/db/services/database";
|
||||||
@@ -20,6 +23,8 @@ import type {
|
|||||||
OnboardingFlowData,
|
OnboardingFlowData,
|
||||||
OnboardingMeta,
|
OnboardingMeta,
|
||||||
} from "@/features/onboarding/types";
|
} from "@/features/onboarding/types";
|
||||||
|
import { getOAuthProviders } from "@/lib/auth/oauth";
|
||||||
|
import { getOidcProviders } from "@/lib/auth/oidc";
|
||||||
import { generateEdgeKey } from "@/utils/edge_key";
|
import { generateEdgeKey } from "@/utils/edge_key";
|
||||||
import { getServerUrl } from "@/utils/get-server-url";
|
import { getServerUrl } from "@/utils/get-server-url";
|
||||||
|
|
||||||
@@ -38,26 +43,33 @@ export async function resolveOnboardingState(): Promise<ResolvedOnboardingState>
|
|||||||
emailPasswordEnabled: env.AUTH_EMAIL_PASSWORD_ENABLED === "true",
|
emailPasswordEnabled: env.AUTH_EMAIL_PASSWORD_ENABLED === "true",
|
||||||
hasExistingUsers: false,
|
hasExistingUsers: false,
|
||||||
ssoProviders: [],
|
ssoProviders: [],
|
||||||
|
oauthProviders: [],
|
||||||
defaultUserMode: !!(env.AUTH_DEFAULT_USER && env.AUTH_DEFAULT_PASSWORD),
|
defaultUserMode: !!(env.AUTH_DEFAULT_USER && env.AUTH_DEFAULT_PASSWORD),
|
||||||
resumeStepId: "login",
|
resumeStepId: "login",
|
||||||
};
|
};
|
||||||
|
|
||||||
meta.hasExistingUsers = await hasUsers();
|
meta.hasExistingUsers = await hasUsers();
|
||||||
|
|
||||||
if (env.AUTH_OIDC_CLIENT) {
|
for (const p of getOidcProviders()) {
|
||||||
meta.ssoProviders.push({
|
meta.ssoProviders.push({
|
||||||
id: env.AUTH_OIDC_ID ?? "oidc",
|
id: p.id,
|
||||||
label: env.AUTH_OIDC_TITLE ?? "SSO",
|
label: p.title,
|
||||||
|
icon: p.icon,
|
||||||
|
description: p.description || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const p of getOAuthProviders()) {
|
||||||
|
meta.oauthProviders.push({ id: p.id, label: p.title, icon: p.icon });
|
||||||
|
}
|
||||||
|
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return { stepId: "login", flowData: { meta } };
|
return { stepId: "login", flowData: { meta } };
|
||||||
}
|
}
|
||||||
|
|
||||||
let org = await getUserOrganization(user.id);
|
const inDefaultOrg = await getUserOrganization(user.id);
|
||||||
if (!org) {
|
if (!inDefaultOrg) {
|
||||||
const defaultOrg = await db.query.organization.findFirst({
|
const defaultOrg = await db.query.organization.findFirst({
|
||||||
where: eq(organization.slug, "default"),
|
where: eq(organization.slug, "default"),
|
||||||
});
|
});
|
||||||
@@ -67,13 +79,15 @@ export async function resolveOnboardingState(): Promise<ResolvedOnboardingState>
|
|||||||
organizationId: defaultOrg.id,
|
organizationId: defaultOrg.id,
|
||||||
role: "owner",
|
role: "owner",
|
||||||
});
|
});
|
||||||
org = defaultOrg;
|
|
||||||
} else {
|
|
||||||
meta.resumeStepId = "preferences";
|
|
||||||
return { stepId: "preferences", flowData: { meta } };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const org = await getUserOwnOrganization(user.id);
|
||||||
|
if (!org) {
|
||||||
|
meta.resumeStepId = "preferences";
|
||||||
|
return { stepId: "preferences", flowData: { meta } };
|
||||||
|
}
|
||||||
|
|
||||||
const orgData = { id: org.id, name: org.name };
|
const orgData = { id: org.id, name: org.name };
|
||||||
|
|
||||||
const [notifierChannels, storageChannels, agents, project] =
|
const [notifierChannels, storageChannels, agents, project] =
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useMutation } from "@tanstack/react-query";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Globe, Mail, KeyRound, ShieldAlert } from "lucide-react";
|
import { Globe, Mail, KeyRound, ShieldAlert } from "lucide-react";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { PasswordInput } from "@/components/ui/password-input";
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
@@ -38,8 +39,9 @@ export const StepLogin = () => {
|
|||||||
const passkeyEnabled = meta?.passkeyEnabled ?? false;
|
const passkeyEnabled = meta?.passkeyEnabled ?? false;
|
||||||
const emailPasswordEnabled = meta?.emailPasswordEnabled ?? false;
|
const emailPasswordEnabled = meta?.emailPasswordEnabled ?? false;
|
||||||
const hasAnySsoProvider = (meta?.ssoProviders?.length ?? 0) > 0;
|
const hasAnySsoProvider = (meta?.ssoProviders?.length ?? 0) > 0;
|
||||||
|
const hasAnyOAuthProvider = (meta?.oauthProviders?.length ?? 0) > 0;
|
||||||
const hasAnyAuthMethod =
|
const hasAnyAuthMethod =
|
||||||
emailPasswordEnabled || passkeyEnabled || hasAnySsoProvider;
|
emailPasswordEnabled || passkeyEnabled || hasAnySsoProvider || hasAnyOAuthProvider;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session?.user) {
|
if (session?.user) {
|
||||||
@@ -76,11 +78,21 @@ export const StepLogin = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSso = async (providerId: string) => {
|
const handleOAuth = async (providerId: string) => {
|
||||||
const result = await signIn.social({
|
const result = await signIn.social({
|
||||||
provider: providerId as any,
|
provider: providerId as any,
|
||||||
callbackURL: "/welcome",
|
callbackURL: "/welcome",
|
||||||
});
|
});
|
||||||
|
if (result?.error)
|
||||||
|
toast.error(result.error.message ?? "OAuth sign in failed");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSso = async (providerId: string) => {
|
||||||
|
const result = await signIn.sso({
|
||||||
|
providerId,
|
||||||
|
providerType: "oidc",
|
||||||
|
callbackURL: "/welcome",
|
||||||
|
});
|
||||||
if (result?.error)
|
if (result?.error)
|
||||||
toast.error(result.error.message ?? "SSO sign in failed");
|
toast.error(result.error.message ?? "SSO sign in failed");
|
||||||
};
|
};
|
||||||
@@ -129,7 +141,7 @@ export const StepLogin = () => {
|
|||||||
Set up your instance by creating the first account.
|
Set up your instance by creating the first account.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{hasAnySsoProvider && (
|
{(hasAnySsoProvider || hasAnyOAuthProvider) && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{meta?.ssoProviders.map((provider) => (
|
{meta?.ssoProviders.map((provider) => (
|
||||||
<button
|
<button
|
||||||
@@ -139,7 +151,29 @@ export const StepLogin = () => {
|
|||||||
className="flex items-center gap-3 rounded-lg border border-border p-3 text-sm hover:bg-accent/50 hover:border-primary/20 transition-colors w-full"
|
className="flex items-center gap-3 rounded-lg border border-border p-3 text-sm hover:bg-accent/50 hover:border-primary/20 transition-colors w-full"
|
||||||
>
|
>
|
||||||
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center shrink-0">
|
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center shrink-0">
|
||||||
<Globe className="size-4 text-muted-foreground" />
|
{provider.icon ? (
|
||||||
|
<Icon icon={provider.icon} className="size-4" />
|
||||||
|
) : (
|
||||||
|
<Globe className="size-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-start text-left">
|
||||||
|
<span>Continue with {provider.label}</span>
|
||||||
|
{provider.description && (
|
||||||
|
<span className="text-xs text-muted-foreground">{provider.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{meta?.oauthProviders.map((provider) => (
|
||||||
|
<button
|
||||||
|
key={provider.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleOAuth(provider.id)}
|
||||||
|
className="flex items-center gap-3 rounded-lg border border-border p-3 text-sm hover:bg-accent/50 hover:border-primary/20 transition-colors w-full"
|
||||||
|
>
|
||||||
|
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center shrink-0">
|
||||||
|
<Icon icon={provider.icon} className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<span>Continue with {provider.label}</span>
|
<span>Continue with {provider.label}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -174,7 +208,7 @@ export const StepLogin = () => {
|
|||||||
: "Your session expired. Sign in to continue where you left off."}
|
: "Your session expired. Sign in to continue where you left off."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{hasAnySsoProvider && !meta?.defaultUserMode && (
|
{(hasAnySsoProvider || hasAnyOAuthProvider) && !meta?.defaultUserMode && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{meta?.ssoProviders.map((provider) => (
|
{meta?.ssoProviders.map((provider) => (
|
||||||
<button
|
<button
|
||||||
@@ -184,7 +218,29 @@ export const StepLogin = () => {
|
|||||||
className="flex items-center gap-3 rounded-lg border border-border p-3 text-sm hover:bg-accent/50 hover:border-primary/20 transition-colors w-full"
|
className="flex items-center gap-3 rounded-lg border border-border p-3 text-sm hover:bg-accent/50 hover:border-primary/20 transition-colors w-full"
|
||||||
>
|
>
|
||||||
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center shrink-0">
|
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center shrink-0">
|
||||||
<Globe className="size-4 text-muted-foreground" />
|
{provider.icon ? (
|
||||||
|
<Icon icon={provider.icon} className="size-4" />
|
||||||
|
) : (
|
||||||
|
<Globe className="size-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-start text-left">
|
||||||
|
<span>Continue with {provider.label}</span>
|
||||||
|
{provider.description && (
|
||||||
|
<span className="text-xs text-muted-foreground">{provider.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{meta?.oauthProviders.map((provider) => (
|
||||||
|
<button
|
||||||
|
key={provider.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleOAuth(provider.id)}
|
||||||
|
className="flex items-center gap-3 rounded-lg border border-border p-3 text-sm hover:bg-accent/50 hover:border-primary/20 transition-colors w-full"
|
||||||
|
>
|
||||||
|
<div className="size-9 rounded-md border bg-muted/50 shadow-sm flex items-center justify-center shrink-0">
|
||||||
|
<Icon icon={provider.icon} className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<span>Continue with {provider.label}</span>
|
<span>Continue with {provider.label}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
export type OnboardingSsoProvider = { id: string; label: string };
|
export type OnboardingSsoProvider = { id: string; label: string; icon: string; description?: string };
|
||||||
|
export type OnboardingOAuthProvider = { id: string; label: string; icon: string };
|
||||||
|
|
||||||
export type OnboardingMeta = {
|
export type OnboardingMeta = {
|
||||||
passkeyEnabled: boolean;
|
passkeyEnabled: boolean;
|
||||||
hasExistingUsers: boolean;
|
hasExistingUsers: boolean;
|
||||||
ssoProviders: OnboardingSsoProvider[];
|
ssoProviders: OnboardingSsoProvider[];
|
||||||
|
oauthProviders: OnboardingOAuthProvider[];
|
||||||
defaultUserMode: boolean;
|
defaultUserMode: boolean;
|
||||||
resumeStepId: string;
|
resumeStepId: string;
|
||||||
emailPasswordEnabled: boolean;
|
emailPasswordEnabled: boolean;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function ProfileAppearance() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ThemeSelector() {
|
function ThemeSelector() {
|
||||||
const {theme} = useTheme();
|
const {theme, setTheme} = useTheme();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -39,6 +39,7 @@ function ThemeSelector() {
|
|||||||
isActive ? "border-primary bg-primary/5" : "border-muted/40"
|
isActive ? "border-primary bg-primary/5" : "border-muted/40"
|
||||||
)}
|
)}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
setTheme(item.value);
|
||||||
await authClient.updateUser({theme: item.value});
|
await authClient.updateUser({theme: item.value});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user