Compare commits
168
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5981dca439 | ||
|
|
af73ce21fe | ||
|
|
1b4bb4830b | ||
|
|
0e31093a74 | ||
|
|
4ed98334f0 | ||
|
|
9921a9a896 | ||
|
|
0aaaa1fa7e | ||
|
|
2952083ef0 | ||
|
|
af886c00a3 | ||
|
|
99203b41dd | ||
|
|
af5811b206 | ||
|
|
7ec15f6af9 | ||
|
|
843791d1ee | ||
|
|
c404568baf | ||
|
|
4c1325cca9 | ||
|
|
5f784733cf | ||
|
|
109e9eead2 | ||
|
|
2705155c2e | ||
|
|
31abfcc2da | ||
|
|
617480df3a | ||
|
|
2cf7f9503e | ||
|
|
b827b3a7bd | ||
|
|
d4f4071ee4 | ||
|
|
caf8fa1053 | ||
|
|
7aafdf2b65 | ||
|
|
d568e189a0 | ||
|
|
d21548746c | ||
|
|
d5a58f710c | ||
|
|
77799ddce2 | ||
|
|
dc592aa79d | ||
|
|
f99ecc8e90 | ||
|
|
30cdcf7339 | ||
|
|
c1e7842cca | ||
|
|
978f6c5e88 | ||
|
|
9ccb0508b4 | ||
|
|
54c79e2b6f | ||
|
|
a6b5d4c570 | ||
|
|
19cce6e851 | ||
|
|
c8284f3f27 | ||
|
|
1ad91d413e | ||
|
|
263e4f6c32 | ||
|
|
e7d7e6aeac | ||
|
|
1408d08f38 | ||
|
|
3ccb6035b5 | ||
|
|
eb97698382 | ||
|
|
c3817a2e0a | ||
|
|
ddcfc62f54 | ||
|
|
1244cb74f6 | ||
|
|
3867fa934d | ||
|
|
880f522457 | ||
|
|
2724995f04 | ||
|
|
513f0c4781 | ||
|
|
126976b33f | ||
|
|
2a6073e5c2 | ||
|
|
1e91e2b50a | ||
|
|
8b57b27eb0 | ||
|
|
10473cb76f | ||
|
|
b2d3952309 | ||
|
|
0590fd306e | ||
|
|
1dd9d0eb42 | ||
|
|
b4aefb200e | ||
|
|
6800768d0b | ||
|
|
b8eb58d801 | ||
|
|
cbf081c0b2 | ||
|
|
639c52d9ab | ||
|
|
085f635cc4 | ||
|
|
baba5bbaa9 | ||
|
|
e426826edc | ||
|
|
583b7667be | ||
|
|
ce60abfbce | ||
|
|
effa28bdb5 | ||
|
|
53c72672ca | ||
|
|
2e30946472 | ||
|
|
f31e261811 | ||
|
|
5168a80b30 | ||
|
|
bfc02040ca | ||
|
|
1e65b74b8d | ||
|
|
a0f91e4b93 | ||
|
|
6ead6e6410 | ||
|
|
0df4ab5a74 | ||
|
|
afa7e0fa7d | ||
|
|
df0f8f612f | ||
|
|
fd68e10379 | ||
|
|
19e93b589e | ||
|
|
bd1e3c04f3 | ||
|
|
c78e97962b | ||
|
|
2aadaf8888 | ||
|
|
797d470468 | ||
|
|
120a1b88fb | ||
|
|
86b0ee68b9 | ||
|
|
a44bae5edd | ||
|
|
6950dec056 | ||
|
|
6e9d6fe7d4 | ||
|
|
5e115f1896 | ||
|
|
4b557e909c | ||
|
|
8c3d677af8 | ||
|
|
6687d842db | ||
|
|
4a969ad9ad | ||
|
|
0b1acdf136 | ||
|
|
13a98ca7e5 | ||
|
|
015c44af80 | ||
|
|
1ebfd522a3 | ||
|
|
82b47a3e76 | ||
|
|
0f59913c14 | ||
|
|
d5c7a6d109 | ||
|
|
c394f623eb | ||
|
|
adde340bbd | ||
|
|
32275c6068 | ||
|
|
ef2f06f824 | ||
|
|
8d5568b807 | ||
|
|
eaabf16b2c | ||
|
|
3c7e4f93d0 | ||
|
|
3f95f1c118 | ||
|
|
996a40ceb9 | ||
|
|
6c1f5e261d | ||
|
|
dc0de8a8dd | ||
|
|
898313f5a5 | ||
|
|
a279392ba9 | ||
|
|
6951b2d92a | ||
|
|
76444e2992 | ||
|
|
47c256a53c | ||
|
|
a0570a18ca | ||
|
|
58d31905ef | ||
|
|
b8c6bb093f | ||
|
|
24b22dc1df | ||
|
|
c9b249c969 | ||
|
|
f677204bc2 | ||
|
|
71b243ad9b | ||
|
|
7216556de2 | ||
|
|
223d81adbf | ||
|
|
e19ae608d9 | ||
|
|
be9c7dedae | ||
|
|
9bdbe333ad | ||
|
|
f1a686e39d | ||
|
|
d3bd1802cd | ||
|
|
3ae1dbb678 | ||
|
|
58476317a5 | ||
|
|
c670f29273 | ||
|
|
996d5f0e21 | ||
|
|
0dd914a87d | ||
|
|
febb9322a7 | ||
|
|
661014c893 | ||
|
|
53d328c2d9 | ||
|
|
5f81333e6e | ||
|
|
47922d48d7 | ||
|
|
3794b3eeff | ||
|
|
8feb0e2d6e | ||
|
|
4e0969ab71 | ||
|
|
3c20959060 | ||
|
|
4145145b74 | ||
|
|
9d7b8bf8f2 | ||
|
|
2425a8933b | ||
|
|
d1a3603837 | ||
|
|
70beab2c5e | ||
|
|
c7c1f59649 | ||
|
|
7fd0d7bdfe | ||
|
|
cf1e783d8f | ||
|
|
5df098f630 | ||
|
|
6d7d88431b | ||
|
|
ce2b40daad | ||
|
|
2709049f68 | ||
|
|
e81d4118aa | ||
|
|
5d041f20f8 | ||
|
|
8a874e10e0 | ||
|
|
7c50bf6242 | ||
|
|
fb51399469 | ||
|
|
053394901c | ||
|
|
5e7ff9fa29 |
@@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/.next
|
||||
.git
|
||||
.env*
|
||||
.vscode/
|
||||
.idea/
|
||||
coverage/
|
||||
*.test.ts
|
||||
*.spec.ts
|
||||
.DS_Store
|
||||
*.md
|
||||
docs/
|
||||
+28
-5
@@ -12,9 +12,15 @@ POSTGRES_PRISMA_URL_NON_POOLING=
|
||||
# This variable is from Vercel Storage Blob
|
||||
BLOB_READ_WRITE_TOKEN=
|
||||
|
||||
# Google client id and secret for authentication
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
# Hanzo IAM OAuth (required for production)
|
||||
IAM_URL="https://hanzo.id"
|
||||
IAM_CLIENT_ID=""
|
||||
IAM_CLIENT_SECRET=""
|
||||
# IAM_PROVIDER_NAME="Hanzo"
|
||||
|
||||
# Google client id and secret (deprecated — use Hanzo IAM above)
|
||||
# GOOGLE_CLIENT_ID=
|
||||
# GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# This variable is from Resend to send emails
|
||||
RESEND_API_KEY=
|
||||
@@ -67,8 +73,25 @@ NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS=
|
||||
# Encryption key for document passwords.
|
||||
NEXT_PRIVATE_DOCUMENT_PASSWORD_KEY=my-superstrong-document-secret
|
||||
|
||||
# [[REDIS LOCKER CONFIGURATION]]
|
||||
# For bulk upload using tus.io, we use a Redis-based locker to prevent corruption of the data.
|
||||
# [[HANZO KV]] — OPTIONAL. Leave UNSET to run on the in-process backend
|
||||
# (single-replica correct: cache, rate-limit, tus upload locks, export/download
|
||||
# job stores and digest queues all work with no external datastore). SET it to
|
||||
# an external Hanzo KV instance for multi-replica HA (shared state across pods).
|
||||
# Accepts the Hanzo KV brand scheme kv:// (kvs:// for TLS) or a redis:// DSN.
|
||||
# A malformed value fails CLOSED (the app throws rather than silently degrade).
|
||||
#
|
||||
# Re-enabling multi-replica (replicas > 1) REQUIRES KV_URL. dataroom is
|
||||
# single-replica-by-construction otherwise: SQLite on a ReadWriteOnce volume plus
|
||||
# the in-process KV. Without KV_URL each pod owns its OWN map, so sessions,
|
||||
# rate-limit windows and tus upload locks split-brain across replicas — scaling
|
||||
# out needs a shared DB AND KV_URL set.
|
||||
# KV_URL=kv://:password@hanzo-kv:6379
|
||||
KV_URL=
|
||||
|
||||
# [[TUS UPLOAD LOCKER]] — for bulk upload via tus.io, an exclusive locker
|
||||
# prevents data corruption. It uses the Hanzo KV client above (KV_URL); with
|
||||
# KV_URL unset the lock is in-process (correct for a single replica). The legacy
|
||||
# Upstash REST locker below is unused and kept only for reference.
|
||||
UPSTASH_REDIS_REST_LOCKER_URL=
|
||||
UPSTASH_REDIS_REST_LOCKER_TOKEN=
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-label="dataroom">
|
||||
<rect width="1280" height="640" fill="#0A0A0A"/>
|
||||
<svg x="96" y="215" width="210" height="210" viewBox="0 0 67 67"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#fff"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#fff"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#fff"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#fff"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#fff"/></svg>
|
||||
<text x="378" y="276" font-family="Inter,system-ui,-apple-system,sans-serif" font-size="78" font-weight="800" letter-spacing="-2" fill="#ffffff">dataroom</text>
|
||||
<text x="378" y="322" font-family="Inter,system-ui,sans-serif" font-size="30" fill="#ffffff" opacity=".66">Papermark is the open-source DocSend alternative with built-in…</text>
|
||||
<rect x="378" y="338" width="806" height="3" rx="1.5" fill="#ffffff" opacity=".9"/>
|
||||
<text x="378" y="390" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">github.com/hanzoai</text>
|
||||
<text x="1184" y="390" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">hanzo.ai</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -13,7 +13,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
CLAAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
@@ -25,10 +25,10 @@ jobs:
|
||||
# This token is required only if you have configured to store the signatures in a remote repository/organization
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
with:
|
||||
remote-organization-name: 'papermark'
|
||||
remote-organization-name: 'hanzo-dataroom'
|
||||
remote-repository-name: 'cla-signatures'
|
||||
path-to-signatures: 'signatures/version1/cla.json'
|
||||
path-to-document: 'https://github.com/mfts/papermark/blob/main/CLA.md'
|
||||
path-to-document: 'https://github.com/hanzoai/dataroom/blob/main/CLA.md'
|
||||
# branch should not be protected
|
||||
branch: 'main'
|
||||
allowlist: cursoragent
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
name: Docker
|
||||
# Native deploy pipeline is .hanzo/workflows/deploy.yml (Hanzo Git → act_runner →
|
||||
# BuildKit → ghcr.io/hanzoai/dataroom:<sha> → operator reconcile → hanzocd).
|
||||
# GitHub is a mirror; this workflow is retained only as a manual sync notice.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
notice:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "native pipeline is .hanzo/workflows/deploy.yml; GitHub is a mirror"
|
||||
@@ -0,0 +1,7 @@
|
||||
name: Workflow Sanity
|
||||
on:
|
||||
pull_request:
|
||||
paths: ['.github/workflows/**']
|
||||
jobs:
|
||||
sanity:
|
||||
uses: hanzoai/.github/.github/workflows/workflow-sanity.yml@main
|
||||
+1
-1
@@ -62,4 +62,4 @@ lib/emails/marketing
|
||||
|
||||
# changelog and docs
|
||||
changelog
|
||||
.docs
|
||||
.docsvendor/
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: deploy
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: hanzo-linux-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build + push image
|
||||
run: |
|
||||
SHA="${GITHUB_SHA::8}"
|
||||
buildctl-daemonless.sh build --frontend=dockerfile.v0 \
|
||||
--opt context="${{ github.server_url }}/${{ github.repository }}.git#${GITHUB_SHA}" \
|
||||
--opt filename=Dockerfile --opt platform=linux/amd64 \
|
||||
--secret id=GIT_AUTH_TOKEN,env=GIT_AUTH_TOKEN \
|
||||
--output "type=image,name=ghcr.io/hanzoai/dataroom:${SHA},push=true" --progress=plain
|
||||
env:
|
||||
GIT_AUTH_TOKEN: ${{ secrets.GIT_CLONE_TOKEN }}
|
||||
- name: Deploy — declare tag to operator
|
||||
run: |
|
||||
for app in dataroom; do
|
||||
kubectl -n hanzo patch app "$app" --type=merge -p "{\"spec\":{\"image\":{\"repository\":\"ghcr.io/hanzoai/dataroom\",\"tag\":\"${GITHUB_SHA::8}\"}}}"
|
||||
done
|
||||
@@ -1,16 +1,16 @@
|
||||
# Papermark Contributors License Agreement
|
||||
# Hanzo Dataroom Contributors License Agreement
|
||||
|
||||
This Contributors License Agreement ("CLA") is entered into between the Contributor, and Papermark, Inc. ("Papermark"), collectively referred to as the "Parties."
|
||||
This Contributors License Agreement ("CLA") is entered into between the Contributor, and Hanzo AI, Inc. ("Hanzo Dataroom"), collectively referred to as the "Parties."
|
||||
|
||||
## Background:
|
||||
|
||||
Papermark is an open-source project aimed at providing an open-source document sharing and tracking infrastructure for all parties. This CLA governs the rights and contributions made by the Contributor to the Papermark project.
|
||||
Hanzo Dataroom is an open-source project aimed at providing an open-source document sharing and tracking infrastructure for all parties. This CLA governs the rights and contributions made by the Contributor to the Hanzo Dataroom project.
|
||||
|
||||
## Agreement:
|
||||
|
||||
**Contributor Grant of License:**
|
||||
|
||||
By submitting code, documentation, or any other materials (collectively, "Contributions") to the Papermark project, the Contributor grants Papermark a perpetual, worldwide, non-exclusive, royalty-free, sublicensable license to use, modify, distribute, and otherwise exploit the Contributions, including any intellectual property rights therein, for the purposes of the Papermark project.
|
||||
By submitting code, documentation, or any other materials (collectively, "Contributions") to the Hanzo Dataroom project, the Contributor grants Hanzo Dataroom a perpetual, worldwide, non-exclusive, royalty-free, sublicensable license to use, modify, distribute, and otherwise exploit the Contributions, including any intellectual property rights therein, for the purposes of the Hanzo Dataroom project.
|
||||
|
||||
**Representation of Ownership and Right to Contribute:**
|
||||
|
||||
@@ -18,11 +18,11 @@ The Contributor represents that they have the legal right to grant the license s
|
||||
|
||||
**Patent Grant:**
|
||||
|
||||
If the Contributions include any method, process, or apparatus that is covered by a patent, the Contributor agrees to grant Papermark a non-exclusive, worldwide, royalty-free license under any patent claims necessary to use, modify, distribute, and otherwise exploit the Contributions for the purposes of the Papermark project.
|
||||
If the Contributions include any method, process, or apparatus that is covered by a patent, the Contributor agrees to grant Hanzo Dataroom a non-exclusive, worldwide, royalty-free license under any patent claims necessary to use, modify, distribute, and otherwise exploit the Contributions for the purposes of the Hanzo Dataroom project.
|
||||
|
||||
**No Implied Warranties or Support:**
|
||||
|
||||
The Contributor acknowledges that the Contributions are provided "as is," without any warranties or support of any kind. Papermark shall have no obligation to provide maintenance, updates, bug fixes, or support for the Contributions.
|
||||
The Contributor acknowledges that the Contributions are provided "as is," without any warranties or support of any kind. Hanzo Dataroom shall have no obligation to provide maintenance, updates, bug fixes, or support for the Contributions.
|
||||
|
||||
**Retention of Contributor Rights:**
|
||||
|
||||
@@ -38,8 +38,8 @@ This CLA constitutes the entire agreement between the Parties with respect to th
|
||||
|
||||
**Acceptance:**
|
||||
|
||||
By submitting Contributions to the Papermark project, the Contributor acknowledges and agrees to the terms and conditions of this CLA. If the Contributor is agreeing to this CLA on behalf of an entity, they represent that they have the necessary authority to bind that entity to these terms.
|
||||
By submitting Contributions to the Hanzo Dataroom project, the Contributor acknowledges and agrees to the terms and conditions of this CLA. If the Contributor is agreeing to this CLA on behalf of an entity, they represent that they have the necessary authority to bind that entity to these terms.
|
||||
|
||||
**Effective Date:**
|
||||
|
||||
This CLA is effective as of the date of the first Contribution made by the Contributor to the Papermark project.
|
||||
This CLA is effective as of the date of the first Contribution made by the Contributor to the Hanzo Dataroom project.
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
FROM ghcr.io/hanzoai/nodejs:v24.18.0 AS base
|
||||
RUN apk add --no-cache libc6-compat openssl python3 make g++
|
||||
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
COPY prisma ./prisma
|
||||
ENV HUSKY=0
|
||||
RUN npm install --legacy-peer-deps
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV DOCKER_OUTPUT=1
|
||||
# Dummy env vars to prevent module-scope crashes during Next.js build
|
||||
# (OpenAI, Hanko, etc. initialize clients at import time)
|
||||
ENV OPENAI_API_KEY=build-placeholder
|
||||
ENV HANKO_API_KEY=build-placeholder
|
||||
ENV NEXT_PUBLIC_HANKO_TENANT_ID=build-placeholder
|
||||
RUN npx prisma generate
|
||||
# SQLite has no Prisma enums; re-inject the enum objects the app imports from
|
||||
# @prisma/client (e.g. LinkType.DOCUMENT_LINK) so runtime/prerender resolves them.
|
||||
RUN node prisma/inject-sqlite-enums.cjs
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
RUN npm run build
|
||||
|
||||
FROM ghcr.io/hanzoai/nodejs:v24.18.0 AS runner
|
||||
RUN apk add --no-cache openssl
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
COPY --from=builder /app/public ./public
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
||||
RUN ln -s /app/prisma/migrations /app/prisma/schema/migrations
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/prisma ./node_modules/prisma
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.bin ./node_modules/.bin
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000
|
||||
# SQLite: reconcile schema on startup via `db push` (Prisma has no migrate-deploy
|
||||
# path for the sqlite provider here; the pg migration history under prisma/migrations
|
||||
# is postgres-specific and unused). Schema folder = prismaSchemaFolder preview.
|
||||
CMD ["sh", "-c", "node_modules/.bin/prisma db push --schema prisma/schema --skip-generate --accept-data-loss && node server.js"]
|
||||
@@ -0,0 +1,49 @@
|
||||
# Hanzo Dataroom
|
||||
|
||||
## Overview
|
||||
Hanzo dataroom service.
|
||||
|
||||
**Upstream**: [Papermark](https://github.com/mfts/papermark) (AGPL-3.0). LICENSE retains "Copyright (c) 2023-present Papermark, Inc." Open-source DocSend alternative. This fork is Hanzo Dataroom — single-license AGPL, no `ee/` commercial directory.
|
||||
|
||||
## In-process fold (HIP-0106, task #101)
|
||||
The production runtime is the **goja bundle** in [`goja/`](goja/): the ESM-free
|
||||
port of the API handlers, run in-process inside the unified `hanzoai/cloud` binary
|
||||
over Hanzo Base/SQLite (per-tenant) + the cloud object-storage seam. The standalone
|
||||
Next.js app + Postgres pod is **retired**; cloud serves `/v1/dataroom/*` itself.
|
||||
See [`goja/README.md`](goja/README.md) for the host contract. The Next.js/TS
|
||||
sources below remain the reference for the domain model that `goja/bundle.js`
|
||||
implements.
|
||||
|
||||
## Tech Stack
|
||||
- **Language**: TypeScript/JavaScript
|
||||
|
||||
## Build & Run
|
||||
```bash
|
||||
npm install && npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
## Structure
|
||||
```
|
||||
dataroom/
|
||||
CLA.md
|
||||
Dockerfile
|
||||
LICENSE
|
||||
LLM.md
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
README.md
|
||||
SECURITY.md
|
||||
app/
|
||||
components/
|
||||
components.json
|
||||
context/
|
||||
features/
|
||||
lib/
|
||||
middleware.ts
|
||||
```
|
||||
|
||||
## Key Files
|
||||
- `README.md` -- Project documentation
|
||||
- `package.json` -- Dependencies and scripts
|
||||
- `Dockerfile` -- Container build
|
||||
@@ -1,26 +1,28 @@
|
||||
<p align="center"><img src=".github/hero.svg" alt="dataroom" width="880"></p>
|
||||
|
||||
<div align="center">
|
||||
<h1 align="center">Papermark</h1>
|
||||
<h1 align="center">Hanzo Dataroom</h1>
|
||||
<h3>The open-source DocSend alternative.</h3>
|
||||
|
||||
<a target="_blank" href="https://www.producthunt.com/posts/papermark-3?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-papermark"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=411605&theme=light&period=daily" alt="Papermark - The open-source DocSend alternative | Product Hunt" style="width:250px;height:40px"></a>
|
||||
<a target="_blank" href="https://www.producthunt.com/posts/hanzo-dataroom-3?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-hanzo-dataroom"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=411605&theme=light&period=daily" alt="Hanzo Dataroom - The open-source DocSend alternative | Product Hunt" style="width:250px;height:40px"></a>
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.papermark.com">papermark.com</a>
|
||||
<a href="https://www.dataroom.hanzo.ai">dataroom.hanzo.ai</a>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/mfts/papermark/stargazers"><img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/mfts/papermark"></a>
|
||||
<a href="https://twitter.com/papermarkio"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/papermarkio"></a>
|
||||
<a href="https://github.com/mfts/papermark/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/license-AGPLv3-purple"></a>
|
||||
<a href="https://github.com/hanzoai/dataroom/stargazers"><img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/hanzoai/dataroom"></a>
|
||||
<a href="https://twitter.com/hanzoai"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/hanzoai"></a>
|
||||
<a href="https://github.com/hanzoai/dataroom/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/license-AGPLv3-purple"></a>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
Papermark is the open-source document-sharing alternative to DocSend, featuring built-in analytics and custom domains.
|
||||
Hanzo Dataroom is the open-source document-sharing alternative to DocSend, featuring built-in analytics and custom domains.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -31,7 +33,7 @@ Papermark is the open-source document-sharing alternative to DocSend, featuring
|
||||
|
||||
## Demo
|
||||
|
||||

|
||||

|
||||
|
||||
## Tech Stack
|
||||
|
||||
@@ -51,7 +53,7 @@ Papermark is the open-source document-sharing alternative to DocSend, featuring
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Here's what you need to run Papermark:
|
||||
Here's what you need to run Hanzo Dataroom:
|
||||
|
||||
- Node.js (version >= 18.17.0)
|
||||
- PostgreSQL Database
|
||||
@@ -61,8 +63,8 @@ Here's what you need to run Papermark:
|
||||
### 1. Clone the repository
|
||||
|
||||
```shell
|
||||
git clone https://github.com/mfts/papermark.git
|
||||
cd papermark
|
||||
git clone https://github.com/hanzoai/dataroom.git
|
||||
cd hanzo-dataroom
|
||||
```
|
||||
|
||||
### 2. Install npm dependencies
|
||||
@@ -119,19 +121,19 @@ To prepare the Tinybird database, follow these steps:
|
||||
pipenv shell
|
||||
## start: pkgx-specific
|
||||
cd ..
|
||||
cd papermark
|
||||
cd hanzo-dataroom
|
||||
## end: pkgx-specific
|
||||
pipenv update tinybird-cli
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Papermark is an open-source project, and we welcome contributions from the community.
|
||||
Hanzo Dataroom is an open-source project, and we welcome contributions from the community.
|
||||
|
||||
If you'd like to contribute, please fork the repository and make any changes you'd like. Pull requests are warmly welcome.
|
||||
|
||||
### Our Contributors ✨
|
||||
|
||||
<a href="https://github.com/mfts/papermark/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=mfts/papermark" />
|
||||
<a href="https://github.com/hanzoai/dataroom/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hanzoai/dataroom" />
|
||||
</a>
|
||||
|
||||
+2
-2
@@ -2,10 +2,10 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
The latest version of Papermark is currently being supported with security updates.
|
||||
The latest version of Hanzo Dataroom is currently being supported with security updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, send an email to security@papermark.com.
|
||||
To report a vulnerability, send an email to security@dataroom.hanzo.ai.
|
||||
|
||||
We will respond within 48 hours acknowledging your report with details about next steps and potential rewards/compensation for responsible disclosure.
|
||||
|
||||
@@ -20,19 +20,19 @@ export const runtime = "nodejs";
|
||||
|
||||
const data = {
|
||||
description: "Confirm email change",
|
||||
title: "Confirm email change | Papermark",
|
||||
title: "Confirm email change | Hanzo Dataroom",
|
||||
url: "/auth/confirm-email-change",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://www.papermark.com"),
|
||||
metadataBase: new URL("https://dataroom.hanzo.ai"),
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
openGraph: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
siteName: "Papermark",
|
||||
siteName: "Hanzo Dataroom",
|
||||
images: [
|
||||
{
|
||||
url: "/_static/meta-image.png",
|
||||
@@ -47,13 +47,13 @@ export const metadata: Metadata = {
|
||||
card: "summary_large_image",
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
creator: "@papermarkio",
|
||||
creator: "@hanzoai",
|
||||
images: ["/_static/meta-image.png"],
|
||||
},
|
||||
};
|
||||
|
||||
interface PageProps {
|
||||
params: { token: string };
|
||||
params: Promise<{ token: string }>;
|
||||
}
|
||||
|
||||
export default async function ConfirmEmailChangePage(props: PageProps) {
|
||||
@@ -64,7 +64,8 @@ export default async function ConfirmEmailChangePage(props: PageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const VerifyEmailChange = async ({ params: { token } }: PageProps) => {
|
||||
const VerifyEmailChange = async ({ params }: PageProps) => {
|
||||
const { token } = await params;
|
||||
const tokenFound = await prisma.verificationToken.findUnique({
|
||||
where: {
|
||||
token: hashToken(token),
|
||||
|
||||
@@ -115,10 +115,10 @@ export default function EmailVerificationClient() {
|
||||
<div className="flex w-full justify-center bg-gray-50 md:w-1/2 lg:w-1/2">
|
||||
<div className="z-10 mx-5 mt-[calc(1vh)] h-fit w-full max-w-md overflow-hidden rounded-lg sm:mx-0 sm:mt-[calc(2vh)] md:mt-[calc(3vh)]">
|
||||
<div className="items-left flex flex-col space-y-3 px-4 py-6 pt-8 sm:px-12">
|
||||
<Link href="https://www.papermark.com" target="_blank">
|
||||
<Link href="https://dataroom.hanzo.ai" target="_blank">
|
||||
<img
|
||||
src="/_static/papermark-logo.svg"
|
||||
alt="Papermark Logo"
|
||||
src="/_static/hanzo-dataroom-logo.svg"
|
||||
alt="Hanzo Dataroom Logo"
|
||||
className="-mt-8 mb-36 h-7 w-auto self-start sm:mb-32 md:mb-48"
|
||||
/>
|
||||
</Link>
|
||||
@@ -153,10 +153,10 @@ export default function EmailVerificationClient() {
|
||||
></div>
|
||||
<div className="z-10 mx-5 mt-[calc(1vh)] h-fit w-full max-w-md overflow-hidden rounded-lg sm:mx-0 sm:mt-[calc(2vh)] md:mt-[calc(3vh)]">
|
||||
<div className="items-left flex flex-col space-y-3 px-4 py-6 pt-8 sm:px-12">
|
||||
<Link href="https://www.papermark.com" target="_blank">
|
||||
<Link href="https://dataroom.hanzo.ai" target="_blank">
|
||||
<img
|
||||
src="/_static/papermark-logo.svg"
|
||||
alt="Papermark Logo"
|
||||
src="/_static/hanzo-dataroom-logo.svg"
|
||||
alt="Hanzo Dataroom Logo"
|
||||
className="-mt-8 mb-36 h-7 w-auto self-start sm:mb-32 md:mb-48"
|
||||
/>
|
||||
</Link>
|
||||
@@ -187,10 +187,10 @@ export default function EmailVerificationClient() {
|
||||
<p className="mt-2 text-sm text-orange-800">
|
||||
Check your junk/spam and quarantine folders and ensure that{" "}
|
||||
<a
|
||||
href="mailto:system@papermark.com"
|
||||
href="mailto:dataroom@hanzo.ai"
|
||||
className="font-medium text-orange-600 underline hover:text-orange-700"
|
||||
>
|
||||
system@papermark.com
|
||||
dataroom@hanzo.ai
|
||||
</a>{" "}
|
||||
is on your allowed senders list.
|
||||
</p>
|
||||
@@ -262,7 +262,7 @@ export default function EmailVerificationClient() {
|
||||
|
||||
<p className="mt-10 w-full max-w-md px-4 text-xs text-muted-foreground sm:px-12">
|
||||
By clicking continue, you acknowledge that you have read and agree
|
||||
to Papermark's{" "}
|
||||
to Hanzo Dataroom's{" "}
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_MARKETING_URL}/terms`}
|
||||
target="_blank"
|
||||
@@ -312,7 +312,7 @@ function TestimonialSection() {
|
||||
<div className="max-w-xl text-center">
|
||||
<blockquote className="text-balance font-normal leading-8 text-white sm:text-xl sm:leading-9">
|
||||
<p>
|
||||
"We raised our €30M Fund with Papermark Data Rooms. Love
|
||||
"We raised our €30M Fund with Hanzo Dataroom. Love
|
||||
the customization, security and ease of use."
|
||||
</p>
|
||||
</blockquote>
|
||||
|
||||
@@ -3,20 +3,20 @@ import { Metadata } from "next";
|
||||
import EmailVerificationClient from "./page-client";
|
||||
|
||||
const data = {
|
||||
description: "Verify your login to Papermark",
|
||||
title: "Verify Login | Papermark",
|
||||
description: "Verify your login to Hanzo Dataroom",
|
||||
title: "Verify Login | Hanzo Dataroom",
|
||||
url: "/auth/email",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://www.papermark.com"),
|
||||
metadataBase: new URL("https://dataroom.hanzo.ai"),
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
openGraph: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
siteName: "Papermark",
|
||||
siteName: "Hanzo Dataroom",
|
||||
images: [
|
||||
{
|
||||
url: "/_static/meta-image.png",
|
||||
@@ -31,7 +31,7 @@ export const metadata: Metadata = {
|
||||
card: "summary_large_image",
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
creator: "@papermarkio",
|
||||
creator: "@hanzoai",
|
||||
images: ["/_static/meta-image.png"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Metadata } from "next";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import SAMLCallbackClient from "./page-client";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "SSO Login | Papermark",
|
||||
title: "SSO Login | Hanzo Dataroom",
|
||||
description: "Completing SSO login",
|
||||
};
|
||||
|
||||
export default function SAMLCallbackPage() {
|
||||
return <SAMLCallbackClient />;
|
||||
return (
|
||||
<Suspense>
|
||||
<SAMLCallbackClient />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,318 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
import { SSOLogin } from "@/ee/features/security/sso";
|
||||
import { signInWithPasskey } from "@teamhanko/passkeys-next-auth-provider/client";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { LastUsed, useLastUsed } from "@/components/hooks/useLastUsed";
|
||||
import Google from "@/components/shared/icons/google";
|
||||
import LinkedIn from "@/components/shared/icons/linkedin";
|
||||
import Passkey from "@/components/shared/icons/passkey";
|
||||
import { LogoCloud } from "@/components/shared/logo-cloud";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { HanzoMark } from "@/components/shared/icons/hanzo-mark";
|
||||
|
||||
// White-label hooks: every visible brand token comes from env so tenants
|
||||
// drop in their own name/words/IAM provider without touching this file.
|
||||
//
|
||||
// NEXT_PUBLIC_APP_NAME e.g. "Hanzo Dataroom" | "Acme Rooms"
|
||||
// NEXT_PUBLIC_APP_NAME_PRIMARY first word of wordmark; defaults to first
|
||||
// word of NEXT_PUBLIC_APP_NAME ("Hanzo")
|
||||
// NEXT_PUBLIC_APP_NAME_SUFFIX second token of wordmark; defaults to
|
||||
// remainder of NEXT_PUBLIC_APP_NAME ("Dataroom")
|
||||
// NEXT_PUBLIC_APP_TAGLINE one-line under the welcome headline
|
||||
// NEXT_PUBLIC_IAM_PROVIDER_NAME IAM brand for the "Sign in with X" button
|
||||
// NEXT_PUBLIC_MARKETING_URL base URL the Terms / Privacy links resolve against
|
||||
|
||||
const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME || "Hanzo Dataroom";
|
||||
const [defaultPrimary, ...defaultSuffixParts] = APP_NAME.split(" ");
|
||||
const APP_NAME_PRIMARY =
|
||||
process.env.NEXT_PUBLIC_APP_NAME_PRIMARY || defaultPrimary || APP_NAME;
|
||||
const APP_NAME_SUFFIX =
|
||||
process.env.NEXT_PUBLIC_APP_NAME_SUFFIX || defaultSuffixParts.join(" ");
|
||||
const APP_TAGLINE =
|
||||
process.env.NEXT_PUBLIC_APP_TAGLINE || "Share documents. Not attachments.";
|
||||
const IAM_PROVIDER_NAME =
|
||||
process.env.NEXT_PUBLIC_IAM_PROVIDER_NAME || "Hanzo";
|
||||
const MARKETING_URL = process.env.NEXT_PUBLIC_MARKETING_URL || "";
|
||||
|
||||
export default function Login() {
|
||||
const { next } = useParams as { next?: string };
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const authError = searchParams?.get("error");
|
||||
const isSSORequired = authError === "require-saml-sso";
|
||||
|
||||
const [lastUsed, setLastUsed] = useLastUsed();
|
||||
const authMethods = ["google", "email", "linkedin", "passkey"] as const;
|
||||
type AuthMethod = (typeof authMethods)[number];
|
||||
const [clickedMethod, setClickedMethod] = useState<AuthMethod | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [emailButtonText, setEmailButtonText] = useState<string>(
|
||||
"Continue with Email",
|
||||
);
|
||||
|
||||
const emailSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.min(3, { message: "Please enter a valid email." })
|
||||
.email({ message: "Please enter a valid email." });
|
||||
|
||||
const emailValidation = emailSchema.safeParse(email);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-wrap">
|
||||
{/* Left part */}
|
||||
<div className="flex w-full justify-center bg-gray-50 md:w-1/2 lg:w-1/2">
|
||||
<div
|
||||
className="absolute inset-x-0 top-10 -z-10 flex transform-gpu justify-center overflow-hidden blur-3xl"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<div className="z-10 mx-5 mt-[calc(1vh)] h-fit w-full max-w-md overflow-hidden rounded-lg sm:mx-0 sm:mt-[calc(2vh)] md:mt-[calc(3vh)]">
|
||||
<div className="items-left flex flex-col space-y-3 px-4 py-6 pt-8 sm:px-12">
|
||||
<Link href="https://www.papermark.com" target="_blank">
|
||||
<img
|
||||
src="/_static/papermark-logo.svg"
|
||||
alt="Papermark Logo"
|
||||
className="md:mb-48s -mt-8 mb-36 h-7 w-auto self-start sm:mb-32"
|
||||
/>
|
||||
</Link>
|
||||
<Link href="/">
|
||||
<span className="text-balance text-3xl font-semibold text-gray-900">
|
||||
Welcome to Papermark
|
||||
</span>
|
||||
</Link>
|
||||
<h3 className="text-balance text-sm text-gray-800">
|
||||
Share documents. Not attachments.
|
||||
</h3>
|
||||
</div>
|
||||
{isSSORequired && (
|
||||
<div className="mx-4 mb-2 flex items-start gap-3 rounded-lg border border-orange-200 bg-orange-50 px-4 py-3 sm:mx-12">
|
||||
<AlertCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-orange-600" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-orange-900">
|
||||
Your organization requires SSO login
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-orange-700">
|
||||
Please use the <strong>SAML SSO</strong> option below to sign
|
||||
in with your company's identity provider.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
className="flex flex-col gap-4 px-4 pt-8 sm:px-12"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (!emailValidation.success) {
|
||||
toast.error(emailValidation.error.errors[0].message);
|
||||
return;
|
||||
}
|
||||
<main className="flex min-h-screen w-full items-center justify-center bg-black px-6 text-white">
|
||||
<div className="w-full max-w-sm">
|
||||
<Link href="/" className="mb-12 flex items-center gap-3">
|
||||
<HanzoMark size={28} className="text-white" />
|
||||
<span className="text-base font-medium tracking-tight">
|
||||
{APP_NAME_PRIMARY}
|
||||
{APP_NAME_SUFFIX && (
|
||||
<span className="text-zinc-400"> {APP_NAME_SUFFIX}</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
setClickedMethod("email");
|
||||
signIn("email", {
|
||||
email: emailValidation.data,
|
||||
redirect: false,
|
||||
...(next && next.length > 0 ? { callbackUrl: next } : {}),
|
||||
}).then((res) => {
|
||||
if (res?.ok && !res?.error) {
|
||||
setLastUsed("credentials");
|
||||
// Store email in sessionStorage for the verification page
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
"pendingVerificationEmail",
|
||||
emailValidation.data,
|
||||
);
|
||||
} catch {
|
||||
// sessionStorage not available, verification page will show email input
|
||||
}
|
||||
router.push("/auth/email");
|
||||
} else {
|
||||
setEmailButtonText("Error sending email - try again?");
|
||||
toast.error("Error sending email - try again?");
|
||||
setClickedMethod(undefined);
|
||||
}
|
||||
});
|
||||
}}
|
||||
<h1 className="text-balance text-3xl font-semibold text-white">
|
||||
Welcome to {APP_NAME}
|
||||
</h1>
|
||||
<p className="mt-2 text-balance text-sm text-zinc-400">{APP_TAGLINE}</p>
|
||||
|
||||
<Button
|
||||
onClick={() =>
|
||||
signIn("hanzo-iam", {
|
||||
...(next && next.length > 0 ? { callbackUrl: next } : {}),
|
||||
})
|
||||
}
|
||||
className="mt-8 flex w-full items-center justify-center bg-white font-normal text-black hover:bg-zinc-200"
|
||||
>
|
||||
<span>
|
||||
Sign in with <span className="font-bold">{IAM_PROVIDER_NAME}</span>
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<p className="mt-8 text-xs text-zinc-500">
|
||||
By clicking continue, you acknowledge that you have read and agree to{" "}
|
||||
{APP_NAME}'s{" "}
|
||||
<a
|
||||
href={`${MARKETING_URL}/terms`}
|
||||
target="_blank"
|
||||
className="text-zinc-300 underline hover:text-white"
|
||||
>
|
||||
<Label className="sr-only" htmlFor="email">
|
||||
Email
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
autoCorrect="off"
|
||||
disabled={clickedMethod === "email"}
|
||||
// pattern={patternSimpleEmailRegex}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border-0 bg-background bg-white px-3 py-2 text-sm text-gray-900 ring-1 ring-gray-200 transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white",
|
||||
email.length > 0 && !emailValidation.success
|
||||
? "ring-red-500"
|
||||
: "ring-gray-200",
|
||||
)}
|
||||
/>
|
||||
<div className="relative">
|
||||
<Button
|
||||
type="submit"
|
||||
loading={clickedMethod === "email"}
|
||||
disabled={!emailValidation.success || !!clickedMethod}
|
||||
className={cn(
|
||||
"focus:shadow-outline w-full transform rounded px-4 py-2 text-white transition-colors duration-300 ease-in-out focus:outline-none",
|
||||
clickedMethod === "email"
|
||||
? "bg-black"
|
||||
: "bg-gray-800 hover:bg-gray-900",
|
||||
)}
|
||||
>
|
||||
{emailButtonText}
|
||||
</Button>
|
||||
{lastUsed === "credentials" && <LastUsed />}
|
||||
</div>
|
||||
</form>
|
||||
<p className="py-4 text-center">or</p>
|
||||
<div className="flex flex-col space-y-2 px-4 sm:px-12">
|
||||
<div className="relative">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setClickedMethod("google");
|
||||
setLastUsed("google");
|
||||
signIn("google", {
|
||||
...(next && next.length > 0 ? { callbackUrl: next } : {}),
|
||||
}).then((res) => {
|
||||
setClickedMethod(undefined);
|
||||
});
|
||||
}}
|
||||
loading={clickedMethod === "google"}
|
||||
disabled={clickedMethod && clickedMethod !== "google"}
|
||||
className="flex w-full items-center justify-center space-x-2 border border-gray-300 bg-gray-100 font-normal text-gray-900 hover:bg-gray-200"
|
||||
>
|
||||
<Google className="h-5 w-5" />
|
||||
<span>Continue with Google</span>
|
||||
{clickedMethod !== "google" && lastUsed === "google" && (
|
||||
<LastUsed />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setClickedMethod("linkedin");
|
||||
setLastUsed("linkedin");
|
||||
signIn("linkedin", {
|
||||
...(next && next.length > 0 ? { callbackUrl: next } : {}),
|
||||
}).then((res) => {
|
||||
setClickedMethod(undefined);
|
||||
});
|
||||
}}
|
||||
loading={clickedMethod === "linkedin"}
|
||||
disabled={clickedMethod && clickedMethod !== "linkedin"}
|
||||
className="flex w-full items-center justify-center space-x-2 border border-gray-300 bg-gray-100 font-normal text-gray-900 hover:bg-gray-200"
|
||||
>
|
||||
<LinkedIn />
|
||||
<span>Continue with LinkedIn</span>
|
||||
{clickedMethod !== "linkedin" && lastUsed === "linkedin" && (
|
||||
<LastUsed />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setLastUsed("passkey");
|
||||
setClickedMethod("passkey");
|
||||
signInWithPasskey({
|
||||
tenantId: process.env.NEXT_PUBLIC_HANKO_TENANT_ID as string,
|
||||
}).then(() => {
|
||||
setClickedMethod(undefined);
|
||||
});
|
||||
}}
|
||||
variant="outline"
|
||||
loading={clickedMethod === "passkey"}
|
||||
disabled={clickedMethod && clickedMethod !== "passkey"}
|
||||
className="flex w-full items-center justify-center space-x-2 border border-gray-300 bg-gray-100 font-normal text-gray-900 hover:bg-gray-200 hover:text-gray-900"
|
||||
>
|
||||
<Passkey className="h-4 w-4" />
|
||||
<span>Continue with a passkey</span>
|
||||
{lastUsed === "passkey" && <LastUsed />}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<SSOLogin autoExpand={isSSORequired} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-10 w-full max-w-md px-4 text-xs text-muted-foreground sm:px-12">
|
||||
By clicking continue, you acknowledge that you have read and agree
|
||||
to Papermark's{" "}
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_MARKETING_URL}/terms`}
|
||||
target="_blank"
|
||||
className="underline"
|
||||
>
|
||||
Terms of Service
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_MARKETING_URL}/privacy`}
|
||||
target="_blank"
|
||||
className="underline"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative hidden w-full justify-center overflow-hidden bg-black md:flex md:w-1/2 lg:w-1/2">
|
||||
<div className="relative m-0 flex h-full min-h-[700px] w-full p-0">
|
||||
<div
|
||||
className="relative flex h-full w-full flex-col justify-between"
|
||||
id="features"
|
||||
Terms of Service
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href={`${MARKETING_URL}/privacy`}
|
||||
target="_blank"
|
||||
className="text-zinc-300 underline hover:text-white"
|
||||
>
|
||||
{/* Testimonial top 2/3 */}
|
||||
<div
|
||||
className="flex w-full flex-col items-center justify-center"
|
||||
style={{ height: "66.6666%" }}
|
||||
>
|
||||
{/* Image container */}
|
||||
<div className="mb-4 h-64 w-80">
|
||||
<img
|
||||
className="h-full w-full rounded-2xl object-cover shadow-2xl"
|
||||
src="/_static/testimonials/backtrace.jpeg"
|
||||
alt="Backtrace Capital"
|
||||
/>
|
||||
</div>
|
||||
{/* Text content */}
|
||||
<div className="max-w-xl text-center">
|
||||
<blockquote className="text-balance font-normal leading-8 text-white sm:text-xl sm:leading-9">
|
||||
<p>
|
||||
"We raised our €30M Fund with Papermark Data Rooms.
|
||||
Love the customization, security and ease of use."
|
||||
</p>
|
||||
</blockquote>
|
||||
<figcaption className="mt-4">
|
||||
<div className="text-balance font-normal text-white">
|
||||
Michael Münnix
|
||||
</div>
|
||||
<div className="text-balance font-light text-gray-400">
|
||||
Partner, Backtrace Capital
|
||||
</div>
|
||||
</figcaption>
|
||||
</div>
|
||||
</div>
|
||||
{/* White block with logos bottom 1/3, full width/height */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0 flex w-full flex-col items-center justify-center bg-white"
|
||||
style={{ height: "33.3333%" }}
|
||||
>
|
||||
<div className="mb-4 max-w-xl text-balance text-center font-semibold text-gray-900">
|
||||
Trusted by teams at
|
||||
</div>
|
||||
<LogoCloud />
|
||||
{/* <img
|
||||
src="https://assets.papermark.io/upload/file_7JEGY7zM9ZTfmxu8pe7vWj-Screenshot-2025-05-09-at-18.09.13.png"
|
||||
alt="Trusted teams illustration"
|
||||
className="mt-4 max-w-full h-auto object-contain"
|
||||
style={{maxHeight: '120px'}}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Privacy Policy
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { Metadata } from "next";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { APP_NAME, APP_URL } from "@/lib/branding";
|
||||
import { GTMComponent } from "@/components/gtm-component";
|
||||
|
||||
import LoginClient from "./page-client";
|
||||
|
||||
const data = {
|
||||
description: "Login to Papermark",
|
||||
title: "Login | Papermark",
|
||||
description: `Login to ${APP_NAME}`,
|
||||
title: `Login | ${APP_NAME}`,
|
||||
url: "/login",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://www.papermark.com"),
|
||||
metadataBase: new URL(APP_URL),
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
openGraph: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
siteName: "Papermark",
|
||||
siteName: APP_NAME,
|
||||
images: [
|
||||
{
|
||||
url: "/_static/meta-image.png",
|
||||
@@ -33,7 +35,7 @@ export const metadata: Metadata = {
|
||||
card: "summary_large_image",
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
creator: "@papermarkio",
|
||||
creator: "@hanzoai",
|
||||
images: ["/_static/meta-image.png"],
|
||||
},
|
||||
};
|
||||
@@ -42,7 +44,9 @@ export default function LoginPage() {
|
||||
return (
|
||||
<>
|
||||
<GTMComponent />
|
||||
<LoginClient />
|
||||
<Suspense>
|
||||
<LoginClient />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useParams } from "next/navigation";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import PapermarkLogo from "@/public/_static/papermark-logo.svg";
|
||||
import HanzoLogo from "@/public/_static/hanzo-dataroom-logo.svg";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -35,12 +35,12 @@ export default function Register() {
|
||||
</div>
|
||||
<div className="z-10 mx-5 mt-[calc(20vh)] h-fit w-full max-w-md overflow-hidden rounded-lg border border-border bg-gray-50 dark:bg-gray-900 sm:mx-0 sm:shadow-xl">
|
||||
<div className="flex flex-col items-center justify-center space-y-3 px-4 py-6 pt-8 text-center sm:px-16">
|
||||
<Link href="https://www.papermark.com" target="_blank">
|
||||
<Link href="https://dataroom.hanzo.ai" target="_blank">
|
||||
<Image
|
||||
src={PapermarkLogo}
|
||||
src={HanzoLogo}
|
||||
width={119}
|
||||
height={32}
|
||||
alt="Papermark Logo"
|
||||
alt="Hanzo Dataroom Logo"
|
||||
/>
|
||||
</Link>
|
||||
<h3 className="text-2xl font-medium text-foreground">
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { Metadata } from "next";
|
||||
|
||||
import { APP_NAME, APP_URL } from "@/lib/branding";
|
||||
import RegisterClient from "./page-client";
|
||||
|
||||
const data = {
|
||||
description: "Signup to Papermark",
|
||||
title: "Sign up | Papermark",
|
||||
description: `Signup to ${APP_NAME}`,
|
||||
title: `Sign up | ${APP_NAME}`,
|
||||
url: "/register",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://www.papermark.com"),
|
||||
metadataBase: new URL(APP_URL),
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
openGraph: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
siteName: "Papermark",
|
||||
siteName: APP_NAME,
|
||||
images: [
|
||||
{
|
||||
url: "/_static/meta-image.png",
|
||||
@@ -31,7 +32,7 @@ export const metadata: Metadata = {
|
||||
card: "summary_large_image",
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
creator: "@papermarkio",
|
||||
creator: "@hanzoai",
|
||||
images: ["/_static/meta-image.png"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function InvitationStatusContent({
|
||||
</div>
|
||||
<div className="w-full space-y-4">
|
||||
<h4 className="text-center text-sm font-medium text-gray-800">
|
||||
Create your own Papermark account
|
||||
Create your own Hanzo Dataroom account
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
<Link href="/login" className="block w-full">
|
||||
|
||||
@@ -13,20 +13,20 @@ import InvitationStatusContent from "./InvitationStatusContent";
|
||||
import CleanUrlOnExpire from "./status/ClientRedirect";
|
||||
|
||||
const data = {
|
||||
description: "Accept your team invitation on Papermark",
|
||||
title: "Accept Invitation | Papermark",
|
||||
description: "Accept your team invitation on Hanzo Dataroom",
|
||||
title: "Accept Invitation | Hanzo Dataroom",
|
||||
url: "/verify/invitation",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://www.papermark.com"),
|
||||
metadataBase: new URL("https://dataroom.hanzo.ai"),
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
openGraph: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
siteName: "Papermark",
|
||||
siteName: "Hanzo Dataroom",
|
||||
images: [
|
||||
{
|
||||
url: "/_static/meta-image.png",
|
||||
@@ -41,7 +41,7 @@ export const metadata: Metadata = {
|
||||
card: "summary_large_image",
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
creator: "@papermarkio",
|
||||
creator: "@hanzoai",
|
||||
images: ["/_static/meta-image.png"],
|
||||
},
|
||||
};
|
||||
@@ -49,11 +49,11 @@ export const metadata: Metadata = {
|
||||
export default async function VerifyInvitationPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: {
|
||||
searchParams: Promise<{
|
||||
token?: string;
|
||||
};
|
||||
}>;
|
||||
}) {
|
||||
const { token: jwtToken } = searchParams;
|
||||
const { token: jwtToken } = await searchParams;
|
||||
|
||||
if (!jwtToken) {
|
||||
return <NotFound />;
|
||||
@@ -100,13 +100,13 @@ export default async function VerifyInvitationPage({
|
||||
<div className="flex flex-col items-center justify-center space-y-3 px-4 py-6 pt-8 text-center sm:px-16">
|
||||
<Link href="/">
|
||||
<span className="text-balance text-2xl font-semibold text-gray-800">
|
||||
Welcome to Papermark
|
||||
Welcome to Hanzo Dataroom
|
||||
</span>
|
||||
</Link>
|
||||
{!isExpired && !isRevoked && (
|
||||
<>
|
||||
<h3 className="text-balance py-1 text-sm font-normal text-gray-800">
|
||||
You've been invited to join a team on Papermark
|
||||
You've been invited to join a team on Hanzo Dataroom
|
||||
</h3>
|
||||
<div className="mt-2 flex w-auto items-center justify-center gap-2 rounded-full bg-gray-50 px-5 py-2.5 text-sm text-gray-600 shadow-sm">
|
||||
<MailIcon className="h-4 w-4 text-gray-400" />
|
||||
@@ -148,7 +148,7 @@ export default async function VerifyInvitationPage({
|
||||
</div>
|
||||
<p className="mt-10 w-full max-w-md px-4 text-xs text-muted-foreground sm:px-16">
|
||||
By accepting this invitation, you acknowledge that you have
|
||||
read and agree to Papermark's{" "}
|
||||
read and agree to Hanzo Dataroom's{" "}
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_MARKETING_URL}/terms`}
|
||||
target="_blank"
|
||||
@@ -191,7 +191,7 @@ export default async function VerifyInvitationPage({
|
||||
<blockquote className="text-l text-balance leading-8 text-gray-100 sm:text-xl sm:leading-9">
|
||||
<p>
|
||||
True builders listen to their users and build what they
|
||||
need. Thanks Papermark team for solving a big pain point.
|
||||
need. Thanks Hanzo Dataroom team for solving a big pain point.
|
||||
DocSend monopoly will end soon!
|
||||
</p>
|
||||
</blockquote>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../ee/LICENSE.md
|
||||
@@ -1 +0,0 @@
|
||||
../ee/README.md
|
||||
+45
-9
@@ -1,10 +1,10 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { generateChatTitle } from "@/ee/features/ai/lib/chat/generate-chat-title";
|
||||
import { getFilteredDataroomDocumentIds } from "@/ee/features/ai/lib/chat/get-filtered-dataroom-document-ids";
|
||||
import { sendMessage } from "@/ee/features/ai/lib/chat/send-message";
|
||||
import { validateChatAccess } from "@/ee/features/ai/lib/permissions/validate-chat-access";
|
||||
import { sendMessageSchema } from "@/ee/features/ai/schemas/chat";
|
||||
import { generateChatTitle } from "@/features/ai/lib/chat/generate-chat-title";
|
||||
import { getFilteredDataroomDocumentIds } from "@/features/ai/lib/chat/get-filtered-dataroom-document-ids";
|
||||
import { sendMessage } from "@/features/ai/lib/chat/send-message";
|
||||
import { validateChatAccess } from "@/features/ai/lib/permissions/validate-chat-access";
|
||||
import { sendMessageSchema } from "@/features/ai/schemas/chat";
|
||||
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -35,8 +35,11 @@ export async function POST(
|
||||
);
|
||||
}
|
||||
|
||||
const { content, filterDocumentId, filterDataroomDocumentIds } =
|
||||
validation.data;
|
||||
const {
|
||||
content,
|
||||
filterDocumentId,
|
||||
filterDataroomDocumentIds,
|
||||
} = validation.data;
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
@@ -133,16 +136,49 @@ export async function POST(
|
||||
}
|
||||
|
||||
// Send message and get streaming response
|
||||
const result = await sendMessage({
|
||||
const { result, referencesForStream } = await sendMessage({
|
||||
chatId,
|
||||
content,
|
||||
vectorStoreId: chat.vectorStoreId,
|
||||
filteredDataroomDocumentIds,
|
||||
filterDocumentId,
|
||||
userSelectedDataroomDocumentIds: filterDataroomDocumentIds,
|
||||
dataroomId: chat.dataroomId || undefined,
|
||||
linkId: chat.linkId || undefined,
|
||||
});
|
||||
|
||||
return result.toTextStreamResponse();
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
try {
|
||||
for await (const chunk of result.textStream) {
|
||||
controller.enqueue(encoder.encode(chunk));
|
||||
}
|
||||
|
||||
const referencesSection = await Promise.race([
|
||||
referencesForStream,
|
||||
new Promise<string>((resolve) =>
|
||||
setTimeout(() => resolve(""), 5000),
|
||||
),
|
||||
]);
|
||||
|
||||
if (referencesSection) {
|
||||
controller.enqueue(encoder.encode(referencesSection));
|
||||
}
|
||||
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
console.error("Error streaming AI response:", error);
|
||||
controller.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error sending message:", error);
|
||||
return new Response(JSON.stringify({ error: "Internal server error" }), {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { validateChatAccess } from "@/ee/features/ai/lib/permissions/validate-chat-access";
|
||||
import { validateChatAccess } from "@/features/ai/lib/permissions/validate-chat-access";
|
||||
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { createChat } from "@/ee/features/ai/lib/chat/create-chat";
|
||||
import { createChatSchema } from "@/ee/features/ai/schemas/chat";
|
||||
import { createChat } from "@/features/ai/lib/chat/create-chat";
|
||||
import { createChatSchema } from "@/features/ai/schemas/chat";
|
||||
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
+2
-2
@@ -4,8 +4,8 @@ import {
|
||||
SUPPORTED_AI_CONTENT_TYPES,
|
||||
addFileToVectorStoreTask,
|
||||
processDocumentForAITask,
|
||||
} from "@/ee/features/ai/lib/trigger";
|
||||
import { createDataroomVectorStore } from "@/ee/features/ai/lib/vector-stores/create-dataroom-vector-store";
|
||||
} from "@/features/ai/lib/trigger";
|
||||
import { createDataroomVectorStore } from "@/features/ai/lib/vector-stores/create-dataroom-vector-store";
|
||||
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
+3
-3
@@ -4,9 +4,9 @@ import {
|
||||
addFileToVectorStoreTask,
|
||||
processDocumentForAITask,
|
||||
SUPPORTED_AI_CONTENT_TYPES,
|
||||
} from "@/ee/features/ai/lib/trigger";
|
||||
import { createTeamVectorStore } from "@/ee/features/ai/lib/vector-stores/create-team-vector-store";
|
||||
import { removeFileFromVectorStore } from "@/ee/features/ai/lib/vector-stores/remove-file-from-vector-store";
|
||||
} from "@/features/ai/lib/trigger";
|
||||
import { createTeamVectorStore } from "@/features/ai/lib/vector-stores/create-team-vector-store";
|
||||
import { removeFileFromVectorStore } from "@/features/ai/lib/vector-stores/remove-file-from-vector-store";
|
||||
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { getVectorStoreInfo } from "@/ee/features/ai/lib/vector-stores/get-vector-store-info";
|
||||
import { getVectorStoreInfo } from "@/features/ai/lib/vector-stores/get-vector-store-info";
|
||||
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { receiver } from "@/lib/cron";
|
||||
import { processDataroomDigest } from "@/lib/emails/process-dataroom-digest";
|
||||
import { log } from "@/lib/utils";
|
||||
|
||||
// Runs daily at 9 AM UTC (0 9 * * *)
|
||||
export const maxDuration = 300;
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const body = await req.json();
|
||||
if (process.env.VERCEL === "1") {
|
||||
const isValid = await receiver.verify({
|
||||
signature: req.headers.get("Upstash-Signature") || "",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!isValid) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await processDataroomDigest("daily");
|
||||
return NextResponse.json({ success: true, ...result });
|
||||
} catch (error) {
|
||||
await log({
|
||||
message: `Daily dataroom digest cron failed. \n\nError: ${(error as Error).message}`,
|
||||
type: "cron",
|
||||
mention: true,
|
||||
});
|
||||
return NextResponse.json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { receiver } from "@/lib/cron";
|
||||
import { processDataroomDigest } from "@/lib/emails/process-dataroom-digest";
|
||||
import { log } from "@/lib/utils";
|
||||
|
||||
// Runs weekly on Monday at 9 AM UTC (0 9 * * 1)
|
||||
export const maxDuration = 300;
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const body = await req.json();
|
||||
if (process.env.VERCEL === "1") {
|
||||
const isValid = await receiver.verify({
|
||||
signature: req.headers.get("Upstash-Signature") || "",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!isValid) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await processDataroomDigest("weekly");
|
||||
return NextResponse.json({ success: true, ...result });
|
||||
} catch (error) {
|
||||
await log({
|
||||
message: `Weekly dataroom digest cron failed. \n\nError: ${(error as Error).message}`,
|
||||
type: "cron",
|
||||
mention: true,
|
||||
});
|
||||
return NextResponse.json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export async function POST(req: Request) {
|
||||
where: {
|
||||
slug: {
|
||||
not: {
|
||||
in: ["papermark.io", "papermark.com"],
|
||||
in: ["dataroom.hanzo.ai", "dataroom.hanzo.ai"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,6 +10,107 @@ import { supportsAdvancedExcelMode } from "@/lib/utils/get-content-type";
|
||||
import { runs } from "@trigger.dev/sdk/v3";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
|
||||
/**
|
||||
* GET /api/links/[id]/upload?dataroomId=xxx
|
||||
* Returns the viewer's previously uploaded documents for this dataroom.
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
) {
|
||||
try {
|
||||
const linkId = params.id;
|
||||
const dataroomId = request.nextUrl.searchParams.get("dataroomId");
|
||||
|
||||
if (!linkId || !dataroomId) {
|
||||
return NextResponse.json(
|
||||
{ message: "Missing required parameters" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the dataroom session
|
||||
const dataroomSession = await verifyDataroomSession(
|
||||
request,
|
||||
linkId,
|
||||
dataroomId,
|
||||
);
|
||||
|
||||
if (!dataroomSession || !dataroomSession.viewerId) {
|
||||
return NextResponse.json(
|
||||
{ message: "Unauthorized" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const { viewerId } = dataroomSession;
|
||||
|
||||
// Fetch the viewer's uploads for this dataroom
|
||||
const uploads = await prisma.documentUpload.findMany({
|
||||
where: {
|
||||
viewerId,
|
||||
dataroomId,
|
||||
linkId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
documentId: true,
|
||||
dataroomDocumentId: true,
|
||||
originalFilename: true,
|
||||
uploadedAt: true,
|
||||
document: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
type: true,
|
||||
versions: {
|
||||
where: { isPrimary: true },
|
||||
select: {
|
||||
id: true,
|
||||
hasPages: true,
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
dataroomDocument: {
|
||||
select: {
|
||||
folderId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { uploadedAt: "desc" },
|
||||
});
|
||||
|
||||
const formattedUploads = uploads.map((upload) => {
|
||||
const fileType = upload.document?.type ?? "";
|
||||
const hasPages = upload.document?.versions?.[0]?.hasPages ?? false;
|
||||
const needsProcessing = ["pdf", "docs", "slides"].includes(fileType);
|
||||
const isComplete = !needsProcessing || hasPages;
|
||||
|
||||
return {
|
||||
id: upload.id,
|
||||
documentId: upload.documentId,
|
||||
dataroomDocumentId: upload.dataroomDocumentId,
|
||||
documentVersionId: upload.document?.versions?.[0]?.id ?? null,
|
||||
name: upload.originalFilename ?? upload.document?.name ?? "Unknown",
|
||||
fileType,
|
||||
folderId: upload.dataroomDocument?.folderId ?? null,
|
||||
uploadedAt: upload.uploadedAt,
|
||||
status: isComplete ? "complete" : "processing",
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({ uploads: formattedUploads });
|
||||
} catch (error) {
|
||||
console.error("Error fetching viewer uploads:", error);
|
||||
return NextResponse.json(
|
||||
{ message: "Error fetching uploads" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
@@ -167,7 +268,7 @@ export async function POST(
|
||||
viewId: viewId,
|
||||
linkId: linkId,
|
||||
originalFilename: document.name,
|
||||
fileSize: document.versions[0].fileSize,
|
||||
fileSize: documentData.fileSize ?? 0,
|
||||
numPages: document.numPages,
|
||||
mimeType: document.contentType,
|
||||
dataroomId: dataroomId,
|
||||
@@ -226,7 +327,20 @@ export async function POST(
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
// Return document data for optimistic UI rendering
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
document: {
|
||||
id: document.id,
|
||||
name: document.name,
|
||||
dataroomDocumentId: newDataroomDocument.id,
|
||||
documentVersionId: document.versions[0]?.id,
|
||||
folderId: dataroomFolderId,
|
||||
fileType: document.type,
|
||||
hasPages: (document.numPages ?? 0) > 0,
|
||||
createdAt: document.createdAt,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error uploading document:", error);
|
||||
return NextResponse.json(
|
||||
@@ -10,7 +10,7 @@ export const runtime = "edge";
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const title = searchParams.get("title") || "Papermark Document";
|
||||
const title = searchParams.get("title") || "Hanzo Dataroom Document";
|
||||
const Inter = await fetch(
|
||||
new URL("@/public/_static/Inter-Bold.ttf", import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
@@ -18,7 +18,7 @@ export async function GET(request: NextRequest) {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div tw="flex flex-col items-center justify-between w-full h-full bg-white text-black p-12">
|
||||
<div tw="text-[32px] flex items-center tracking-tighter">Papermark</div>
|
||||
<div tw="text-[32px] flex items-center tracking-tighter">Hanzo Dataroom</div>
|
||||
<div tw="text-[42px] text-center">{title}</div>
|
||||
<div tw="text-[32px] flex items-center">
|
||||
Open-Source Document Sharing
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function GET(req: NextRequest) {
|
||||
{/* Left Side Text */}
|
||||
<div tw="flex flex-col text-white" style={{ marginLeft: "48px" }}>
|
||||
<div tw="flex text-7xl font-bold mb-4 tracking-tighter">
|
||||
Papermark
|
||||
Hanzo Dataroom
|
||||
</div>
|
||||
<div tw="flex text-5xl mb-4">Year in Review</div>
|
||||
<div tw="flex text-7xl font-bold">{year}</div>
|
||||
@@ -49,7 +49,7 @@ export async function GET(req: NextRequest) {
|
||||
{/* Header Section */}
|
||||
<div tw="flex items-start p-8 items-center">
|
||||
<div tw="flex text-2xl font-bold text-white tracking-tighter">
|
||||
Papermark
|
||||
Hanzo Dataroom
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ export async function POST(
|
||||
const result = await directorySyncController.directories.create({
|
||||
tenant: teamId,
|
||||
product: jacksonProduct,
|
||||
name: name || "Papermark SCIM Directory",
|
||||
name: name || "Hanzo Dataroom SCIM Directory",
|
||||
type: type || "azure-scim-v2",
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { reportDeniedAccessAttempt } from "@/ee/features/access-notifications";
|
||||
import { getTeamStorageConfigById } from "@/ee/features/storage/config";
|
||||
import { reportDeniedAccessAttempt } from "@/features/access-notifications";
|
||||
import { getTeamStorageConfigById } from "@/features/storage/config";
|
||||
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
||||
import { ItemType, LinkAudienceType } from "@prisma/client";
|
||||
import { ipAddress, waitUntil } from "@vercel/functions";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { reportDeniedAccessAttempt } from "@/ee/features/access-notifications";
|
||||
import { getTeamStorageConfigById } from "@/ee/features/storage/config";
|
||||
import { reportDeniedAccessAttempt } from "@/features/access-notifications";
|
||||
import { getTeamStorageConfigById } from "@/features/storage/config";
|
||||
// Import authOptions directly from the source
|
||||
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
||||
import { ipAddress, waitUntil } from "@vercel/functions";
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { WorkflowEngine } from "@/ee/features/workflows/lib/engine";
|
||||
import { WorkflowEngine } from "@/features/workflows/lib/engine";
|
||||
import {
|
||||
AccessRequestSchema,
|
||||
VerifyEmailRequestSchema,
|
||||
} from "@/ee/features/workflows/lib/types";
|
||||
} from "@/features/workflows/lib/types";
|
||||
import { ipAddress, waitUntil } from "@vercel/functions";
|
||||
import { z } from "zod";
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { WorkflowEngine } from "@/ee/features/workflows/lib/engine";
|
||||
import { AccessRequestSchema } from "@/ee/features/workflows/lib/types";
|
||||
import { WorkflowEngine } from "@/features/workflows/lib/engine";
|
||||
import { AccessRequestSchema } from "@/features/workflows/lib/types";
|
||||
import { ipAddress } from "@vercel/functions";
|
||||
import { z } from "zod";
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { ratelimit } from "@/lib/redis";
|
||||
import { sendOtpVerificationEmail } from "@/lib/emails/send-email-otp-verification";
|
||||
import { generateOTP } from "@/lib/utils/generate-otp";
|
||||
import { validateEmail } from "@/lib/utils/validate-email";
|
||||
import { VerifyEmailRequestSchema } from "@/ee/features/workflows/lib/types";
|
||||
import { VerifyEmailRequestSchema } from "@/features/workflows/lib/types";
|
||||
|
||||
// POST /app/(ee)/api/workflow-entry/[entryLinkId]/verify - Send OTP
|
||||
export async function POST(
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
UpdateWorkflowRequestSchema,
|
||||
formatZodError,
|
||||
} from "@/ee/features/workflows/lib/validation";
|
||||
} from "@/features/workflows/lib/validation";
|
||||
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { getServerSession } from "next-auth";
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {
|
||||
formatZodError,
|
||||
validateActions,
|
||||
validateConditions,
|
||||
} from "@/ee/features/workflows/lib/validation";
|
||||
} from "@/features/workflows/lib/validation";
|
||||
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
+2
-2
@@ -10,8 +10,8 @@ import {
|
||||
formatZodError,
|
||||
validateConditions,
|
||||
validateActions,
|
||||
} from "@/ee/features/workflows/lib/validation";
|
||||
import { ReorderStepsRequest } from "@/ee/features/workflows/lib/types";
|
||||
} from "@/features/workflows/lib/validation";
|
||||
import { ReorderStepsRequest } from "@/features/workflows/lib/types";
|
||||
|
||||
// GET /app/(ee)/api/workflows/[workflowId]/steps?teamId=xxx - List all steps
|
||||
export async function GET(
|
||||
@@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
CreateWorkflowRequestSchema,
|
||||
formatZodError,
|
||||
} from "@/ee/features/workflows/lib/validation";
|
||||
} from "@/features/workflows/lib/validation";
|
||||
import { authOptions } from "@/pages/api/auth/[...nextauth]";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
+8
-7
@@ -5,22 +5,23 @@ import "@/styles/globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
import { APP_DESCRIPTION, APP_NAME, APP_URL } from "@/lib/branding";
|
||||
|
||||
const data = {
|
||||
description:
|
||||
"Papermark is an open-source document sharing infrastructure. Free alternative to Docsend with custom domain. Manage secure document sharing with real-time analytics.",
|
||||
title: "Papermark | The Open Source DocSend Alternative",
|
||||
description: APP_DESCRIPTION,
|
||||
title: `${APP_NAME} | Secure Data Room Infrastructure`,
|
||||
url: "/",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://www.papermark.com"),
|
||||
metadataBase: new URL(APP_URL),
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
openGraph: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
siteName: "Papermark",
|
||||
siteName: APP_NAME,
|
||||
images: [
|
||||
{
|
||||
url: "/_static/meta-image.png",
|
||||
@@ -35,7 +36,7 @@ export const metadata: Metadata = {
|
||||
card: "summary_large_image",
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
creator: "@papermarkio",
|
||||
creator: "@hanzoai",
|
||||
images: ["/_static/meta-image.png"],
|
||||
},
|
||||
};
|
||||
@@ -46,7 +47,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" className="dark">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
User-Agent: *
|
||||
Disallow: /login
|
||||
Disallow: /register
|
||||
Disallow: /verify/
|
||||
Disallow: /auth/
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import {
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import {
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import { differenceInDays, format, startOfDay, subDays } from "date-fns";
|
||||
import { CalendarIcon, ChevronDown, CrownIcon } from "lucide-react";
|
||||
import { DateRange } from "react-day-picker";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import {
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import {
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { getPriceIdFromPlan } from "@/ee/stripe/functions/get-price-id-from-plan";
|
||||
import { getQuantityFromPriceId } from "@/ee/stripe/functions/get-quantity-from-plan";
|
||||
import { getPriceIdFromPlan } from "@/lib/billing/legacy/functions/get-price-id-from-plan";
|
||||
import { getQuantityFromPriceId } from "@/lib/billing/legacy/functions/get-quantity-from-plan";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useAnalytics } from "@/lib/analytics";
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Stub `CancellationModal` — the commercial cancellation funnel was removed
|
||||
* with the AGPL paywall rip. We keep the component as a no-op so existing
|
||||
* call sites compile.
|
||||
*/
|
||||
|
||||
export type CancellationModalProps = {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export function CancellationModal(_props: CancellationModalProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default CancellationModal;
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { getPriceIdFromPlan } from "@/ee/stripe/functions/get-price-id-from-plan";
|
||||
import { getPriceIdFromPlan } from "@/lib/billing/legacy/functions/get-price-id-from-plan";
|
||||
import Cookies from "js-cookie";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function ProAnnualBanner({
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
<div className="flex space-x-2">
|
||||
<span className="text-sm font-bold">Papermark Pro Annual ✨</span>
|
||||
<span className="text-sm font-bold">Hanzo Dataroom Pro Annual ✨</span>
|
||||
</div>
|
||||
<p className="my-4 text-sm">
|
||||
Lock in a better price and get 2 months free.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
import X from "@/components/shared/icons/x";
|
||||
@@ -31,7 +31,7 @@ export default function ProBanner({
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
<div className="flex space-x-2">
|
||||
<span className="text-sm font-bold">✨ Papermark Business ✨</span>
|
||||
<span className="text-sm font-bold">✨ Hanzo Dataroom Business ✨</span>
|
||||
</div>
|
||||
<p className="my-4 text-sm">
|
||||
Upgrade to unlock custom branding, team members, domains and data rooms.
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { CancellationModal } from "@/ee/features/billing/cancellation/components";
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { CancellationModal } from "@/components/billing/cancellation-modal";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import {
|
||||
BanIcon,
|
||||
CirclePauseIcon,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { getStripe } from "@/ee/stripe/client";
|
||||
import { PLANS } from "@/ee/stripe/utils";
|
||||
import { getStripe } from "@/lib/billing/legacy/client";
|
||||
import { PLANS } from "@/lib/billing/legacy/utils";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -53,7 +53,7 @@ export function UpgradePlanModal({
|
||||
"Custom branding",
|
||||
"Folder organization",
|
||||
"Require email verification",
|
||||
"Papermark branding removed",
|
||||
"Hanzo Dataroom branding removed",
|
||||
"1-year analytics retention",
|
||||
];
|
||||
}
|
||||
@@ -287,11 +287,11 @@ export function UpgradePlanModal({
|
||||
</DataroomTrialModal>
|
||||
) : (
|
||||
<a
|
||||
href="https://cal.com/marcseitz/papermark"
|
||||
href="https://dataroom.hanzo.ai/contact"
|
||||
target="_blank"
|
||||
className="underline-offset-4 transition-all hover:text-gray-800 hover:underline hover:dark:text-muted-foreground/80"
|
||||
>
|
||||
Looking for Papermark Enterprise?
|
||||
Looking for Hanzo Dataroom Enterprise?
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,10 +5,10 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { getStripe } from "@/ee/stripe/client";
|
||||
import { Feature, PlanEnum, getPlanFeatures } from "@/ee/stripe/constants";
|
||||
import { getPriceIdFromPlan } from "@/ee/stripe/functions/get-price-id-from-plan";
|
||||
import { PLANS } from "@/ee/stripe/utils";
|
||||
import { getStripe } from "@/lib/billing/legacy/client";
|
||||
import { Feature, PlanEnum, getPlanFeatures } from "@/lib/billing/legacy/constants";
|
||||
import { getPriceIdFromPlan } from "@/lib/billing/legacy/functions/get-price-id-from-plan";
|
||||
import { PLANS } from "@/lib/billing/legacy/utils";
|
||||
import {
|
||||
CheckIcon,
|
||||
CircleHelpIcon,
|
||||
|
||||
@@ -5,10 +5,10 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { getStripe } from "@/ee/stripe/client";
|
||||
import { Feature, PlanEnum, getPlanFeatures } from "@/ee/stripe/constants";
|
||||
import { getPriceIdFromPlan } from "@/ee/stripe/functions/get-price-id-from-plan";
|
||||
import { PLANS } from "@/ee/stripe/utils";
|
||||
import { getStripe } from "@/lib/billing/legacy/client";
|
||||
import { Feature, PlanEnum, getPlanFeatures } from "@/lib/billing/legacy/constants";
|
||||
import { getPriceIdFromPlan } from "@/lib/billing/legacy/functions/get-price-id-from-plan";
|
||||
import { PLANS } from "@/lib/billing/legacy/utils";
|
||||
import { CheckIcon, CircleHelpIcon, Users2Icon, XIcon } from "lucide-react";
|
||||
|
||||
import { useAnalytics } from "@/lib/analytics";
|
||||
|
||||
@@ -5,9 +5,9 @@ import { useRouter } from "next/router";
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { PlanEnum, getPlanFeatures } from "@/ee/stripe/constants";
|
||||
import { getPriceIdFromPlan } from "@/ee/stripe/functions/get-price-id-from-plan";
|
||||
import { PLANS } from "@/ee/stripe/utils";
|
||||
import { PlanEnum, getPlanFeatures } from "@/lib/billing/legacy/constants";
|
||||
import { getPriceIdFromPlan } from "@/lib/billing/legacy/functions/get-price-id-from-plan";
|
||||
import { PLANS } from "@/lib/billing/legacy/utils";
|
||||
import Cookies from "js-cookie";
|
||||
import { CheckIcon, Sparkles, X } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
@@ -11,16 +11,19 @@ const CustomTooltip = ({
|
||||
payload: any;
|
||||
active: boolean | undefined;
|
||||
}) => {
|
||||
const router = useRouter(); // Call useRouter at the top level
|
||||
const documentId = router.query.id as string;
|
||||
const router = useRouter();
|
||||
const routerDocumentId = router.query.id as string;
|
||||
|
||||
// Default pageNumber to 0 or a sensible default if payload is not available
|
||||
const pageNumber =
|
||||
payload && payload.length > 0 ? parseInt(payload[0].payload.pageNumber) : 0;
|
||||
|
||||
// Default versionNumber to 0 or a sensible default if payload is not available
|
||||
const documentId =
|
||||
payload && payload.length > 0 && payload[0].payload.documentId
|
||||
? (payload[0].payload.documentId as string)
|
||||
: routerDocumentId;
|
||||
|
||||
const versionNumber =
|
||||
payload && payload.length > 0
|
||||
payload && payload.length > 0 && !isNaN(parseInt(payload[0].payload.versionNumber))
|
||||
? parseInt(payload[0].payload.versionNumber)
|
||||
: 1;
|
||||
const { data, error } = useDocumentThumbnail(
|
||||
|
||||
@@ -24,13 +24,18 @@ const renameDummyDurationKey = (data: Data[]): TransformedData[] => {
|
||||
}, [] as TransformedData[]);
|
||||
};
|
||||
|
||||
const renameSumDurationKey = (data: SumData[], versionNumber?: number) => {
|
||||
const renameSumDurationKey = (
|
||||
data: SumData[],
|
||||
versionNumber?: number,
|
||||
documentId?: string,
|
||||
) => {
|
||||
return data.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
"Time spent per page": item.sum_duration,
|
||||
sum_duration: undefined,
|
||||
versionNumber: versionNumber,
|
||||
versionNumber: versionNumber ?? 1,
|
||||
documentId: documentId,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -64,16 +69,18 @@ export default function BarChartComponent({
|
||||
isSum = false,
|
||||
isDummy = false,
|
||||
versionNumber,
|
||||
documentId,
|
||||
}: {
|
||||
data: any;
|
||||
isSum?: boolean;
|
||||
isDummy?: boolean;
|
||||
versionNumber?: number;
|
||||
documentId?: string;
|
||||
}) {
|
||||
const [, setValue] = useState<any>(null);
|
||||
|
||||
if (isSum) {
|
||||
const renamedData = renameSumDurationKey(data, versionNumber);
|
||||
const renamedData = renameSumDurationKey(data, versionNumber, documentId);
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ConversationListItem as ConversationListItemEE } from "@/ee/features/conversations/components/dashboard/conversation-list-item";
|
||||
import { ConversationMessage as ConversationMessageEE } from "@/ee/features/conversations/components/shared/conversation-message";
|
||||
import { ConversationListItem as ConversationListItemEE } from "@/features/conversations/components/dashboard/conversation-list-item";
|
||||
import { ConversationMessage as ConversationMessageEE } from "@/features/conversations/components/shared/conversation-message";
|
||||
|
||||
export function ConversationListItem(props: any) {
|
||||
return <ConversationListItemEE {...props} />;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import {
|
||||
FileJson,
|
||||
FileSlidersIcon,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import { ListOrderedIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import {
|
||||
Brain,
|
||||
BriefcaseIcon,
|
||||
@@ -148,7 +148,7 @@ export function AddDataroomModal({
|
||||
},
|
||||
{
|
||||
id: "fund-management",
|
||||
name: "Fund Management",
|
||||
name: "Fundraising & Reporting",
|
||||
icon: BuildingIcon,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -191,7 +191,7 @@ export default function DataroomDocumentCard({
|
||||
};
|
||||
|
||||
const handleCardClick = (e: React.MouseEvent) => {
|
||||
if (isDragging) {
|
||||
if (isDragging || menuOpen) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
@@ -288,7 +288,7 @@ export default function DataroomDocumentCard({
|
||||
<DropdownMenu open={menuOpen} onOpenChange={handleMenuStateChange}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
// size="icon"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
variant="outline"
|
||||
className="z-10 h-8 w-8 border-gray-200 bg-transparent p-0 hover:bg-gray-200 dark:border-gray-700 hover:dark:bg-gray-700 lg:h-9 lg:w-9"
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import { E164Number } from "libphonenumber-js";
|
||||
import { toast } from "sonner";
|
||||
import { mutate } from "swr";
|
||||
@@ -263,7 +263,7 @@ export function DataroomTrialModal({
|
||||
<div className="text-xs text-muted-foreground">
|
||||
After the trial, upgrade to{" "}
|
||||
<UpgradePlanModal clickedPlan={PlanEnum.Business}>
|
||||
<button className="underline">Papermark Business</button>
|
||||
<button className="underline">Hanzo Dataroom Business</button>
|
||||
</UpgradePlanModal>{" "}
|
||||
to continue using data rooms.
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { useUninvitedMembers } from "@/ee/features/dataroom-invitations/lib/swr/use-dataroom-invitations";
|
||||
import { useUninvitedMembers } from "@/features/dataroom-invitations/lib/swr/use-dataroom-invitations";
|
||||
import {
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
import { TimestampTooltip } from "@/components/ui/timestamp-tooltip";
|
||||
import { VisitorAvatar } from "@/components/visitors/visitor-avatar";
|
||||
|
||||
import { InviteViewersModal } from "../../../ee/features/dataroom-invitations/components/invite-viewers-modal";
|
||||
import { InviteViewersModal } from "@/features/dataroom-invitations/components/invite-viewers-modal";
|
||||
import { AddGroupMemberModal } from "./add-member-modal";
|
||||
|
||||
export default function GroupMemberTable({
|
||||
|
||||
@@ -2,7 +2,7 @@ import Link from "next/link";
|
||||
|
||||
import { Dispatch, SetStateAction, useMemo, useState } from "react";
|
||||
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import { CircleHelpIcon, Tag } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { mutate } from "swr";
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useRouter } from "next/router";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLimits } from "@/ee/limits/swr-handler";
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { useLimits } from "@/lib/billing/limits/swr-handler";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import { toast } from "sonner";
|
||||
import { mutate } from "swr";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import { DefaultPermissionStrategy } from "@prisma/client";
|
||||
import { parsePageId } from "notion-utils";
|
||||
import { toast } from "sonner";
|
||||
@@ -85,7 +85,7 @@ export function AddDocumentModal({
|
||||
}[]
|
||||
>([]);
|
||||
const teamInfo = useTeam();
|
||||
const { canAddDocuments } = useLimits();
|
||||
const { canAddDocuments, limits } = useLimits();
|
||||
const { plan, isFree, isTrial, isPaused } = usePlan();
|
||||
const { dataroom } = useDataroom();
|
||||
const teamId = teamInfo?.currentTeam?.id as string;
|
||||
@@ -229,7 +229,18 @@ export function AddDocumentModal({
|
||||
}
|
||||
|
||||
if (!canAddDocuments) {
|
||||
toast.error("You have reached the maximum number of documents.");
|
||||
toast.error(
|
||||
limits?.documents
|
||||
? `You've reached your plan's document limit (${limits.usage?.documents}/${limits.documents} documents). Upgrade your plan to upload more.`
|
||||
: "You have reached the maximum number of documents.",
|
||||
{
|
||||
action: {
|
||||
label: "Upgrade",
|
||||
onClick: () => router.push("/settings/billing"),
|
||||
},
|
||||
duration: 8000,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -415,7 +426,18 @@ export function AddDocumentModal({
|
||||
}
|
||||
|
||||
if (!canAddDocuments) {
|
||||
toast.error("You have reached the maximum number of documents.");
|
||||
toast.error(
|
||||
limits?.documents
|
||||
? `You've reached your plan's document limit (${limits.usage?.documents}/${limits.documents} documents). Upgrade your plan to upload more.`
|
||||
: "You have reached the maximum number of documents.",
|
||||
{
|
||||
action: {
|
||||
label: "Upgrade",
|
||||
onClick: () => router.push("/settings/billing"),
|
||||
},
|
||||
duration: 8000,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -566,7 +588,18 @@ export function AddDocumentModal({
|
||||
}
|
||||
|
||||
if (!canAddDocuments) {
|
||||
toast.error("You have reached the maximum number of documents.");
|
||||
toast.error(
|
||||
limits?.documents
|
||||
? `You've reached your plan's document limit (${limits.usage?.documents}/${limits.documents} documents). Upgrade your plan to upload more.`
|
||||
: "You have reached the maximum number of documents.",
|
||||
{
|
||||
action: {
|
||||
label: "Upgrade",
|
||||
onClick: () => router.push("/settings/billing"),
|
||||
},
|
||||
duration: 8000,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -759,7 +792,7 @@ export function AddDocumentModal({
|
||||
<>
|
||||
Upload larger files and more{" "}
|
||||
<Link
|
||||
href="https://www.papermark.com/help/article/document-types"
|
||||
href="https://dataroom.hanzo.ai/help/article/document-types"
|
||||
target="_blank"
|
||||
className="underline underline-offset-4 transition-all hover:text-muted-foreground/80 hover:dark:text-muted-foreground/80"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { TeamContextType } from "@/context/team-context";
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import {
|
||||
BetweenHorizontalStartIcon,
|
||||
ChevronRight,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useRouter } from "next/router";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { DocumentAIDialog } from "@/ee/features/ai/components/document-ai-dialog";
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { DocumentAIDialog } from "@/features/ai/components/document-ai-dialog";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import { Document, DocumentVersion } from "@prisma/client";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
|
||||
@@ -147,7 +147,7 @@ export default function FolderCard({
|
||||
};
|
||||
|
||||
const handleCardClick = (e: React.MouseEvent) => {
|
||||
if (isDragging) {
|
||||
if (isDragging || menuOpen) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
@@ -258,7 +258,7 @@ export default function FolderCard({
|
||||
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
// size="icon"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
variant="outline"
|
||||
className="z-10 h-8 w-8 border-gray-200 bg-transparent p-0 hover:bg-gray-200 dark:border-gray-700 hover:dark:bg-gray-700 lg:h-9 lg:w-9"
|
||||
>
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { useEffect, useRef, useState, type ElementType } from "react";
|
||||
import { type ElementType, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useTeam } from "@/context/team-context";
|
||||
import { PlanEnum } from "@/ee/stripe/constants";
|
||||
import { PlanEnum } from "@/lib/billing/legacy/constants";
|
||||
import { LinkType } from "@prisma/client";
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
} from "lucide-react";
|
||||
import { AlertTriangleIcon, CircleCheckIcon, InfoIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
@@ -69,7 +65,7 @@ const STATUS_CONFIG: Record<
|
||||
},
|
||||
"has site": {
|
||||
suffix:
|
||||
"is currently pointing to an existing website. Only proceed if you're sure you want to use this domain for Papermark links.",
|
||||
"is currently pointing to an existing website. Only proceed if you're sure you want to use this domain for Hanzo Dataroom links.",
|
||||
icon: InfoIcon,
|
||||
className: "bg-blue-100 text-blue-800",
|
||||
},
|
||||
@@ -143,9 +139,9 @@ export function AddDomainModal({
|
||||
return;
|
||||
}
|
||||
|
||||
if (debouncedDomain.includes("papermark")) {
|
||||
if (debouncedDomain.includes("hanzo")) {
|
||||
setDomainStatus("invalid");
|
||||
setStatusMessageOverride("Domain cannot contain 'papermark'.");
|
||||
setStatusMessageOverride("Domain cannot contain 'hanzo'.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -205,31 +201,27 @@ export function AddDomainModal({
|
||||
return toast.error("Please enter a valid domain (e.g., example.com).");
|
||||
}
|
||||
|
||||
if (normalizedDomain.includes("papermark")) {
|
||||
return toast.error("Domain cannot contain 'papermark'.");
|
||||
if (normalizedDomain.includes("hanzo")) {
|
||||
return toast.error("Domain cannot contain 'hanzo'.");
|
||||
}
|
||||
|
||||
if (saveDisabled) {
|
||||
return toast.error(
|
||||
statusMessageOverride ??
|
||||
"Please enter a valid domain before adding.",
|
||||
statusMessageOverride ?? "Please enter a valid domain before adding.",
|
||||
);
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/teams/${teamId}/domains`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
domain: normalizedDomain,
|
||||
}),
|
||||
const response = await fetch(`/api/teams/${teamId}/domains`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
body: JSON.stringify({
|
||||
domain: normalizedDomain,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const { message } = await response.json();
|
||||
@@ -370,9 +362,7 @@ export function AddDomainModal({
|
||||
"Enter a valid domain to check availability."
|
||||
)}
|
||||
</p>
|
||||
{StatusIcon && (
|
||||
<StatusIcon className="h-5 w-5 shrink-0" />
|
||||
)}
|
||||
{StatusIcon && <StatusIcon className="h-5 w-5 shrink-0" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -87,11 +87,11 @@ function DeleteDomainModal({
|
||||
<DialogTitle className="text-2xl">Delete Domain</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will permanently delete your domain. Links using this domain will
|
||||
be reset to <span className="font-medium">papermark.com</span> links.
|
||||
be reset to <span className="font-medium">dataroom.hanzo.ai</span> links.
|
||||
This action cannot be undone.
|
||||
<div className="mt-3 text-sm font-medium text-foreground">
|
||||
{domain}{" "}
|
||||
<span className="text-muted-foreground">→ papermark.com</span>
|
||||
<span className="text-muted-foreground">→ dataroom.hanzo.ai</span>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTeam } from "@/context/team-context";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
CircleCheckIcon,
|
||||
ExternalLinkIcon,
|
||||
FlagIcon,
|
||||
GlobeIcon,
|
||||
MoreVertical,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
TrashIcon,
|
||||
} from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { toast } from "sonner";
|
||||
import { mutate } from "swr";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -24,6 +26,7 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { StatusBadge } from "@/components/ui/status-badge";
|
||||
|
||||
import { useDeleteDomainModal } from "./delete-domain-modal";
|
||||
@@ -33,15 +36,21 @@ import { useDomainStatus } from "./use-domain-status";
|
||||
export default function DomainCard({
|
||||
domain,
|
||||
isDefault,
|
||||
redirectUrl: initialRedirectUrl,
|
||||
redirectsAllowed,
|
||||
onDelete,
|
||||
}: {
|
||||
domain: string;
|
||||
isDefault: boolean;
|
||||
redirectUrl?: string | null;
|
||||
redirectsAllowed: boolean;
|
||||
onDelete: (deletedDomain: string) => void;
|
||||
}) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [groupHover, setGroupHover] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [redirectUrl, setRedirectUrl] = useState(initialRedirectUrl || "");
|
||||
const [savingRedirect, setSavingRedirect] = useState(false);
|
||||
|
||||
const domainRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -65,6 +74,34 @@ export default function DomainCard({
|
||||
onDelete,
|
||||
});
|
||||
|
||||
const handleSaveRedirectUrl = async () => {
|
||||
setSavingRedirect(true);
|
||||
try {
|
||||
const response = await fetch(`/api/teams/${teamId}/domains/${domain}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ redirectUrl: redirectUrl || null }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
toast.error(data?.message || "Failed to save redirect URL");
|
||||
return;
|
||||
}
|
||||
|
||||
mutate(`/api/teams/${teamId}/domains`);
|
||||
toast.success(
|
||||
redirectUrl
|
||||
? "Root redirect URL saved"
|
||||
: "Root redirect URL removed",
|
||||
);
|
||||
} catch {
|
||||
toast.error("Failed to save redirect URL");
|
||||
} finally {
|
||||
setSavingRedirect(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMakeDefault = async () => {
|
||||
const response = await fetch(`/api/teams/${teamId}/domains/${domain}`, {
|
||||
method: "PATCH",
|
||||
@@ -232,6 +269,45 @@ export default function DomainCard({
|
||||
) : (
|
||||
<div className="mt-6 h-6 w-32 animate-pulse rounded-md bg-gray-200 dark:bg-gray-400" />
|
||||
)}
|
||||
|
||||
{/* Root domain redirect */}
|
||||
<div className="mt-4 rounded-lg border border-gray-200 p-4 dark:border-gray-400">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||
<ExternalLinkIcon className="h-4 w-4" />
|
||||
Root Domain Redirect
|
||||
</div>
|
||||
{redirectsAllowed ? (
|
||||
<>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Redirect visitors who land on{" "}
|
||||
<span className="font-medium">{domain}</span> to a specific
|
||||
URL. Leave empty to redirect to dataroom.hanzo.ai.
|
||||
</p>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<Input
|
||||
type="url"
|
||||
placeholder="https://example.com"
|
||||
value={redirectUrl}
|
||||
onChange={(e) => setRedirectUrl(e.target.value)}
|
||||
className="h-9 flex-1"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSaveRedirectUrl}
|
||||
disabled={savingRedirect}
|
||||
className="h-9 shrink-0"
|
||||
>
|
||||
{savingRedirect ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Root domain redirects require a{" "}
|
||||
<span className="font-semibold">Business</span> plan or higher.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
<DeleteDomainModal />
|
||||
|
||||
@@ -128,7 +128,7 @@ export default function DomainConfiguration({
|
||||
]}
|
||||
warning={
|
||||
txtVerification
|
||||
? "Warning: if you are using this domain for another site, setting this TXT record will transfer domain ownership away from that site and break it. Please exercise caution when setting this record; make sure that the domain that is shown in the TXT verification value is actually the <b><i>domain you want to use on Papermark</i></b> – <b><i>not your production site</i></b>."
|
||||
? "Warning: if you are using this domain for another site, setting this TXT record will transfer domain ownership away from that site and break it. Please exercise caution when setting this record; make sure that the domain that is shown in the TXT verification value is actually the <b><i>domain you want to use on Hanzo Dataroom</i></b> – <b><i>not your production site</i></b>."
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -45,12 +45,12 @@ export default function CustomDomainSetup({
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>Your Papermark custom domain set up</Preview>
|
||||
<Preview>Your Hanzo Dataroom custom domain set up</Preview>
|
||||
<Tailwind>
|
||||
<Body className="mx-auto my-auto bg-white font-sans">
|
||||
<Container className="mx-auto my-10 w-[465px] p-5">
|
||||
<Text className="mx-0 mb-8 mt-4 p-0 text-center text-2xl font-normal">
|
||||
<span className="font-bold tracking-tighter">Papermark</span>
|
||||
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
|
||||
</Text>
|
||||
<Text className="mx-0 my-7 p-0 text-center text-xl font-semibold text-black">
|
||||
{title}
|
||||
@@ -83,8 +83,8 @@ export default function CustomDomainSetup({
|
||||
</Text>
|
||||
<ol className="list-inside list-decimal text-sm">
|
||||
<li>Choose your subdomain (e.g. docs.yourcompany.com)</li>
|
||||
<li>Add a CNAME record pointing to papermark.com</li>
|
||||
<li>Configure the domain in your Papermark settings</li>
|
||||
<li>Add a CNAME record pointing to dataroom.hanzo.ai</li>
|
||||
<li>Configure the domain in your Hanzo Dataroom settings</li>
|
||||
<li>Start sharing with your branded domain!</li>
|
||||
</ol>
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function CustomDomainSetup({
|
||||
{hasAccess ? (
|
||||
<Button
|
||||
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
|
||||
href={`https://app.papermark.com/settings/domains`}
|
||||
href={`https://dataroom.hanzo.ai/settings/domains`}
|
||||
style={{ padding: "12px 20px" }}
|
||||
>
|
||||
Set up your custom domain
|
||||
@@ -100,7 +100,7 @@ export default function CustomDomainSetup({
|
||||
) : (
|
||||
<Button
|
||||
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
|
||||
href={`https://app.papermark.com/settings/upgrade`}
|
||||
href={`https://dataroom.hanzo.ai/settings/upgrade`}
|
||||
style={{ padding: "12px 20px" }}
|
||||
>
|
||||
Upgrade to use custom domains
|
||||
@@ -113,7 +113,7 @@ export default function CustomDomainSetup({
|
||||
<>
|
||||
Need help? Check out our{" "}
|
||||
<Link
|
||||
href="https://docs.papermark.com/custom-domains"
|
||||
href="https://docs.dataroom.hanzo.ai/custom-domains"
|
||||
className="font-medium text-blue-600 no-underline"
|
||||
>
|
||||
custom domain documentation
|
||||
@@ -124,7 +124,7 @@ export default function CustomDomainSetup({
|
||||
<>
|
||||
Want to learn more about our plans?{" "}
|
||||
<Link
|
||||
href="https://app.papermark.com/settings/upgrade"
|
||||
href="https://dataroom.hanzo.ai/settings/upgrade"
|
||||
className="font-medium text-blue-600 no-underline"
|
||||
>
|
||||
View pricing
|
||||
|
||||
@@ -25,14 +25,14 @@ const DataRoomsInformationEmail = () => {
|
||||
<Body className="mx-auto my-auto bg-white font-sans">
|
||||
<Container className="mx-auto my-10 w-[465px] p-5">
|
||||
<Text className="mx-0 mb-8 mt-4 p-0 text-center text-2xl font-normal">
|
||||
<span className="font-bold tracking-tighter">Papermark</span>
|
||||
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
|
||||
</Text>
|
||||
<Text className="mx-0 mb-8 mt-4 p-0 text-center text-2xl font-normal">
|
||||
Virtual Data Rooms
|
||||
</Text>
|
||||
<Text className="text-sm">Unlimited branded data rooms!</Text>
|
||||
<Text className="text-sm">
|
||||
With Papermark Data Rooms plan you can:
|
||||
With Hanzo Dataroom plan you can:
|
||||
</Text>
|
||||
<ul className="list-inside list-disc text-sm">
|
||||
<li>Share data rooms with one link</li>
|
||||
@@ -45,9 +45,9 @@ const DataRoomsInformationEmail = () => {
|
||||
<li>Use advanced link settings</li>
|
||||
</ul>
|
||||
<Text className="text-sm">
|
||||
All about Papermark{" "}
|
||||
All about Hanzo Dataroom{" "}
|
||||
<a
|
||||
href="https://www.papermark.com/data-room"
|
||||
href="https://dataroom.hanzo.ai/data-room"
|
||||
className="text-blue-500 underline"
|
||||
>
|
||||
Data Rooms
|
||||
@@ -57,7 +57,7 @@ const DataRoomsInformationEmail = () => {
|
||||
<Section className="my-8 text-center">
|
||||
<Button
|
||||
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
|
||||
href={`https://app.papermark.com/datarooms?utm_source=dataroom-info&utm_medium=email&utm_campaign=20240723&utm_content=upload_documents`}
|
||||
href={`https://dataroom.hanzo.ai/datarooms?utm_source=dataroom-info&utm_medium=email&utm_campaign=20240723&utm_content=upload_documents`}
|
||||
style={{ padding: "12px 20px" }}
|
||||
>
|
||||
Create new data room
|
||||
@@ -66,7 +66,7 @@ const DataRoomsInformationEmail = () => {
|
||||
<Text className="text-sm">
|
||||
If you require a fully customizable experience,{" "}
|
||||
<a
|
||||
href="https://cal.com/marcseitz/papermark"
|
||||
href="https://dataroom.hanzo.ai/contact"
|
||||
className="text-blue-500 underline"
|
||||
>
|
||||
book a call
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
type DocumentChange = {
|
||||
documentName: string;
|
||||
};
|
||||
|
||||
export default function DataroomDigestNotification({
|
||||
dataroomName = "Example Data Room",
|
||||
documents = [
|
||||
{ documentName: "Document A" },
|
||||
{ documentName: "Document B" },
|
||||
{ documentName: "Document C" },
|
||||
],
|
||||
senderEmail = "example@example.com",
|
||||
url = "https://dataroom.hanzo.ai/datarooms/123",
|
||||
preferencesUrl = "https://dataroom.hanzo.ai/notification-preferences?token=abc",
|
||||
frequency = "daily",
|
||||
}: {
|
||||
dataroomName: string;
|
||||
documents: DocumentChange[];
|
||||
senderEmail: string;
|
||||
url: string;
|
||||
preferencesUrl: string;
|
||||
frequency: "daily" | "weekly";
|
||||
}) {
|
||||
const count = documents.length;
|
||||
const periodLabel = frequency === "daily" ? "today" : "this week";
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>
|
||||
{`${count} new document${count !== 1 ? "s" : ""} in ${dataroomName} ${periodLabel}`}
|
||||
</Preview>
|
||||
<Tailwind>
|
||||
<Body className="mx-auto my-auto bg-white font-sans">
|
||||
<Container className="mx-auto my-10 w-[465px] p-5">
|
||||
<Text className="mb-8 mt-4 text-center text-2xl font-normal">
|
||||
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
|
||||
</Text>
|
||||
<Text className="font-semibold mb-8 mt-4 text-center text-xl">
|
||||
{`${count} new document${count !== 1 ? "s" : ""} in ${dataroomName}`}
|
||||
</Text>
|
||||
<Text className="text-sm leading-6 text-black">
|
||||
The following document{count !== 1 ? "s have" : " has"} been added
|
||||
to <span className="font-semibold">{dataroomName}</span>{" "}
|
||||
{periodLabel}:
|
||||
</Text>
|
||||
<Section className="my-4">
|
||||
{documents.map((doc, i) => (
|
||||
<Text
|
||||
key={i}
|
||||
className="my-1 text-sm leading-6 text-black"
|
||||
>
|
||||
• <span className="font-semibold">{doc.documentName}</span>
|
||||
</Text>
|
||||
))}
|
||||
</Section>
|
||||
<Section className="my-8 text-center">
|
||||
<Button
|
||||
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
|
||||
href={url}
|
||||
style={{ padding: "12px 20px" }}
|
||||
>
|
||||
View the dataroom
|
||||
</Button>
|
||||
</Section>
|
||||
<Text className="text-sm text-black">
|
||||
or copy and paste this URL into your browser: <br />
|
||||
{url}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">Hanzo Dataroom</Text>
|
||||
|
||||
<Hr />
|
||||
<Section className="text-gray-400">
|
||||
<Text className="text-xs">
|
||||
© {new Date().getFullYear()} Hanzo AI, Inc. All rights
|
||||
reserved.
|
||||
</Text>
|
||||
<Text className="text-xs">
|
||||
You received this {frequency} digest from{" "}
|
||||
<span className="font-semibold">{senderEmail}</span> because you
|
||||
viewed the dataroom{" "}
|
||||
<span className="font-semibold">{dataroomName}</span> on
|
||||
Hanzo Dataroom. If you have any feedback or questions about this
|
||||
email, simply reply to it.{" "}
|
||||
<a
|
||||
href={preferencesUrl}
|
||||
className="text-gray-400 underline underline-offset-2"
|
||||
>
|
||||
Manage your notification preferences
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
@@ -17,8 +17,8 @@ export default function DataroomNotification({
|
||||
dataroomName = "Example Data Room",
|
||||
documentName = "Example Document",
|
||||
senderEmail = "example@example.com",
|
||||
url = "https://app.papermark.com/datarooms/123",
|
||||
unsubscribeUrl = "https://app.papermark.com/datarooms/123/unsubscribe",
|
||||
url = "https://dataroom.hanzo.ai/datarooms/123",
|
||||
unsubscribeUrl = "https://dataroom.hanzo.ai/datarooms/123/unsubscribe",
|
||||
}: {
|
||||
dataroomName: string;
|
||||
documentName: string | undefined;
|
||||
@@ -34,7 +34,7 @@ export default function DataroomNotification({
|
||||
<Body className="mx-auto my-auto bg-white font-sans">
|
||||
<Container className="mx-auto my-10 w-[465px] p-5">
|
||||
<Text className="mb-8 mt-4 text-center text-2xl font-normal">
|
||||
<span className="font-bold tracking-tighter">Papermark</span>
|
||||
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
|
||||
</Text>
|
||||
<Text className="font-seminbold mb-8 mt-4 text-center text-xl">
|
||||
{`New document available for ${dataroomName}`}
|
||||
@@ -43,7 +43,7 @@ export default function DataroomNotification({
|
||||
A new document{" "}
|
||||
<span className="font-semibold">{documentName}</span> has been
|
||||
added to <span className="font-semibold">{dataroomName}</span>{" "}
|
||||
dataroom on Papermark.
|
||||
dataroom on Hanzo Dataroom.
|
||||
</Text>
|
||||
<Section className="my-8 text-center">
|
||||
<Button
|
||||
@@ -58,12 +58,12 @@ export default function DataroomNotification({
|
||||
or copy and paste this URL into your browser: <br />
|
||||
{`${url}`}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">Papermark</Text>
|
||||
<Text className="text-sm text-gray-400">Hanzo Dataroom</Text>
|
||||
|
||||
<Hr />
|
||||
<Section className="text-gray-400">
|
||||
<Text className="text-xs">
|
||||
© {new Date().getFullYear()} Papermark, Inc. All rights
|
||||
© {new Date().getFullYear()} Hanzo AI, Inc. All rights
|
||||
reserved.
|
||||
</Text>
|
||||
<Text className="text-xs">
|
||||
@@ -71,14 +71,13 @@ export default function DataroomNotification({
|
||||
<span className="font-semibold">{senderEmail}</span> because you
|
||||
viewed the dataroom{" "}
|
||||
<span className="font-semibold">{dataroomName}</span> on
|
||||
Papermark. If you have any feedback or questions about this
|
||||
email, simply reply to it. To unsubscribe from updates about
|
||||
this dataroom,{" "}
|
||||
Hanzo Dataroom. If you have any feedback or questions about this
|
||||
email, simply reply to it.{" "}
|
||||
<a
|
||||
href={unsubscribeUrl}
|
||||
className="text-gray-400 underline underline-offset-2"
|
||||
>
|
||||
click here
|
||||
Manage your notification preferences
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
|
||||
@@ -24,12 +24,12 @@ const DataroomTrial24hReminderEmail = ({ name }: TrialEndReminderEmail) => {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>Upgrade to Papermark Data Rooms Plan</Preview>
|
||||
<Preview>Upgrade to Hanzo Dataroom Plan</Preview>
|
||||
<Tailwind>
|
||||
<Body className="mx-auto my-auto bg-white font-sans">
|
||||
<Container className="mx-auto my-10 w-[465px] p-5">
|
||||
<Text className="mx-0 mb-8 mt-4 p-0 text-center text-2xl font-normal">
|
||||
<span className="font-bold tracking-tighter">Papermark</span>
|
||||
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
|
||||
</Text>
|
||||
<Text className="font-seminbold mx-0 mb-8 mt-4 p-0 text-center text-xl">
|
||||
Your Data Room plan trial expires in 24 hours
|
||||
@@ -38,9 +38,9 @@ const DataroomTrial24hReminderEmail = ({ name }: TrialEndReminderEmail) => {
|
||||
Hey{name && ` ${name}`}!
|
||||
</Text>
|
||||
<Text className="text-sm leading-6 text-black">
|
||||
Your Papermark Data Room plan trial expires in 24 hours.
|
||||
Your Hanzo Dataroom Data Room plan trial expires in 24 hours.
|
||||
Don't lose access to these features -{" "}
|
||||
<Link href={`https://app.papermark.com/settings/billing`}>
|
||||
<Link href={`https://dataroom.hanzo.ai/settings/billing`}>
|
||||
upgrade today
|
||||
</Link>
|
||||
:
|
||||
@@ -68,7 +68,7 @@ const DataroomTrial24hReminderEmail = ({ name }: TrialEndReminderEmail) => {
|
||||
<Section className="mb-[32px] mt-[32px] text-center">
|
||||
<Button
|
||||
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
|
||||
href={`https://app.papermark.com/settings/billing`}
|
||||
href={`https://dataroom.hanzo.ai/settings/billing`}
|
||||
style={{ padding: "12px 20px" }}
|
||||
>
|
||||
Upgrade now
|
||||
@@ -80,7 +80,7 @@ const DataroomTrial24hReminderEmail = ({ name }: TrialEndReminderEmail) => {
|
||||
<span className="text-red-500 underline">disabled</span> in 24
|
||||
hours.
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">Marc from Papermark</Text>
|
||||
<Text className="text-sm text-gray-400">Marc from Hanzo Dataroom</Text>
|
||||
<Footer />
|
||||
</Container>
|
||||
</Body>
|
||||
|
||||
@@ -29,7 +29,7 @@ const DataroomTrialEnd = ({ name }: DataroomTrialEnd) => {
|
||||
<Body className="mx-auto my-auto bg-white font-sans">
|
||||
<Container className="mx-auto my-10 w-[465px] p-5">
|
||||
<Text className="mb-8 mt-4 text-center text-2xl font-normal">
|
||||
<span className="font-bold tracking-tighter">Papermark</span>
|
||||
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
|
||||
</Text>
|
||||
<Text className="font-seminbold mb-8 mt-4 text-center text-xl">
|
||||
Your Data Room plan trial has expired
|
||||
@@ -38,10 +38,10 @@ const DataroomTrialEnd = ({ name }: DataroomTrialEnd) => {
|
||||
Hey{name && ` ${name}`}!
|
||||
</Text>
|
||||
<Text className="text-sm leading-6 text-black">
|
||||
Your Papermark Data Room trial has expired.
|
||||
Your Hanzo Dataroom Data Room trial has expired.
|
||||
<br />
|
||||
<Link
|
||||
href={`https://app.papermark.com/settings/billing`}
|
||||
href={`https://dataroom.hanzo.ai/settings/billing`}
|
||||
className="underline"
|
||||
>
|
||||
Upgrade now
|
||||
@@ -68,7 +68,7 @@ const DataroomTrialEnd = ({ name }: DataroomTrialEnd) => {
|
||||
<Section className="my-8 text-center">
|
||||
<Button
|
||||
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
|
||||
href={`https://app.papermark.com/settings/billing`}
|
||||
href={`https://dataroom.hanzo.ai/settings/billing`}
|
||||
style={{ padding: "12px 20px" }}
|
||||
>
|
||||
Upgrade now
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user