This commit is contained in:
Théo LAGACHE
2026-06-23 11:34:20 +02:00
parent fd41f7ea50
commit 866bc76475
8 changed files with 110 additions and 49 deletions
+5 -2
View File
@@ -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() {
return (
@@ -12,7 +13,9 @@ export default async function NotFound() {
The content you are trying to view is not available.
</p>
</div>
<BackButton>Go home</BackButton>
<Button asChild>
<Link href="/">Go home</Link>
</Button>
</div>
</div>
);
-18
View File
@@ -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>
);
}
+13 -1
View File
@@ -1,7 +1,7 @@
"use server";
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 { organization } from "@/db/schema/03_organization";
@@ -15,3 +15,15 @@ export async function getUserOrganization(userId: string) {
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 {
createOrganizationAction,
getMyOrganizationAction,
updateOrganizationAction,
} from "@/features/organizations/organization.action";
import { slugify } from "@/utils/slugify";
@@ -18,18 +17,10 @@ export const useCreateOrg = () => {
const trimmed = name.trim();
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 }
| 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) {
const result = await updateOrganizationAction({
organizationId: existingOrg.id,
+24 -10
View File
@@ -7,7 +7,10 @@ import { member } from "@/db/schema/04_member";
import { currentUser } from "@/lib/auth/current-user";
import { getSettings } from "@/db/services/setting";
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 { getOrganizationAgents } from "@/db/services/agent";
import { getDatabasesSettings } from "@/db/services/database";
@@ -20,6 +23,8 @@ import type {
OnboardingFlowData,
OnboardingMeta,
} from "@/features/onboarding/types";
import { getOAuthProviders } from "@/lib/auth/oauth";
import { getOidcProviders } from "@/lib/auth/oidc";
import { generateEdgeKey } from "@/utils/edge_key";
import { getServerUrl } from "@/utils/get-server-url";
@@ -38,26 +43,33 @@ export async function resolveOnboardingState(): Promise<ResolvedOnboardingState>
emailPasswordEnabled: env.AUTH_EMAIL_PASSWORD_ENABLED === "true",
hasExistingUsers: false,
ssoProviders: [],
oauthProviders: [],
defaultUserMode: !!(env.AUTH_DEFAULT_USER && env.AUTH_DEFAULT_PASSWORD),
resumeStepId: "login",
};
meta.hasExistingUsers = await hasUsers();
if (env.AUTH_OIDC_CLIENT) {
for (const p of getOidcProviders()) {
meta.ssoProviders.push({
id: env.AUTH_OIDC_ID ?? "oidc",
label: env.AUTH_OIDC_TITLE ?? "SSO",
id: p.id,
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();
if (!user) {
return { stepId: "login", flowData: { meta } };
}
let org = await getUserOrganization(user.id);
if (!org) {
const inDefaultOrg = await getUserOrganization(user.id);
if (!inDefaultOrg) {
const defaultOrg = await db.query.organization.findFirst({
where: eq(organization.slug, "default"),
});
@@ -67,13 +79,15 @@ export async function resolveOnboardingState(): Promise<ResolvedOnboardingState>
organizationId: defaultOrg.id,
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 [notifierChannels, storageChannels, agents, project] =
+62 -6
View File
@@ -7,6 +7,7 @@ import { useMutation } from "@tanstack/react-query";
import { z } from "zod";
import { toast } from "sonner";
import { Globe, Mail, KeyRound, ShieldAlert } from "lucide-react";
import { Icon } from "@iconify/react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/ui/password-input";
@@ -38,8 +39,9 @@ export const StepLogin = () => {
const passkeyEnabled = meta?.passkeyEnabled ?? false;
const emailPasswordEnabled = meta?.emailPasswordEnabled ?? false;
const hasAnySsoProvider = (meta?.ssoProviders?.length ?? 0) > 0;
const hasAnyOAuthProvider = (meta?.oauthProviders?.length ?? 0) > 0;
const hasAnyAuthMethod =
emailPasswordEnabled || passkeyEnabled || hasAnySsoProvider;
emailPasswordEnabled || passkeyEnabled || hasAnySsoProvider || hasAnyOAuthProvider;
useEffect(() => {
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({
provider: providerId as any,
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)
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.
</p>
</div>
{hasAnySsoProvider && (
{(hasAnySsoProvider || hasAnyOAuthProvider) && (
<div className="flex flex-col gap-2">
{meta?.ssoProviders.map((provider) => (
<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"
>
<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>
<span>Continue with {provider.label}</span>
</button>
@@ -174,7 +208,7 @@ export const StepLogin = () => {
: "Your session expired. Sign in to continue where you left off."}
</p>
</div>
{hasAnySsoProvider && !meta?.defaultUserMode && (
{(hasAnySsoProvider || hasAnyOAuthProvider) && !meta?.defaultUserMode && (
<div className="flex flex-col gap-2">
{meta?.ssoProviders.map((provider) => (
<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"
>
<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>
<span>Continue with {provider.label}</span>
</button>
+3 -1
View File
@@ -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 = {
passkeyEnabled: boolean;
hasExistingUsers: boolean;
ssoProviders: OnboardingSsoProvider[];
oauthProviders: OnboardingOAuthProvider[];
defaultUserMode: boolean;
resumeStepId: string;
emailPasswordEnabled: boolean;
+2 -1
View File
@@ -21,7 +21,7 @@ export function ProfileAppearance() {
}
function ThemeSelector() {
const {theme} = useTheme();
const {theme, setTheme} = useTheme();
return (
@@ -39,6 +39,7 @@ function ThemeSelector() {
isActive ? "border-primary bg-primary/5" : "border-muted/40"
)}
onClick={async () => {
setTheme(item.value);
await authClient.updateUser({theme: item.value});
}}
>