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() { 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>
); );
-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"; "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,
+24 -10
View File
@@ -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] =
+62 -6
View File
@@ -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>
+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 = { 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;
+2 -1
View File
@@ -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});
}} }}
> >