Skip to content

Commit b876cc7

Browse files
committed
feat: registration gate with enterprise email validation and spam protection
- Registration page gates demo access (name, email, company, title) - Blocks free email providers (gmail, yahoo, hotmail, etc. — 50+ domains) - Honeypot field catches bots silently - Rate limiting: 3 registrations per IP per hour - Duplicate email handling via upsert - Cookie-based: registered users skip the form for 90 days - Admin endpoint GET /api/registrations to export leads - Registration model in Prisma with email uniqueness constraint
1 parent fa2f69d commit b876cc7

4 files changed

Lines changed: 371 additions & 0 deletions

File tree

editor/register.html

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>SmartDocs — Try the Editor</title>
7+
<style>
8+
* { box-sizing: border-box; margin: 0; padding: 0; }
9+
body {
10+
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
11+
background: #0f172a;
12+
color: #e2e8f0;
13+
min-height: 100vh;
14+
display: flex;
15+
align-items: center;
16+
justify-content: center;
17+
}
18+
.container {
19+
width: 100%;
20+
max-width: 440px;
21+
padding: 20px;
22+
}
23+
.card {
24+
background: #1e293b;
25+
border: 1px solid #334155;
26+
border-radius: 16px;
27+
padding: 40px 36px;
28+
}
29+
.logo {
30+
font-size: 28px;
31+
font-weight: 700;
32+
color: #fff;
33+
margin-bottom: 8px;
34+
}
35+
.logo span { color: #6366f1; }
36+
.subtitle {
37+
font-size: 14px;
38+
color: #94a3b8;
39+
margin-bottom: 32px;
40+
line-height: 1.5;
41+
}
42+
.form-group {
43+
margin-bottom: 20px;
44+
}
45+
label {
46+
display: block;
47+
font-size: 13px;
48+
font-weight: 500;
49+
color: #cbd5e1;
50+
margin-bottom: 6px;
51+
}
52+
input {
53+
width: 100%;
54+
padding: 12px 14px;
55+
font-size: 14px;
56+
border: 1px solid #334155;
57+
border-radius: 8px;
58+
background: #0f172a;
59+
color: #e2e8f0;
60+
outline: none;
61+
transition: border-color 0.15s;
62+
}
63+
input:focus {
64+
border-color: #6366f1;
65+
}
66+
input::placeholder {
67+
color: #475569;
68+
}
69+
.hp-field {
70+
position: absolute;
71+
left: -9999px;
72+
opacity: 0;
73+
height: 0;
74+
width: 0;
75+
pointer-events: none;
76+
tab-index: -1;
77+
}
78+
.submit-btn {
79+
width: 100%;
80+
padding: 14px;
81+
font-size: 15px;
82+
font-weight: 600;
83+
border: none;
84+
border-radius: 8px;
85+
background: #6366f1;
86+
color: #fff;
87+
cursor: pointer;
88+
transition: background 0.15s;
89+
margin-top: 8px;
90+
}
91+
.submit-btn:hover {
92+
background: #4f46e5;
93+
}
94+
.submit-btn:disabled {
95+
background: #334155;
96+
cursor: not-allowed;
97+
}
98+
.error {
99+
background: #7f1d1d;
100+
color: #fca5a5;
101+
padding: 10px 14px;
102+
border-radius: 8px;
103+
font-size: 13px;
104+
margin-bottom: 16px;
105+
display: none;
106+
}
107+
.footer {
108+
text-align: center;
109+
margin-top: 20px;
110+
font-size: 12px;
111+
color: #475569;
112+
}
113+
.footer a {
114+
color: #6366f1;
115+
text-decoration: none;
116+
}
117+
</style>
118+
</head>
119+
<body>
120+
<div class="container">
121+
<div class="card">
122+
<div class="logo">Smart<span>Docs</span></div>
123+
<div class="subtitle">
124+
Try our visual document editor — design templates with charts, barcodes, QR codes, and more. Generate print-ready PDFs instantly.
125+
</div>
126+
<div id="error" class="error"></div>
127+
<form id="register-form" autocomplete="off">
128+
<div class="form-group">
129+
<label for="name">Full Name</label>
130+
<input type="text" id="name" name="name" placeholder="Jane Smith" required />
131+
</div>
132+
<div class="form-group">
133+
<label for="email">Work Email</label>
134+
<input type="email" id="email" name="email" placeholder="jane@company.com" required />
135+
</div>
136+
<div class="form-group">
137+
<label for="company">Company</label>
138+
<input type="text" id="company" name="company" placeholder="Acme Corp" required />
139+
</div>
140+
<div class="form-group">
141+
<label for="title">Title (optional)</label>
142+
<input type="text" id="title" name="title" placeholder="VP of Engineering" />
143+
</div>
144+
<!-- Honeypot field — invisible to humans, bots fill it -->
145+
<input type="text" name="website" id="website" class="hp-field" tabindex="-1" autocomplete="off" />
146+
<button type="submit" class="submit-btn" id="submit-btn">Start Building Templates</button>
147+
</form>
148+
</div>
149+
<div class="footer">
150+
Open source &middot; MIT License &middot; <a href="https://github.com/snehalsurti12/smartdocs" target="_blank">GitHub</a>
151+
</div>
152+
</div>
153+
<script>
154+
// Check if already registered
155+
if (document.cookie.includes('smartdocs_registered=1')) {
156+
window.location.href = '/editor/index.html';
157+
}
158+
159+
document.getElementById('register-form').addEventListener('submit', async (e) => {
160+
e.preventDefault();
161+
const errorEl = document.getElementById('error');
162+
const btn = document.getElementById('submit-btn');
163+
errorEl.style.display = 'none';
164+
165+
const name = document.getElementById('name').value.trim();
166+
const email = document.getElementById('email').value.trim();
167+
const company = document.getElementById('company').value.trim();
168+
const title = document.getElementById('title').value.trim();
169+
const honeypot = document.getElementById('website').value;
170+
171+
if (!name || !email || !company) {
172+
errorEl.textContent = 'Please fill in all required fields.';
173+
errorEl.style.display = 'block';
174+
return;
175+
}
176+
177+
btn.disabled = true;
178+
btn.textContent = 'Registering...';
179+
180+
try {
181+
const res = await fetch('/api/register', {
182+
method: 'POST',
183+
headers: { 'Content-Type': 'application/json' },
184+
body: JSON.stringify({ name, email, company, title, website: honeypot })
185+
});
186+
const data = await res.json();
187+
if (!res.ok) {
188+
errorEl.textContent = data.error || 'Registration failed.';
189+
errorEl.style.display = 'block';
190+
btn.disabled = false;
191+
btn.textContent = 'Start Building Templates';
192+
return;
193+
}
194+
// Set cookie for 90 days
195+
document.cookie = 'smartdocs_registered=1; path=/; max-age=' + (90 * 24 * 60 * 60);
196+
window.location.href = '/editor/index.html';
197+
} catch (err) {
198+
errorEl.textContent = 'Something went wrong. Please try again.';
199+
errorEl.style.display = 'block';
200+
btn.disabled = false;
201+
btn.textContent = 'Start Building Templates';
202+
}
203+
});
204+
</script>
205+
</body>
206+
</html>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- CreateTable
2+
CREATE TABLE "Registration" (
3+
"id" TEXT NOT NULL,
4+
"name" TEXT NOT NULL,
5+
"email" TEXT NOT NULL,
6+
"company" TEXT NOT NULL,
7+
"title" TEXT,
8+
"ip" TEXT,
9+
"userAgent" TEXT,
10+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
11+
12+
CONSTRAINT "Registration_pkey" PRIMARY KEY ("id")
13+
);
14+
15+
-- CreateIndex
16+
CREATE UNIQUE INDEX "Registration_email_key" ON "Registration"("email");
17+
18+
-- CreateIndex
19+
CREATE INDEX "Registration_email_idx" ON "Registration"("email");
20+
21+
-- CreateIndex
22+
CREATE INDEX "Registration_createdAt_idx" ON "Registration"("createdAt");

prisma/schema.prisma

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ enum TemplateStatus {
1515
ARCHIVED
1616
}
1717

18+
model Registration {
19+
id String @id @default(cuid())
20+
name String
21+
email String @unique
22+
company String
23+
title String?
24+
ip String?
25+
userAgent String?
26+
createdAt DateTime @default(now())
27+
28+
@@index([email])
29+
@@index([createdAt])
30+
}
31+
1832
model Tenant {
1933
id String @id @default(cuid())
2034
name String

0 commit comments

Comments
 (0)