Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions app/auth/confirm/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import { createClient, createAdminClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import dayjs from "dayjs";
import { queryRateLimit } from "@/app/rateLimiter";

const ms_to_minute = 60 * 1000;
const minutes_to_ms = 60 * 1000;

export const verifyToken = async (email: string, token: string) => {
// const token = formData.get("token")?.toString();
Expand All @@ -27,6 +28,11 @@ export const verifyToken = async (email: string, token: string) => {
export const resendMagicLink = async (email: string) => {
const supabase = await createClient();

const { error: RateLimitError } = await queryRateLimit(email);
if (RateLimitError) {
return { error: RateLimitError };
}

const { error } = await supabase.auth.signInWithOtp({
email,
options: {
Expand All @@ -36,8 +42,10 @@ export const resendMagicLink = async (email: string) => {
});

if (error) {
console.error(error.code + " " + error.message);
return { error };
}

return {};
};

// TODO: decide on how much time to allow users to be able to enter code for
Expand All @@ -62,7 +70,7 @@ export const canLoadPage = async (email: string) => {
m_time.add(20, "minute"); // change this to minutes later
// console.log(dayjs());
console.log("difference", dayjs().diff(m_time));
if (dayjs().diff(m_time) < 20 * ms_to_minute) {
if (dayjs().diff(m_time) < 20 * minutes_to_ms) {
return true;
}

Expand Down
12 changes: 10 additions & 2 deletions app/auth/confirm/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export default function Confirm() {
const [value, setValue] = useState("");
const [disabled, setDisabled] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [rateLimitError, setRateLimitError] = useState<string | undefined>(
undefined
);
const { focusRef, setFocus } = useInputFocus();

useEffect(() => {
Expand Down Expand Up @@ -99,8 +102,13 @@ export default function Confirm() {
<div className="flex-1">
<Button
className="w-full"
onClick={() => {
resendMagicLink(email);
onClick={async () => {
const { error } = await resendMagicLink(email);
console.log(
"error got when click resend: ",
error?.message
);
setError(error?.message);
}}
>
Resend
Expand Down
5 changes: 4 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ export default async function Home() {
// return redirect("/signup");
// }

// Filter webring profile data
const filteredRingProfiles = ringProfiles.filter((profile) => profile.domain);

return (
<div className="min-h-screen bg-background flex flex-col">
<Navbar user={userData} />
<div className="overflow-clip">
<WebRing data={ringProfiles} />
<WebRing data={filteredRingProfiles} />
<div className="px-4">
<h2 className="max-w-[85rem] w-full mx-auto">Preview</h2>
<div className="max-w-[85rem] mx-auto overflow-clip">
Expand Down
35 changes: 35 additions & 0 deletions app/rateLimiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use server";

import { Ratelimit } from "@upstash/ratelimit"; // for deno: see above
import { Redis } from "@upstash/redis";

// Create a new ratelimiter, that allows 10 requests per 10 seconds
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(2, "20 m"), // 2 requests per 20 minutes
analytics: true,
});

const ms_to_seconds = 1 / 1000;

// Query rate limit server side from redis cache
export async function queryRateLimit(email: string) {
const { success, reset } = await ratelimit.limit(email);

// Rate limit resend
if (!success) {
const timeToTryAgain = (reset - Date.now()) * ms_to_seconds; // time to try again in seconds
const minutes = Math.floor(timeToTryAgain / 60);
const seconds = Math.floor(timeToTryAgain - minutes * 60);

return {
error: {
message: `Resend rate limit reached, try again in ${minutes.toString()}:${seconds
.toString()
.padStart(2, "0")}`,
},
};
}

return {};
}
2 changes: 1 addition & 1 deletion app/signin/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function SigninForm() {
console.log("Success");
const { error } = await signInAction(email);
if (error) {
setEmailError("Email not registered");
setEmailError(error?.message);
} else {
redirect(`/auth/confirm?email=${email}`);
}
Expand Down
1 change: 1 addition & 0 deletions app/signin/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const signInAction = async (email: string) => {
},
});

// Catch user not registered error
if (error) return { error };

return {}; // return email in auth/confirm link as a search param
Expand Down
2 changes: 1 addition & 1 deletion app/signup/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function SignupForm() {
console.log("Success");
const { error } = await signUpAction(name, email);
if (error) {
setEmailError("Email registered");
setEmailError(error?.message);
} else {
redirect(`/auth/confirm?email=${email}`);
}
Expand Down
2 changes: 1 addition & 1 deletion app/signup/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const signUpAction = async (name: string, email: string) => {

console.log("data found for user email on signup", data);
if (data?.length) {
return { error: 1 };
return { error: { message: "Email Registered" } };
}

const { error } = await supabase.auth.signInWithOtp({
Expand Down
4 changes: 2 additions & 2 deletions components/FallbackImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ export default function FallbackImage({
height={height}
src={avatar}
alt="Avatar"
className={cn("object-cover", className)}
className={cn("object-cover bg-popover", className)}
/>
) : (
<Image
src={src}
width={width}
height={height}
className={cn("object-cover", className)}
className={cn("object-cover bg-popover", className)}
alt={alt}
onError={(err) => {
console.log("[FallbackImage] Profile Image error! Switching to fallback: ", err);
Expand Down
69 changes: 52 additions & 17 deletions components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import verifiedIcon from "@/icons/verified.svg";
import { cn } from "@/lib/utils";
import { House, LayoutDashboard, LogOut } from "lucide-react";
import { DropdownMenuArrow } from "@radix-ui/react-dropdown-menu";

export default function Navbar({
user,
Expand All @@ -32,26 +36,57 @@ export default function Navbar({
</Link>
{user ? (
<DropdownMenu>
<DropdownMenuTrigger className="outline-none focus:outline-none">
<FallbackImage
width={64}
height={64}
src={user.image_url}
seed={user.ring_id}
alt={user.name + "'s Profile picture"}
className="rounded-full w-14 aspect-square border-4 border-card outline-2 outline-white"
/>
<DropdownMenuTrigger
className={cn(
"outline-none focus:outline-none"
// user.is_verified && "bg-popover rounded-t-full border-x"
)}
>
<div className="w-14 aspect-square rounded-full relative">
<FallbackImage
src={user.image_url}
seed={user.ring_id}
alt={user.name + "'s Profile picture"}
className={cn(
"rounded-full w-14 aspect-square object-cover pointer-events-none drag-none select-none",
user.is_verified && "border-1 border-white"
)}
/>
{user.is_verified && (
<Image
src={verifiedIcon}
alt="Verified"
className="absolute right-0 bottom-0 size-4"
/>
)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="text-md">
<Link href="/">
<DropdownMenuItem>Home</DropdownMenuItem>
<DropdownMenuContent
className="min-w-14 rounded-full border-t-0 py-2"
align="center"
sideOffset={10}
>
<Link href="/" className="flex mb-2" title="Home">
<DropdownMenuItem className="mx-auto rounded-full aspect-square">
<House className="size-6" />
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<Link href="/dashboard">
<DropdownMenuItem>Dashboard</DropdownMenuItem>

<Link href="/dashboard" className="flex mb-2" title="Dashboard">
<DropdownMenuItem className="mx-auto rounded-full aspect-square">
<LayoutDashboard className="size-6" />
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={signOutAction}>Sign out</DropdownMenuItem>

<div className="flex" title="Sign out">
<DropdownMenuItem
className="mx-auto rounded-full aspect-square"
onClick={signOutAction}
>
<LogOut className="size-6" />
</DropdownMenuItem>
</div>
{/* <DropdownMenuArrow className="fill-popover" /> */}
</DropdownMenuContent>
</DropdownMenu>
) : (
Expand Down
2 changes: 1 addition & 1 deletion components/ui/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md z-50",
className
)}
{...props}
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.49.4",
"@types/three": "^0.176.0",
"@upstash/ratelimit": "^2.0.6",
"@upstash/redis": "^1.35.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dayjs": "^1.11.13",
"dompurify": "^3.2.6",
"dotenv": "^16.5.0",
"eslint-plugin-unused-imports": "^4.1.4",
"framer-motion": "^12.16.0",
Expand Down
6 changes: 3 additions & 3 deletions supabase/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ password_requirements = ""

[auth.rate_limit]
# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled.
email_sent = 2
email_sent = 4000
# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled.
sms_sent = 30
# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true.
Expand Down Expand Up @@ -158,11 +158,11 @@ enable_confirmations = true
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
secure_password_change = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
max_frequency = "1s"
max_frequency = "0s"
# Number of characters used in the email OTP.
otp_length = 6
# Number of seconds before the email OTP expires (defaults to 1 hour).
otp_expiry = 3600
otp_expiry = 1200

# Use a production-ready SMTP server
# [auth.email.smtp]
Expand Down