feature(auth): replace clerk.com with custom auth (#103)

* feature(auth): replace clerk.com with custom auth

* minor fixes

* remove notification preferences

* decrease live events interval

fix(api): cookies..

# Conflicts:
#	.gitignore
#	apps/api/src/index.ts
#	apps/dashboard/src/app/providers.tsx
#	packages/trpc/src/trpc.ts
This commit is contained in:
Carl-Gerhard Lindesvärd
2024-12-18 21:30:39 +01:00
committed by Carl-Gerhard Lindesvärd
parent f28802b1c2
commit d31d9924a5
151 changed files with 18484 additions and 12853 deletions

View File

@@ -24,14 +24,14 @@ import { usePathname, useRouter } from 'next/navigation';
import { useState } from 'react';
import type {
getCurrentOrganizations,
getOrganizations,
getProjectsByOrganizationId,
} from '@openpanel/db';
import Link from 'next/link';
interface LayoutProjectSelectorProps {
projects: Awaited<ReturnType<typeof getProjectsByOrganizationId>>;
organizations?: Awaited<ReturnType<typeof getCurrentOrganizations>>;
organizations?: Awaited<ReturnType<typeof getOrganizations>>;
align?: 'start' | 'end';
}
export default function LayoutProjectSelector({

View File

@@ -1,11 +1,12 @@
import { FullPageEmptyState } from '@/components/full-page-empty-state';
import {
getCurrentOrganizations,
getCurrentProjects,
getDashboardsByProjectId,
getOrganizations,
getProjects,
} from '@openpanel/db';
import { auth } from '@openpanel/auth/nextjs';
import LayoutContent from './layout-content';
import { LayoutSidebar } from './layout-sidebar';
import SideEffects from './side-effects';
@@ -22,9 +23,10 @@ export default async function AppLayout({
children,
params: { organizationSlug: organizationId, projectId },
}: AppLayoutProps) {
const { userId } = await auth();
const [organizations, projects, dashboards] = await Promise.all([
getCurrentOrganizations(),
getCurrentProjects(organizationId),
getOrganizations(userId),
getProjects({ organizationId, userId }),
getDashboardsByProjectId(projectId),
]);

View File

@@ -48,101 +48,137 @@ export default function CreateInvite({ projects }: Props) {
const mutation = api.organization.inviteUser.useMutation({
onSuccess() {
toast('User invited!', {
description: 'The user has been invited to the organization.',
});
toast.success('User has been invited');
reset();
closeSheet();
router.refresh();
},
onError() {
toast.error('Failed to invite user');
onError(error) {
toast.error('Failed to invite user', {
description: error.message,
});
},
});
return (
<Sheet>
<Sheet onOpenChange={() => mutation.reset()}>
<SheetTrigger asChild>
<Button icon={PlusIcon}>Invite user</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<div>
<SheetTitle>Invite a user</SheetTitle>
<SheetDescription>
Invite users to your organization. They will recieve an email will
instructions.
</SheetDescription>
{mutation.isSuccess ? (
<SheetContent>
<SheetHeader>
<SheetTitle>User has been invited</SheetTitle>
</SheetHeader>
<div className="prose">
{mutation.data.type === 'is_member' ? (
<>
<p>
Since the user already has an account we have added him/her to
your organization. This means you will not see this user in
the list of invites.
</p>
<p>We have also notified the user by email about this.</p>
</>
) : (
<p>
We have sent an email with instructions to join the
organization.
</p>
)}
<div className="row gap-4 mt-8">
<Button onClick={() => mutation.reset()}>
Invite another user
</Button>
<Button variant="outline" onClick={() => closeSheet()}>
Close
</Button>
</div>
</div>
</SheetHeader>
<form
onSubmit={handleSubmit((values) => mutation.mutate(values))}
className="flex flex-col gap-8"
>
<InputWithLabel
className="w-full max-w-sm"
label="Email"
error={formState.errors.email?.message}
placeholder="Who do you want to invite?"
{...register('email')}
/>
<div>
<Label>What role?</Label>
</SheetContent>
) : (
<SheetContent>
<SheetHeader>
<div>
<SheetTitle>Invite a user</SheetTitle>
<SheetDescription>
Invite users to your organization. They will recieve an email
will instructions.
</SheetDescription>
</div>
</SheetHeader>
<form
onSubmit={handleSubmit((values) => mutation.mutate(values))}
className="flex flex-col gap-8"
>
<InputWithLabel
className="w-full max-w-sm"
label="Email"
error={formState.errors.email?.message}
placeholder="Who do you want to invite?"
{...register('email')}
/>
<div>
<Label>What role?</Label>
<Controller
name="role"
control={control}
render={({ field }) => (
<RadioGroup
defaultValue={field.value}
onChange={field.onChange}
ref={field.ref}
onBlur={field.onBlur}
className="flex gap-4"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="org:member" id="member" />
<Label className="mb-0" htmlFor="member">
Member
</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="org:admin" id="admin" />
<Label className="mb-0" htmlFor="admin">
Admin
</Label>
</div>
</RadioGroup>
)}
/>
</div>
<Controller
name="role"
name="access"
control={control}
render={({ field }) => (
<RadioGroup
defaultValue={field.value}
onChange={field.onChange}
ref={field.ref}
onBlur={field.onBlur}
className="flex gap-4"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="org:member" id="member" />
<Label className="mb-0" htmlFor="member">
Member
</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="org:admin" id="admin" />
<Label className="mb-0" htmlFor="admin">
Admin
</Label>
</div>
</RadioGroup>
<div>
<Label>Restrict access</Label>
<ComboboxAdvanced
placeholder="Restrict access to projects"
value={field.value}
onChange={field.onChange}
items={projects.map((item) => ({
label: item.name,
value: item.id,
}))}
/>
<p className="mt-1 text-sm text-muted-foreground">
Leave empty to give access to all projects
</p>
</div>
)}
/>
</div>
<Controller
name="access"
control={control}
render={({ field }) => (
<div>
<Label>Restrict access</Label>
<ComboboxAdvanced
placeholder="Restrict access to projects"
value={field.value}
onChange={field.onChange}
items={projects.map((item) => ({
label: item.name,
value: item.id,
}))}
/>
<p className="mt-1 text-sm text-muted-foreground">
Leave empty to give access to all projects
</p>
</div>
)}
/>
<SheetFooter>
<Button icon={SendIcon} type="submit" loading={mutation.isLoading}>
Invite user
</Button>
</SheetFooter>
</form>
</SheetContent>
<SheetFooter>
<Button
icon={SendIcon}
type="submit"
loading={mutation.isLoading}
>
Invite user
</Button>
</SheetFooter>
</form>
</SheetContent>
)}
</Sheet>
);
}

View File

@@ -1,11 +1,11 @@
import { FullPageEmptyState } from '@/components/full-page-empty-state';
import { PageTabs, PageTabsLink } from '@/components/page-tabs';
import { Padding } from '@/components/ui/padding';
import { auth } from '@clerk/nextjs/server';
import { ShieldAlertIcon } from 'lucide-react';
import { notFound } from 'next/navigation';
import { parseAsStringEnum } from 'nuqs/server';
import { auth } from '@openpanel/auth/nextjs';
import { db } from '@openpanel/db';
import EditOrganization from './edit-organization';
@@ -26,7 +26,7 @@ export default async function Page({
const tab = parseAsStringEnum(['org', 'members', 'invites'])
.withDefault('org')
.parseServerSide(searchParams.tab);
const session = auth();
const session = await auth();
const organization = await db.organization.findUnique({
where: {
id: organizationId,

View File

@@ -1,12 +1,11 @@
import { Padding } from '@/components/ui/padding';
import { auth } from '@clerk/nextjs/server';
import { auth } from '@openpanel/auth/nextjs';
import { getUserById } from '@openpanel/db';
import EditProfile from './edit-profile';
export default async function Page() {
const { userId } = auth();
const { userId } = await auth();
const profile = await getUserById(userId!);
return (

View File

@@ -1,39 +1,11 @@
'use client';
import { pushModal, useOnPushModal } from '@/modals';
import { useUser } from '@clerk/nextjs';
import { differenceInDays } from 'date-fns';
import { useEffect } from 'react';
import { useOpenPanel } from '@openpanel/nextjs';
export default function SideEffects() {
const op = useOpenPanel();
const { user } = useUser();
const accountAgeInDays = differenceInDays(
new Date(),
user?.createdAt || new Date(),
);
useOnPushModal('Testimonial', (open) => {
if (!open) {
user?.update({
unsafeMetadata: {
...user.unsafeMetadata,
testimonial: new Date().toISOString(),
},
});
}
});
const showTestimonial =
user && !user.unsafeMetadata.testimonial && accountAgeInDays > 7;
useEffect(() => {
if (showTestimonial) {
pushModal('Testimonial');
op.track('testimonials_shown');
}
}, [showTestimonial]);
return null;
}

View File

@@ -4,7 +4,8 @@ import ProjectCard from '@/components/projects/project-card';
import { redirect } from 'next/navigation';
import SettingsToggle from '@/components/settings-toggle';
import { getCurrentOrganizations, getCurrentProjects } from '@openpanel/db';
import { auth } from '@openpanel/auth/nextjs';
import { getOrganizations, getProjects } from '@openpanel/db';
import LayoutProjectSelector from './[projectId]/layout-project-selector';
interface PageProps {
@@ -16,9 +17,10 @@ interface PageProps {
export default async function Page({
params: { organizationSlug: organizationId },
}: PageProps) {
const { userId } = await auth();
const [organizations, projects] = await Promise.all([
getCurrentOrganizations(),
getCurrentProjects(organizationId),
getOrganizations(userId),
getProjects({ organizationId, userId }),
]);
const organization = organizations.find((org) => org.id === organizationId);
@@ -32,7 +34,7 @@ export default async function Page({
}
if (projects.length === 0) {
return redirect('/onboarding');
return redirect('/onboarding/project');
}
if (projects.length === 1 && projects[0]) {

View File

@@ -1,13 +1,15 @@
import { redirect } from 'next/navigation';
import { getCurrentOrganizations } from '@openpanel/db';
import { auth } from '@openpanel/auth/nextjs';
import { getOrganizations } from '@openpanel/db';
export default async function Page() {
const organizations = await getCurrentOrganizations();
const { userId } = await auth();
const organizations = await getOrganizations(userId);
if (organizations.length > 0) {
return redirect(`/${organizations[0]?.id}`);
}
return redirect('/onboarding');
return redirect('/onboarding/project');
}