-
{organization?.name}
+
)}
diff --git a/apps/dashboard/src/components/auth/share-enter-password.tsx b/apps/dashboard/src/components/auth/share-enter-password.tsx
new file mode 100644
index 00000000..ee6083b4
--- /dev/null
+++ b/apps/dashboard/src/components/auth/share-enter-password.tsx
@@ -0,0 +1,78 @@
+'use client';
+
+import { ModalHeader } from '@/modals/Modal/Container';
+import { api } from '@/trpc/client';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { type ISignInShare, zSignInShare } from '@openpanel/validation';
+import { useRouter } from 'next/navigation';
+import { useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import { LogoSquare } from '../logo';
+import { Button } from '../ui/button';
+import { Input } from '../ui/input';
+
+export function ShareEnterPassword({ shareId }: { shareId: string }) {
+ const router = useRouter();
+ const mutation = api.auth.signInShare.useMutation({
+ onSuccess() {
+ router.refresh();
+ },
+ onError() {
+ toast.error('Incorrect password');
+ },
+ });
+ const form = useForm
({
+ resolver: zodResolver(zSignInShare),
+ defaultValues: {
+ password: '',
+ shareId,
+ },
+ });
+
+ const onSubmit = form.handleSubmit((data) => {
+ mutation.mutate({
+ password: data.password,
+ shareId,
+ });
+ });
+
+ return (
+
+
+
+
+
Overview is locked
+
+ Please enter correct password to access this overview
+
+
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/components/ui/input-with-toggle.tsx b/apps/dashboard/src/components/ui/input-with-toggle.tsx
new file mode 100644
index 00000000..666ca5b2
--- /dev/null
+++ b/apps/dashboard/src/components/ui/input-with-toggle.tsx
@@ -0,0 +1,28 @@
+import type { Dispatch, SetStateAction } from 'react';
+import AnimateHeight from '../animate-height';
+import { Label } from './label';
+import { Switch } from './switch';
+
+type Props = {
+ active: boolean;
+ onActiveChange: (newValue: boolean) => void;
+ label: string;
+ children: React.ReactNode;
+};
+
+export function InputWithToggle({
+ active,
+ onActiveChange,
+ label,
+ children,
+}: Props) {
+ return (
+
+
+
+
+
+
{children}
+
+ );
+}
diff --git a/apps/dashboard/src/modals/ShareOverviewModal.tsx b/apps/dashboard/src/modals/ShareOverviewModal.tsx
index 2efb20a6..656eb5d4 100644
--- a/apps/dashboard/src/modals/ShareOverviewModal.tsx
+++ b/apps/dashboard/src/modals/ShareOverviewModal.tsx
@@ -6,12 +6,13 @@ import { useAppParams } from '@/hooks/useAppParams';
import { api, handleError } from '@/trpc/client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
-import { Controller, useForm } from 'react-hook-form';
+import { Controller, useForm, useWatch } from 'react-hook-form';
import { toast } from 'sonner';
import type { z } from 'zod';
import { zShareOverview } from '@openpanel/validation';
+import { Input } from '@/components/ui/input';
import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';
@@ -23,7 +24,7 @@ export default function ShareOverviewModal() {
const { projectId, organizationId } = useAppParams();
const router = useRouter();
- const { register, handleSubmit, control } = useForm({
+ const { register, handleSubmit } = useForm({
resolver: zodResolver(validator),
defaultValues: {
public: true,
@@ -47,46 +48,27 @@ export default function ShareOverviewModal() {
});
return (
-
-
+
+
diff --git a/packages/trpc/src/routers/auth.ts b/packages/trpc/src/routers/auth.ts
index 009a463c..bb855b70 100644
--- a/packages/trpc/src/routers/auth.ts
+++ b/packages/trpc/src/routers/auth.ts
@@ -1,5 +1,6 @@
import {
Arctic,
+ COOKIE_OPTIONS,
createSession,
deleteSessionTokenCookie,
generateSessionToken,
@@ -11,12 +12,18 @@ import {
verifyPasswordHash,
} from '@openpanel/auth';
import { generateSecureId } from '@openpanel/common/server/id';
-import { connectUserToOrganization, db, getUserAccount } from '@openpanel/db';
+import {
+ connectUserToOrganization,
+ db,
+ getShareOverviewById,
+ getUserAccount,
+} from '@openpanel/db';
import { sendEmail } from '@openpanel/email';
import {
zRequestResetPassword,
zResetPassword,
zSignInEmail,
+ zSignInShare,
zSignUpEmail,
} from '@openpanel/validation';
import * as bcrypt from 'bcrypt';
@@ -272,4 +279,42 @@ export const authRouter = createTRPCRouter({
session: publicProcedure.query(async ({ ctx }) => {
return ctx.session;
}),
+
+ signInShare: publicProcedure
+ .use(
+ rateLimitMiddleware({
+ max: 3,
+ windowMs: 30_000,
+ }),
+ )
+ .input(zSignInShare)
+ .mutation(async ({ input, ctx }) => {
+ const { password, shareId } = input;
+ const share = await getShareOverviewById(input.shareId);
+
+ if (!share) {
+ throw TRPCNotFoundError('Share not found');
+ }
+
+ if (!share.public) {
+ throw TRPCNotFoundError('Share is not public');
+ }
+
+ if (!share.password) {
+ throw TRPCNotFoundError('Share is not password protected');
+ }
+
+ const validPassword = await verifyPasswordHash(share.password, password);
+
+ if (!validPassword) {
+ throw TRPCAccessError('Incorrect password');
+ }
+
+ ctx.setCookie(`shared-overview-${shareId}`, '1', {
+ maxAge: 60 * 60 * 24 * 7,
+ ...COOKIE_OPTIONS,
+ });
+
+ return true;
+ }),
});
diff --git a/packages/trpc/src/routers/share.ts b/packages/trpc/src/routers/share.ts
index c339a256..356c46ef 100644
--- a/packages/trpc/src/routers/share.ts
+++ b/packages/trpc/src/routers/share.ts
@@ -3,6 +3,7 @@ import ShortUniqueId from 'short-unique-id';
import { db } from '@openpanel/db';
import { zShareOverview } from '@openpanel/validation';
+import { hashPassword } from '@openpanel/auth';
import { createTRPCRouter, protectedProcedure } from '../trpc';
const uid = new ShortUniqueId({ length: 6 });
@@ -11,6 +12,10 @@ export const shareRouter = createTRPCRouter({
shareOverview: protectedProcedure
.input(zShareOverview)
.mutation(async ({ input }) => {
+ const passwordHash = input.password
+ ? await hashPassword(input.password)
+ : null;
+
return db.shareOverview.upsert({
where: {
projectId: input.projectId,
@@ -20,11 +25,11 @@ export const shareRouter = createTRPCRouter({
organizationId: input.organizationId,
projectId: input.projectId,
public: input.public,
- password: input.password || null,
+ password: passwordHash,
},
update: {
public: input.public,
- password: input.password,
+ password: passwordHash,
},
});
}),
diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts
index ea0d7e26..7103154b 100644
--- a/packages/validation/src/index.ts
+++ b/packages/validation/src/index.ts
@@ -334,3 +334,9 @@ export const zRequestResetPassword = z.object({
email: z.string().email(),
});
export type IRequestResetPassword = z.infer;
+
+export const zSignInShare = z.object({
+ password: z.string().min(1),
+ shareId: z.string().min(1),
+});
+export type ISignInShare = z.infer;