Skip to content

Commit 54b3b12

Browse files
Merge pull request #3 from max-programming/access-limit
Access limit feature
2 parents 4d542f2 + 3fcc0c4 commit 54b3b12

File tree

16 files changed

+468
-134
lines changed

16 files changed

+468
-134
lines changed

.github/workflows/ci-cd.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ name: CI CD - Whim
33
on:
44
push:
55
branches: [main]
6+
pull_request:
7+
branches: [main]
68

79
jobs:
810
build:
@@ -26,6 +28,7 @@ jobs:
2628
name: CD - Whim Deploy
2729
runs-on: ubuntu-22.04
2830
needs: build
31+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
2932

3033
steps:
3134
- name: Setup SSH key

bun.lock

Lines changed: 59 additions & 85 deletions
Large diffs are not rendered by default.

src/components/landing/landing-hero.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Card, CardContent } from "~/components/ui/card";
33

44
export function LandingHero() {
55
return (
6-
<div className="space-y-6">
6+
<div className="space-y-9">
77
{/* Brand Badge with Whim */}
88
<div className="inline-flex items-center gap-2 bg-indigo-50 dark:bg-indigo-900/30 px-3 py-1.5 rounded-full text-indigo-700 dark:text-indigo-300 font-medium text-sm border border-indigo-100 dark:border-indigo-800">
99
<Flame className="w-4 h-4" />
@@ -61,7 +61,7 @@ export function LandingHero() {
6161
Self-Destruct
6262
</div>
6363
<div className="text-xs sm:text-xs text-slate-500 dark:text-slate-400 mt-0.5 sm:mt-1">
64-
Deleted after reading
64+
Deleted after reading (once or multiple times)
6565
</div>
6666
</div>
6767
</div>

src/components/landing/landing-process-flow.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,12 @@ export function LandingProcessFlow() {
8686
<div className="flex items-center gap-2 mb-1">
8787
<Eye className="w-4 h-4 text-indigo-600 dark:text-indigo-400" />
8888
<h3 className="font-semibold text-sm text-slate-900 dark:text-slate-100">
89-
One-Time Access
89+
Limited Access
9090
</h3>
9191
</div>
9292
<p className="text-sm text-slate-600 dark:text-slate-300">
9393
The recipient opens the link, enters the OTP, and views your
94-
secret once.
94+
secret for the allowed number of times.
9595
</p>
9696
</div>
9797
</div>
@@ -111,8 +111,8 @@ export function LandingProcessFlow() {
111111
</h3>
112112
</div>
113113
<p className="text-sm text-slate-600 dark:text-slate-300">
114-
The secret is permanently deleted from our servers immediately
115-
after being viewed.
114+
The secret is permanently deleted from our servers after all
115+
allowed accesses are used.
116116
</p>
117117
</div>
118118
</div>

src/components/landing/landing-secret-form.tsx

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ import {
4040
CardHeader,
4141
CardTitle,
4242
} from "~/components/ui/card";
43+
4344
import { useNewWhim } from "~/hooks/use-new-whim";
4445

4546
export function LandingSecretForm() {
4647
const { mutate: newWhim, isPending } = useNewWhim();
4748
const [message, setMessage] = useState("");
49+
const [maxAttempts, setMaxAttempts] = useState(1);
4850
const [isDialogOpen, setIsDialogOpen] = useState(false);
4951
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
5052
const [whimResult, setWhimResult] = useState<{
@@ -62,12 +64,13 @@ export function LandingSecretForm() {
6264
setIsConfirmDialogOpen(false);
6365

6466
newWhim(
65-
{ message },
67+
{ message, maxAttempts },
6668
{
6769
onSuccess: (data: { id: string; otp: string }) => {
6870
setWhimResult(data);
6971
setIsDialogOpen(true);
7072
setMessage(""); // Clear the form
73+
setMaxAttempts(1); // Reset to default
7174
},
7275
}
7376
);
@@ -120,14 +123,50 @@ export function LandingSecretForm() {
120123
/>
121124
</div>
122125

126+
{/* Max Attempts Selection */}
127+
<div>
128+
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">
129+
Access Limit
130+
</label>
131+
<div className="grid grid-cols-5 gap-2">
132+
{[1, 2, 3, 5, 10].map(num => (
133+
<Button
134+
key={num}
135+
type="button"
136+
variant={maxAttempts === num ? "default" : "outline"}
137+
size="sm"
138+
onClick={() => setMaxAttempts(num)}
139+
disabled={isPending}
140+
className={`text-xs ${
141+
maxAttempts === num
142+
? "bg-indigo-600 hover:bg-indigo-700 text-white"
143+
: "hover:bg-slate-50 dark:hover:bg-slate-700"
144+
}`}
145+
>
146+
{num}
147+
</Button>
148+
))}
149+
</div>
150+
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
151+
Number of times this whim can be accessed before
152+
self-destructing
153+
</p>
154+
</div>
155+
123156
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
124157
<div className="flex items-start gap-2">
125158
<Eye className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
126159
<div className="text-sm text-blue-800 dark:text-blue-200">
127-
<p className="font-medium mb-1">One-Time Access</p>
160+
<p className="font-medium mb-1">
161+
{maxAttempts === 1
162+
? "One-Time Access"
163+
: `${maxAttempts}-Time Access`}
164+
</p>
128165
<p className="text-blue-700 dark:text-blue-300">
129-
Your secret will be permanently destroyed the moment someone
130-
opens the link, even if it hasn't reached the expiry time.
166+
Your secret will be permanently destroyed after being
167+
accessed{" "}
168+
{maxAttempts === 1 ? "once" : `${maxAttempts} times`}, even
169+
if it hasn't reached the expiry time.
131170
</p>
132171
</div>
133172
</div>
@@ -164,10 +203,10 @@ export function LandingSecretForm() {
164203
</Tooltip>
165204
</CardContent>
166205

167-
<CardFooter className="justify-center pt-3 pb-5 text-slate-500 dark:text-slate-400">
206+
<CardFooter className="justify-center -mb-2 -mt-3 text-slate-500 dark:text-slate-400">
168207
<Shield className="size-3 mr-1" />
169208
<p className="text-sm">
170-
Your whim is encrypted • OTP protected • One-time access
209+
Your whim is encrypted • OTP protected • Limited access
171210
</p>
172211
</CardFooter>
173212
</Card>
@@ -185,9 +224,9 @@ export function LandingSecretForm() {
185224
</AlertDialogTitle>
186225
<AlertDialogDescription className="text-left">
187226
You're about to create a whim with the following message. This
188-
secret will be encrypted and will{" "}
189-
<strong>self-destruct immediately</strong> after being viewed
190-
once.
227+
secret will be encrypted and will <strong>self-destruct</strong>{" "}
228+
after being accessed{" "}
229+
{maxAttempts === 1 ? "once" : `${maxAttempts} times`}.
191230
</AlertDialogDescription>
192231
</AlertDialogHeader>
193232

src/hooks/use-get-whim.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useQuery } from "@tanstack/react-query";
22
import { getWhim } from "~/server/get-whim";
3-
import { deleteWhim } from "~/server/delete-whim";
3+
import { recordSuccessfulAccess } from "~/server/record-successful-access";
44
import { incrementFailedAttempts } from "~/server/increment-failed-attempts";
55
import { decryptWhim } from "~/lib/crypto-utils";
66

@@ -33,15 +33,26 @@ export function useGetWhim(id: string, otp: string) {
3333
);
3434

3535
try {
36-
await deleteWhim({ data: { id: whimId } });
37-
return { message: decryptedMessage, deletionFailed: false };
38-
} catch (deleteError) {
39-
console.error("Failed to delete whim:", deleteError);
36+
const accessResult = await recordSuccessfulAccess({
37+
data: { id: whimId },
38+
});
39+
return {
40+
message: decryptedMessage,
41+
deletionFailed: false,
42+
deleted: accessResult.deleted,
43+
remainingAttempts: accessResult.remainingAttempts,
44+
maxAttempts: encryptedWhim.maxAttempts,
45+
};
46+
} catch (accessError) {
47+
console.error("Failed to record successful access:", accessError);
4048

4149
return {
4250
message: decryptedMessage,
4351
deletionFailed: true,
44-
warning: "Secret was decrypted but may still exist on server",
52+
deleted: false,
53+
remainingAttempts: encryptedWhim.remainingAttempts,
54+
maxAttempts: encryptedWhim.maxAttempts,
55+
warning: "Secret was decrypted but access tracking failed",
4556
};
4657
}
4758
} catch (decryptError) {

src/hooks/use-new-whim.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { newWhim } from "~/server/new-whim";
44

55
export function useNewWhim() {
66
return useMutation({
7-
async mutationFn({ message }: { message: string }) {
7+
async mutationFn({
8+
message,
9+
maxAttempts = 1,
10+
}: {
11+
message: string;
12+
maxAttempts?: number;
13+
}) {
814
const otp = generateOtp();
915
const encryptedWhim = await encryptWhim(message, otp);
1016

@@ -13,6 +19,7 @@ export function useNewWhim() {
1319
encryptedMessage: Array.from(encryptedWhim.encryptedMessage),
1420
salt: Array.from(encryptedWhim.salt),
1521
iv: Array.from(encryptedWhim.iv),
22+
maxAttempts,
1623
},
1724
});
1825

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE `attempts` ADD `successful_attempts` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
2+
ALTER TABLE `whims` ADD `max_attempts` integer DEFAULT 1 NOT NULL;

0 commit comments

Comments
 (0)